默认 gin.Recovery() 不返回 JSON 错误,因其仅调用 c.Writer.WriteHeader(500) 而不写响应体、不 abort,导致响应为空;应改用 gin.RecoveryWithWriter() 并在错误处理函数中调用 c.AbortWithStatusJSON()。

为什么默认 gin.Recovery() 不返回你想要的 JSON 错误
Gin 的 gin.Default() 自带 gin.Recovery(),但它只做两件事:打印 panic 堆栈、返回空白 500 响应体(HTTP 状态码是 500,但响应 body 是空的)。它不调用 c.AbortWithStatusJSON(),也不走你注册的其他中间件逻辑。用户看到的是 {"error": "Internal Server Error"} 或更糟——完全白屏。
根本原因在于:gin.Recovery() 的实现里没有写响应体,只调用了 c.Writer.WriteHeader(500),没写 body,也没 abort。所以你写的 c.JSON() 或自定义错误结构根本不会生效。
- 别在已有
gin.Recovery()的基础上再加一层 defer+recover —— 中间件执行顺序导致你的 recover 可能根本捕不到 panic - 禁用默认 recovery:改用
gin.New(),手动注册中间件,而不是依赖gin.Default() - 生产环境必须脱敏:不要直接把
debug.Stack()返回给前端,仅 dev 环境可选透出
如何用 gin.RecoveryWithWriter() 正确接管 panic 响应
gin.RecoveryWithWriter() 是 Gin 提供的可控入口,它允许你传入自定义 writer 和错误处理函数,比手写 defer+recover 更安全、更符合 Gin 生命周期。
关键点:
- 必须传入实现了
io.Writer的对象(比如os.Stderr),否则 panic 日志会丢失 - 错误处理函数接收
*gin.Context和interface{}类型的 panic 值,你要在这里调用c.AbortWithStatusJSON() - 务必检查
c.Writer.Written(),避免重复写响应头(尤其当 panic 发生在中间件链后半段时)
示例:
func CustomRecovery() gin.HandlerFunc {
return gin.RecoveryWithWriter(os.Stderr, func(c *gin.Context, err interface{}) {
if c.Writer.Written() {
return
}
c.AbortWithStatusJSON(500, gin.H{
"code": 500,
"msg": "服务暂时不可用",
"data": nil,
})
})
}
中间件里的 panic 为什么捕不到?
如果你在某个中间件(比如鉴权 Authorization())里写了 defer func() { recover() }(),但 panic 还是冒泡到顶层,说明你漏掉了关键约束:Gin 的 handler 和中间件都在同一个 goroutine 执行,但 recovery 中间件必须包裹在它外层——也就是说,你的自定义 recovery 必须注册在所有业务中间件之前。
常见错误写法:
- 把
CustomRecovery()放在r.Use(...)最后一行 → 它根本来不及运行 - 在中间件内部 recover 后只调用
c.JSON()却没c.Abort()→ 后续中间件继续执行,可能二次写响应体,触发http: multiple response.WriteHeader calls - panic 发生在 goroutine 里(比如
go func() { ... }())→recover()捕不到,因为不在同个 goroutine
正确做法:所有中间件都注册在 CustomRecovery() 之后;若中间件内必须启 goroutine,需在 goroutine 内部单独 recover 并上报错误(如发 metrics 或日志),但不能写 HTTP 响应。
业务 error 和 panic 必须分开处理
panic 是程序崩溃信号,recover() 只负责兜底;而业务 error(如参数校验失败、DB 查询为空)应该主动抛出,用 c.Error(err) 注入上下文,再由统一 ErrorHandler 中间件提取并响应。
注意:
-
c.Error()不会中断流程,必须搭配c.Abort()或提前return,否则后续逻辑仍会执行 - ErrorHandler 中间件要放在所有业务中间件之后、recovery 之前,才能读取到
c.Errors里的第一个非 nil error - 别混用
c.JSON()和http.Error(),否则前端要处理两种错误格式
真正容易被忽略的是:panic 恢复后,c.Request.Body 已关闭,c.GetHeader() 仍可用,但不能再读 body。任何依赖原始请求体的逻辑(比如重放、签名验证)必须在 panic 发生前完成。


















