revel无开箱即用全局panic捕获,必须在before拦截器中手动添加defer recover(),且仅对当前goroutine生效;异步goroutine需各自封装recover,业务错误应显式返回revel.result而非panic。

Revel 的全局异常捕获只靠中间件,recover 必须在每个请求 goroutine 入口手动加
Revel 没有类似 Spring 的 @ControllerAdvice 或 Gin 的 gin.Recovery() 那种开箱即用的全局 panic 捕获机制。它默认不 wrap 任何 handler,所以 panic 会直接 crash 当前 goroutine,导致连接中断、日志丢失、监控断点。真正能兜住请求级 panic 的,只有在请求生命周期起始处显式加 defer recover() —— 而 Revel 的入口是拦截器(interceptor)或控制器方法本身。
推荐做法是在一个 BEFORE 拦截器里统一加:
func RecoverInterceptor(c *revel.Controller) revel.Result {
defer func() {
if r := recover(); r != nil {
c.Response.Status = 500
c.RenderError(fmt.Sprintf("server error: %v", r))
revel.ERROR.Printf("Panic recovered: %+v", r)
}
}()
return nil
}
然后注册到 app/init.go 中:
revel.Interceptors = append(revel.Interceptors, revel.BEFORE, RevelInterceptor)
注意两点:
-
recover()只对当前 goroutine 有效;异步启动的go func() { ... }()必须各自包一层defer recover(),否则 panic 会静默消失 - 不要在
FINALLY或AFTER拦截器里做 recover —— 此时响应可能已写入,再设 status 或 body 会触发http: multiple response.WriteHeader calls - 若用了自定义的异步任务封装(如
AsyncTask),必须在 goroutine 内部做 recover,不能指望外层拦截器
业务错误不该靠 panic,要用 revel.Result 显式返回
Revel 的设计哲学是「错误即流程」,panic 应仅用于不可恢复的编程错误(如 nil pointer dereference、map write on nil),而非业务校验失败。比如参数缺失、权限不足、资源不存在,应该走 c.RenderError(err)、c.RenderJSON() 或自定义 revel.Result 实现,而不是 panic("user not found")。
这样做的好处很实在:
- 避免误触发 recover 日志,干扰真实崩溃排查
- HTTP 状态码可控(
RenderError默认 500,但你可以写c.Response.Status = 404再 render) - 前端能稳定解析 JSON 错误体,不需要额外约定 panic 字符串格式
- 配合
revel.Validation的Validate()方法,可自然沉淀出统一错误响应结构
第三方调用超时、网络错误等要包装成业务错误,别让它冒泡成 panic
Revel 本身不干预下游调用,但你写的 HTTP client、DB 查询、Redis 操作如果没做容错,很容易把 context.DeadlineExceeded、net.OpError 这类底层错误直接抛到 controller 层——而这些错误一旦未被 if err != nil 检查,就可能在解引用时引发 panic。
正确姿势是提前拦截并转义:
resp, err := http.DefaultClient.Do(req)
if err != nil {
if errors.Is(err, context.DeadlineExceeded) {
return c.RenderError("service timeout").(revel.Result)
}
if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
return c.RenderError("upstream timeout").(revel.Result)
}
return c.RenderError("network error").(revel.Result)
}
关键点:
- 永远不要让
error值未经检查就进入 struct field 赋值或 JSON marshal 流程 - 数据库查询建议用
rows.Err()补检,因为Scan()成功不代表迭代完成 - 调用外部服务时,务必设置
context.WithTimeout,否则 goroutine 可能永久挂起
生产环境必须关掉 revel.DevMode,否则 panic 会暴露堆栈到 HTML 页面
Revel 在开发模式下会把 panic 堆栈原样渲染进 HTML 错误页,这对生产环境是严重安全风险 —— 泄露路径、变量名、依赖版本,甚至可能包含敏感字段内容。即使你写了 recover,只要 revel.DevMode = true,框架仍会在 recover 失败时 fallback 到默认 panic 页面。
确认方式:检查 conf/app.conf 中是否含:
revel.dev.mode=false
同时建议加一道保险:
- 在
app/init.go的init()函数里,用revel.Config.BoolDefault("revel.dev.mode", false)强制覆盖 - CI/CD 流水线中加入检查脚本,禁止
app.conf提交含revel.dev.mode=true - nginx 层配置
error_page 500 /50x.html,防止上游未捕获 panic 导致空白响应
真正的容灾不是靠 catch 住所有 panic,而是让 panic 尽可能少发生 —— 控制器里每行代码都该清楚自己是否可能 panic,异步逻辑必须自带 recover,外部依赖必须有超时和错误分类。漏掉任意一个 goroutine,就等于留了一个无法监控的崩溃点。











