Gin 默认 gin.Recovery() 仅打印日志并返回空白500,无法满足生产环境对错误结构、堆栈记录、状态码语义和响应脱敏的要求,必须手写自定义 Recovery 中间件,并确保 c.Next() 在 defer 内执行。

直接上结论:Gin 的 gin.Recovery() 默认只打印日志、返回空白 500,不能满足生产环境对错误结构、堆栈记录、状态码语义和响应脱敏的要求——必须手写自定义 Recovery 中间件,并严格保证 c.Next() 在 defer 作用域内执行。
为什么默认的 gin.Recovery() 拦不住 panic?
它确实能捕获 panic,但有三个硬性限制:
-
gin.Recovery()只作用于主 goroutine,你在go func() {}()里触发的 panic 它完全看不见 - 它不调用
c.AbortWithStatusJSON(),而是用http.Error()写响应,后续中间件仍可能被执行(比如日志中间件还会继续记一条“成功”) - 它不输出堆栈,也不提供 error 类型判断能力,
panic("xxx")和panic(errors.New("yyy"))都被当成字符串处理,无法统一归因
自定义 Recovery 中间件必须写的三件事
一个可用的 Recovery 中间件不是“加个 defer 就完事”,得覆盖真实场景中的关键动作:
- 调用
debug.Stack()获取完整堆栈,开发环境可透出,生产环境必须截断或仅存日志 - 检查
c.Writer.Written(),避免多次 panic 导致重复写响应(否则 HTTP body 会拼出两个 JSON,客户端解析失败) - 用
c.AbortWithStatusJSON(500, ...)而非c.JSON(500, ...)+c.Abort(),前者自动触发c.Abort()并阻止后续中间件,后者漏掉c.Abort()就可能继续执行鉴权、日志等逻辑
goroutine 里的 panic 怎么办?
HTTP 请求生命周期外的 panic,Recovery 中间件天然无效。常见于异步任务、定时器回调、数据库连接池初始化等场景:
- 绝对不要在
go func() { c.JSON(...) }()里操作c—— 这会导致write on closed responsepanic,且无法被捕获 - 真要异步,必须在子 goroutine 内部自己加
defer recover(),例如:go func() { defer func() { if r := recover(); r != nil { log.Printf("async panic: %v", r) // 上报监控,不操作 c } }() // 业务逻辑,不碰 c }() - 中间件本身如果含异步逻辑(如鉴权时查 Redis),也要在中间件函数体内加独立
defer recover(),否则 panic 会逃逸到 Recovery 外层
容易被忽略的注册顺序问题
中间件注册顺序决定执行链,Recovery 必须在所有可能 panic 的中间件之后注册,否则它根本“看”不到那些 panic:
- 错误写法:
r.Use(Recovery(), AuthMiddleware(), Logger())→AuthMiddleware里的 panic 发生在 Recovery 作用域外 - 正确写法:
r.Use(Logger(), AuthMiddleware(), Recovery())→ Recovery 包裹全部前置中间件和 handler - 特别注意:
router.NoRoute()和router.NoMethod()注册的 handler 不受 Recovery 影响,它们在中间件链之外,需单独加 recover
最常踩的坑不是不会写 recover(),而是误以为「加了中间件就万事大吉」——实际中,panic 可能藏在 goroutine、第三方库回调、甚至 init() 函数里;而 Recovery 只管住请求主流程这一段。别指望一个中间件兜底所有崩溃,得按 panic 发生的位置分层防御。


















