gin 默认在生产模式下自动recover panic,仅记录日志并返回500,不透出堆栈、不触发自定义错误处理;其内置recover位于最外层handler,导致用户中间件中的recover无效。

为什么 Gin 的 panic 默认不被捕获
Gin 在生产模式下会自动 recover 掉 panic,但只做日志记录并返回 500,不透出堆栈、不触发自定义错误处理逻辑。这意味着你无法在中间件里用 recover() 拿到 panic 值,因为 Gin 自己先抢跑了——它在最外层的 http.HandlerFunc 里做了 defer recover(),且没暴露出来。
常见错误现象:你在中间件里写 defer func() { if r := recover(); r != nil { log.Println(r) } }(),结果 panic 依然被 Gin 吞掉,你的日志一条都不打。
- 真正生效的 recover 必须放在 Gin 内部 handler 执行链的“最外层”,也就是比 Gin 自带 recover 更早的位置
- Gin 的
gin.Recovery()中间件是可替换的,但它默认启用;如果你手动调用r.Use(gin.Recovery()),就等于叠加了两层 recover,容易干扰调试 - 开发时建议关掉默认 Recovery:
gin.SetMode(gin.DebugMode),否则连 panic 堆栈都看不到
如何写一个可替代 gin.Recovery 的 panic 捕获中间件
核心思路:不用 Gin 默认的 gin.Recovery(),自己实现一个,在里面做日志、上报、响应封装,并且确保它处于整个 handler 链的最外层(即第一个 Use())。
关键点在于,必须在 c.Next() 前 defer recover,且不能让后续中间件或 handler 影响这个 defer 的执行时机:
立即学习“go语言免费学习笔记(深入)”;
- panic 发生时,Go 会按 defer 栈逆序执行,所以你的 recover 必须是第一个注册的 defer
- 不要在 recover 里调用
c.Abort()或修改c.Writer后还继续c.Next(),否则可能 panic 二次发生 - 恢复后务必调用
c.AbortWithStatus(500)或手动写响应,避免 Gin 继续执行后续 handler 导致状态错乱
简短示例:
func PanicRecovery() gin.HandlerFunc {
return func(c *gin.Context) {
defer func() {
if err := recover(); err != nil {
log.Printf("PANIC: %+v\n", err)
c.AbortWithStatus(500)
// 可选:上报 Sentry / Prometheus
// metrics.PanicCounter.Inc()
}
}()
c.Next()
}
}
使用时确保它是第一个中间件:r.Use(PanicRecovery()),再跟 r.Use(gin.Logger()) 等。
recover 后怎么拿到完整堆栈和请求上下文
recover() 只返回 panic 的值(比如 interface{}),没有堆栈。要获取真实 panic 跟踪,得用 debug.PrintStack() 或 runtime/debug.Stack(),但后者更可控,支持捕获并格式化。
常见误区:直接 log.Printf("%+v", err),对自定义 error 类型可能只打出字段,漏掉 panic 位置;对字符串 panic(如 panic("xxx"))则完全看不出哪行代码崩的。
- 用
stack := debug.Stack()获取当前 goroutine 完整堆栈,注意它包含 recover 调用点,需截掉前几行 - 把
c.Request.URL.Path、c.Request.Method、c.ClientIP()一起记日志,方便定位问题请求 - 避免在 panic 处理中调用可能再次 panic 的函数(如未加锁的 map 写入、空指针解引用)
示例片段:
if err := recover(); err != nil {
buf := debug.Stack()
log.Printf("PANIC at %s %s | %s\n%s", c.Request.Method, c.Request.URL.Path, err, buf)
c.AbortWithStatus(500)
}
线上环境要注意的兼容性与性能细节
recover 是 Go 运行时机制,本身开销极小,但频繁 panic 会拖慢服务——这不是中间件的问题,而是业务逻辑该修复。真正影响线上的是日志和上报行为。
- 不要在 recover 里做同步网络请求(如直连 Sentry SDK),失败会阻塞整个请求生命周期;改用异步 channel + worker 模式
- 高 QPS 场景下,
debug.Stack()分配内存较多,可考虑用runtime.Caller()获取顶层 panic 位置,减少内存压力 - Gin v1.9+ 对
gin.RecoveryWithWriter()支持自定义 writer,但如果你自己写中间件,就别依赖它——它仍走 Gin 内置 recover 流程,绕不开限制
最容易被忽略的一点:HTTP/2 场景下,panic 可能发生在流处理中途,此时 c.AbortWithStatus() 不一定可靠,更稳妥的是用 c.Status(500) + c.Writer.WriteHeaderNow() 强制刷新头。










