panic恢复中间件必须用defer包裹recover(),且仅在Gin handler的defer中生效;需调用c.AbortWithStatusJSON()终止流程并安全返回错误,避免敏感信息泄露和路由误捕获。

panic 恢复中间件必须用 recover() 且只能在 defer 中生效
Go 的 recover() 不是 try-catch,它只在 defer 函数里、且当前 goroutine 发生 panic 后才有效。Gin 的 handler 是在独立 goroutine(或 Gin 自己的请求协程)中执行的,所以恢复逻辑必须写在 handler 执行路径上的 defer 里——中间件就是最自然的位置。
常见错误是把 recover() 写在普通函数里,或者没包在 defer 中,结果 panic 依然向上冒泡,服务直接 500 或 crash。
- 必须用
defer func() { if r := recover(); r != nil { ... } }()包裹 - 不能只写
recover()而不接defer,那等于没写 - Gin 的
c.Next()之后才可能 panic,所以defer要放在c.Next()前面,但闭包里要等 panic 发生后才执行
func Recovery() gin.HandlerFunc {
return func(c *gin.Context) {
defer func() {
if err := recover(); err != nil {
// 这里才能捕获到 panic
c.AbortWithStatusJSON(500, gin.H{"error": "internal server error"})
}
}()
c.Next()
}
}
不要直接返回 panic 值,要区分类型并过滤敏感信息
panic 可能是 string、error、自定义 struct,甚至 nil。直接 fmt.Sprintf("%v", err) 返回给前端,容易泄露文件路径、变量名、数据库连接串等——尤其在生产环境开启 debug 模式时极危险。
更糟的是,有些 panic 来自第三方库(比如 json 解析失败、空指针解引用),它们的底层值可能含内部状态,不该透出。
立即学习“go语言免费学习笔记(深入)”;
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 永远用
fmt.Sprint(err)或显式类型断言,避免%+v泄露堆栈 - 对已知错误类型(如
*json.UnmarshalTypeError)做特判,转成用户友好的提示 - 生产环境强制忽略 panic 值内容,统一返回泛化错误,例如
"something went wrong" - 开发环境可额外记录完整 panic + 堆栈到日志,但绝不返回给 HTTP 响应
Gin 的 c.Abort() 和 c.AbortWithStatusJSON() 行为差异很关键
panic 恢复后,请求流程不能继续往下走,否则可能重复写响应体、触发多次 DB 操作,甚至 panic 二次发生。这时候必须中断 Gin 的 middleware 链,但方式选错会导致 500 不返回、响应体为空、或 Content-Type 错乱。
c.Abort() 只是跳过后续中间件,不终止响应;而 c.AbortWithStatusJSON() 会设置状态码、Content-Type,并写入 JSON,同时调用 c.Abort()。漏掉这个,就只剩个空 200 或上层默认 500。
- 恢复 panic 后必须调用
c.AbortWithStatusJSON()或至少c.AbortWithError(),不能只 log 就完事 - 别在恢复逻辑里调
c.JSON()+c.Abort()分两步,有竞态风险:万一其他中间件也写了响应头,JSON 会失败 - 如果需要自定义错误结构(比如带
code字段),用c.AbortWithStatusJSON(500, yourErrStruct),别手写http.Error
全局 recovery 中间件要避开健康检查和静态资源路由
加在 router.Use(Recovery()) 确实省事,但会导致 /healthz 或 /static/xxx.js 出错时也走 recovery,掩盖真实问题。比如静态文件 404 被误当成 panic 处理,返回 500 而非 404,前端监控全乱。
更隐蔽的问题是:某些中间件(如 JWT 验证)提前 abort 并返回 401,但 recovery 仍会执行——因为 c.Next() 已调用,只是后续 handler 没跑。这会让 recovery 日志里刷满“假 panic”。
- 把 recovery 放在业务路由组里,而不是全局:
api := r.Group("/api"); api.Use(Recovery()); api.GET(...) - 健康检查路由(如
/health)单独挂,不走 recovery - 静态文件用
r.Static()时,Gin 内部已处理 404,不会 panic,无需 recovery 干预 - 如果用了
gin.Error主动 abort,不属于 panic,recovery 不该也不必捕获它
真正难搞的是嵌套 panic:比如 handler 里 defer 捕获了 panic,又在 defer 里 panic 了。这种得靠日志上下文和 panic 嵌套深度判断,不是加一层 middleware 就能兜住的。

















