默认 gin.recovery() 不返回 json 错误,因为它仅打印 panic 堆栈并调用 c.abort() 返回空白 500 响应,不调用 c.json() 或走其他中间件逻辑;必须禁用默认 recovery,手写中间件并在 recover 后立即调用 c.abortwithstatusjson()。

为什么默认 gin.Recovery() 不返回你想要的 JSON 错误
因为 gin.Recovery() 只做两件事:打印 panic 堆栈、调用 c.Abort() 并返回空白 500 响应。它不调用 c.JSON() 或 c.AbortWithStatusJSON(),也不走你注册的其他中间件逻辑——请求链在它这里就断了,后续 handler 和错误处理完全被跳过。
必须禁用默认 recovery,手写中间件并立即写响应
用 gin.New() 替代 gin.Default(),然后手动注册自定义 recovery 中间件。关键点是:recover() 后必须立刻调用 c.AbortWithStatusJSON(),不能先 c.JSON() 再 c.Abort(),否则可能被后续中间件重复写响应,导致 HTTP body 拼接出两个 JSON。
-
c.Writer.Written()要检查,避免多次写响应(尤其在嵌套中间件场景) - panic 类型可能是
string、error或其他类型,统一转成脱敏字符串,别直接fmt.Sprintf("%v", err) - 开发环境可加
"stack": string(debug.Stack())字段,生产环境必须删掉
中间件内部 panic 无法被全局 recovery 捕获
如果某个中间件(比如鉴权 Authorization())里发生 panic,而它又没自己包 defer/recover,那么这个 panic 会穿透到外层全局 recovery;但一旦你在该中间件里启动了 goroutine(比如异步日志、RPC 调用),那个 goroutine 的 panic 就完全捕获不到——recover() 只对当前 goroutine 有效。
- 高风险中间件(如 JWT 解析、DB 查询封装)建议自带
defer/recover,按业务语义返回对应状态码(如401、403) - goroutine 内部必须自己 recover,且不能依赖
c(已超出请求生命周期),只能打日志或发告警 - 所有中间件里的
c.Abort()后必须紧跟return,否则函数继续执行,可能触发二次 JSON 输出
别把 panic 当业务错误用
panic 应只用于不可恢复的程序异常(空指针、数组越界、类型断言失败),而不是“用户 token 过期”“参数缺失”这类业务逻辑错误。后者应该用 c.Error() + 自定义 error 类型 + 统一错误处理中间件来分流返回,否则会让 recovery 中间件过度承担本不属于它的职责。
真正难搞的从来不是怎么 catch panic,而是怎么让 panic 少出现——比如用 err != nil 显式判断代替裸奔调用,用 interface{} 断言前先做 if v, ok := x.(MyType); ok 检查。











