beego v2 的 panic 恢复唯一可控入口是 bconfig.recoverfunc,必须在 main() 初始化时替换,用于捕获主请求 goroutine panic 并手动调用 debug.stack() 上报堆栈;子 goroutine 需各自 defer/recover,recoverfunc 不覆盖其 panic。

Beego v2 的 RecoverFunc 是唯一可控入口
Beego v2 不提供类似 Gin 的 c.Next() 或中间件链式调用,也没有公开的全局 panic 拦截钩子。它的 panic 恢复逻辑完全由 BConfig.RecoverFunc 控制,且默认值是私有函数 defaultRecoverPanic。你无法通过常规中间件注入 defer/recover,因为中间件执行不包裹控制器方法体——它只是串行调用,不是嵌套作用域。
所以真正能改、也必须改的地方,只有这一处:BConfig.RecoverFunc。它会在每个请求 goroutine 的末尾被框架主动调用,前提是该 goroutine 确实发生了 panic。
- 必须在
main()初始化阶段就替换,不能等路由注册完再改 - 替换前建议先保存原函数(
oldFunc),以便对非 panic 场景回退处理 - 不要试图在控制器里自己写
defer recover()——这只能捕获本方法内 panic,且和框架恢复逻辑冲突,容易重复响应或状态码错乱
自定义 RecoverFunc 必须手动提取堆栈并上报
框架默认的 defaultRecoverPanic 只做两件事:设 500 状态码 + 写错误页,不记录日志、不输出堆栈、不上报监控。如果你只替换函数但漏掉 debug.Stack(),线上出问题时连哪行代码 panic 都看不到。
正确做法是在 recover() 后立刻调用 runtime/debug.Stack(),而不是只打印 err:
if err := recover(); err != nil {
stack := debug.Stack() // ← 关键!不能省
log.Printf("PANIC in request %s %s: %v\n%s", ctx.Input.Method(), ctx.Input.URI(), err, stack)
// 此处可加告警上报、指标打点等
}
-
debug.Stack()返回的是完整调用栈字节切片,需转string才能打印 - 别用
log.Fatal或os.Exit——这会让整个进程退出,不是单个请求 - 如果用了 request ID(如通过中间件注入),记得从
ctx.Input.Data或自定义字段里取出来,和堆栈一起打日志
区分 panic 和业务 error,别让 RecoverFunc 处理所有错误
RecoverFunc 只应处理真正的 panic,不是业务错误。Beego 控制器方法返回 error 是合法的(比如 return errors.New("invalid param")),这种错误不该进 recover(),而应由上层统一响应包装器处理。
常见混淆点:
- 把参数校验失败、数据库查不到、权限不足等写成
panic("user not found")——这是反模式,会掩盖真实错误类型,且无法映射到 404/401 等状态码 - 在
RecoverFunc里对所有err做字符串匹配(如strings.Contains(err.Error(), "not found"))——一旦 error 被包装(fmt.Errorf("wrap: %w", err)),匹配就失效 - 没做类型断言,直接把
err.(bresp.PageError)当万能解——非PageError类型 panic 会触发二次 panic
真正该进 RecoverFunc 的,只有那些你完全无法预判的底层崩溃:模板渲染空指针、第三方库内部 panic、未初始化 map 写入、反射调用失败等。
goroutine 内 panic 必须单独 recover,框架不负责
Beego 的 RecoverFunc 只作用于主请求 goroutine。如果你在控制器里起了子 goroutine(比如异步发邮件、写日志、调用外部 API),它里面 panic,RecoverFunc 完全感知不到——连接静默断开,日志里连痕迹都没有。
解决办法只有一个:每个子 goroutine 自己包一层 defer/recover:
go func() {
defer func() {
if r := recover(); r != nil {
stack := debug.Stack()
log.Printf("Async panic in %s: %v\n%s", "send-email", r, stack)
// 上报监控、发告警,但别往 HTTP 响应里写东西(c 已不可用)
}
}()
sendEmail(...)
}()
- 别写
go sendEmail()就完事,必须显式包裹 - 子 goroutine recover 后禁止调用
ctx.Output.*或任何涉及响应写的操作 - 推荐封装一个
safeGo()工具函数,统一做 recover + 日志 + 上报,避免重复代码
最易被忽略的是:Beego 的 RecoverFunc 看似“全局”,其实只管主线程;子 goroutine 的稳定性,得靠你自己每一处 go 前手动加固。











