revel 框架需通过 interceptor + panicfilter + 自定义 result 实现统一错误处理,覆盖 c.rendererror()、未捕获 panic、拦截器提前返回三类出口,而非仅依赖 panicfilter。

Revel 框架本身不提供开箱即用的全局异常处理器,但通过 Interceptor + PanicFilter + 自定义 Result 就能实现真正可用的统一错误处理——关键不是“捕获所有 panic”,而是把业务错误、系统错误、HTTP 错误三类路径收口到同一套响应结构里。
为什么不能只依赖 PanicFilter
PanicFilter 只捕获未被处理的 panic,对主动返回的 c.RenderError()、c.Abort(400) 或业务校验失败(如 validation.Error)完全无感。线上常见问题:前端看到 500 页面,日志却没 panic 记录;或参数校验失败返回了原始 Go 错误字符串,而非结构化 JSON。
必须同时覆盖以下三类出口:
- 显式调用
c.RenderError(err)或c.RenderJSON(400, ...) - 未被捕获的 panic(由
PanicFilter处理) - 拦截器中提前返回
revel.Result(如权限检查失败)
在 BEFORE 拦截器中注入统一错误上下文
Revel 的 BEFORE 拦截器是预处理入口,适合做错误兜底初始化和请求上下文增强。比如为每个请求绑定一个 errorCh channel,后续业务逻辑可往里发错误,由统一拦截器收口:
func InitErrorContext(c *revel.Controller) revel.Result {
// 创建带缓冲的 error channel,避免 goroutine 阻塞
c.Params.Set("errorCh", make(chan error, 1))
return nil
}
func init() {
revel.InterceptFunc(InitErrorContext, revel.BEFORE)
}
这样控制器里就能安全地异步抛错:c.Params.Get("errorCh").(chan error) ,再由另一个 <code>FINALLY 拦截器统一收集并渲染。
注意点:
- 不要在
BEFORE中直接写c.RenderJSON,否则会跳过后续拦截器和 action -
c.Params.Set是线程安全的,但 channel 必须带缓冲,否则控制器内 goroutine 可能永久阻塞 - channel 不能跨请求复用,必须每个请求新建
用 FINALLY 拦截器统一输出结构化错误
FINALLY 是唯一能确保执行的拦截阶段——无论 action 正常返回、panic 还是提前 abort,它都会运行。这里最适合做最终响应格式标准化:
func UnifiedErrorOutput(c *revel.Controller) revel.Result {
// 1. 检查是否已有 Result(说明 action 已返回)
if c.Result != nil {
return nil
}
// 2. 检查 errorCh 是否有未处理错误
if ch, ok := c.Params.Get("errorCh").(chan error); ok && len(ch) > 0 {
err := <p>这个方案绕开了 Revel 原生 <code>ValidationFilter</code> 和 <code>FlashFilter</code> 的耦合,也避免了修改框架源码。真正的难点在于:如何让业务代码“愿意”往 <code>errorCh</code> 里写错误,而不是直接 <code>return c.RenderError</code> ——这需要团队约定和 code review 卡点。</p><h3>HTTP 状态码与业务错误码必须分离</h3><p>很多团队把 <code>400</code>、<code>401</code>、<code>500</code> 直接当业务错误码用,导致前端无法区分“参数错”和“token 过期”。正确做法是:</p>
- HTTP 状态码只表达传输/协议层问题(4xx/5xx),不承载业务语义
- 所有接口统一返回
200,错误信息放code字段(如"code": "USER_NOT_FOUND") - 只有真正无法恢复的系统级错误(数据库连不上、redis 超时)才用
500状态码
Revel 的 c.RenderJSON(status, data) 默认会设置 HTTP 状态码,容易误用。建议封装一个 c.UnifiedJSON(data) 方法,强制忽略 status 参数,只由 FINALLY 拦截器决定最终状态码——这才是统一错误处理最易被忽略的底层约束。











