beego v2 全局 panic 捕获唯一可靠入口是 bconfig.recoverfunc 钩子,因中间件为顺序调用、无调用栈回溯,defer recover 仅捕获自身 panic;须在 beego.run() 前注册 customrecover 并兼容默认逻辑,且不处理子 goroutine panic。

Beego v2 没有类似 Gin 的 Next() 或中间件 panic 捕获机制,靠写 defer recover() 在自定义中间件里是无效的——因为 Beego 的中间件执行模型不支持在链路中“中断并回溯”。真正能全局捕获 panic 的入口,只有框架预留的 RecoverFunc 钩子。
为什么中间件里 defer recover() 不起作用
Beego v2 的中间件是“顺序调用 + 独立作用域”,每次中间件执行完就退出,不会像 Express/Gin 那样形成一个可被 defer 捕获的函数调用栈。你在中间件里写:
func PanicMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
// 这里永远收不到 Controller 里的 panic
}
}()
next.ServeHTTP(w, r)
})
}
这段代码只能捕获中间件自身逻辑的 panic,Controller 里的 panic 已经脱离该 goroutine 上下文,根本进不来。
正确做法:替换 BConfig.RecoverFunc
Beego v2 在启动时会检查 beego.BConfig.RecoverFunc,一旦非 nil,就会在每个请求的顶层 panic 恢复点调用它。这是唯一可靠的全局异常出口。
- 必须在
main.go初始化阶段(beego.Run()前)设置,否则无效 - 不能直接赋值匿名函数,因为要保留原始行为(比如日志、状态码设置)
- 需保存旧函数,对非预期 panic 回退到默认处理,避免破坏框架基础逻辑
示例代码:
var oldRecoverFunc func(*context.Context, *web.Config)
func initRecover(cfg *web.Config) {
oldRecoverFunc = cfg.RecoverFunc
cfg.RecoverFunc = customRecover
}
func customRecover(ctx *context.Context, cfg *web.Config) {
if err := recover(); err != nil {
switch e := err.(type) {
case bresp.PageError:
ctx.Output.SetStatus(e.Code)
ctx.WriteString(e.Message)
case error:
// 非 PageError,交还给默认逻辑(比如打印堆栈、返回 500)
if oldRecoverFunc != nil {
oldRecoverFunc(ctx, cfg)
}
}
}
}
然后在 main.go 中调用:
func main() {
beego.BConfig.RecoverFunc = nil // 确保未被提前覆盖
initRecover(beego.BConfig)
beego.Run()
}
如何抛出可被识别的业务异常
不要直接 panic("xxx"),那样进不到 customRecover 的 bresp.PageError 分支。必须用 Beego 内置的错误类型:
-
panic(bresp.NewPageError(400, "参数缺失"))→ 返回 400 + 纯文本 -
panic(bresp.NewJSONError(500, "数据库异常"))→ 返回 500 + JSON 包裹的 message - 自定义错误需实现
bresp.PageError接口,否则会被当作普通 error 交由默认逻辑处理
注意:bresp.PageError 的 Code 字段决定 HTTP 状态码,Message 决定响应体内容——这两者必须显式控制,不能依赖 panic 字符串自动解析。
容易忽略的关键细节
RecoverFunc 是 per-request 的,但它的注册时机极早,且只读取一次 BConfig。如果你在运行时动态修改 BConfig.RecoverFunc(比如根据环境切不同策略),不会生效。所有定制逻辑必须在 beego.Run() 前完成绑定。另外,这个钩子不捕获异步 goroutine 中的 panic,比如你在 Controller 里启了个 go func() { panic(...) },它照样会让进程 crash。











