gin.default() 的 panic 处理不返回自定义 json 错误,因为其内置 gin.recovery() 仅打印堆栈并返回空白 500 响应,不调用 c.abortwithstatusjson(),也不触发后续中间件;必须禁用默认 recovery,改用自定义中间件并在全局注册于日志之后、其他中间件之前。

为什么 gin.Default() 的 panic 处理不返回你定义的 JSON 错误
因为 gin.Recovery() 默认只做两件事:打印 panic 堆栈、返回空白 500 响应。它不会调用 c.AbortWithStatusJSON(),也不触发你写的其他中间件或错误处理逻辑——请求链在 panic 后直接终止,根本没机会走到你的 handler 或自定义错误响应逻辑里。
常见错误现象:panic("user not found") 发生后,前端收到的是纯文本 500 页面,而不是你期望的 {"code":500,"msg":"internal server error"};日志里能看到堆栈,但业务侧无法统一脱敏或添加 traceID。
- 不要依赖
gin.Default()自带的 recovery,它只是“保命”,不是“友好报错” - 若要用自定义 JSON 错误,必须禁用默认
gin.Recovery(),改用自己写的中间件 -
gin.New()是起点,之后再手动Use()你需要的中间件(包括日志、CORS、自定义 recovery)
如何写一个能返回结构化 JSON 的 recovery 中间件
核心是 defer + recover() 捕获 panic,然后立即用 c.AbortWithStatusJSON() 写响应。不能用 c.JSON() + c.Abort(),否则后续中间件可能再次执行,导致重复写响应或 panic。
示例关键逻辑:
func CustomRecovery() gin.HandlerFunc {
return func(c *gin.Context) {
defer func() {
if err := recover(); err != nil {
// 生产环境必须脱敏,禁止返回原始 panic 字符串
c.AbortWithStatusJSON(500, gin.H{
"code": 500,
"msg": "internal server error",
"data": nil,
})
}
}()
c.Next()
}
}
- 务必调用
c.AbortWithStatusJSON(),不是c.JSON()+c.Abort() -
recover()只捕获当前 goroutine 的 panic,Gin handler 是单 goroutine,安全 - 类型判断(
err.(type))不是必须,但建议统一用自定义 error 类型,方便后续扩展错误码
注册自定义 recovery 的正确顺序和位置
中间件注册顺序决定执行顺序。CustomRecovery() 必须放在所有可能 panic 的中间件和 handler 之前,但通常应紧挨着 gin.Logger() 之后——既保证日志记录到 panic 前的状态,又确保它能捕获所有下游 panic。
错误写法:r.Use(AuthMiddleware(), CustomRecovery()) → 若 AuthMiddleware panic,CustomRecovery 捕获不到(它在后面)
- 推荐顺序:
r.Use(gin.Logger(), CustomRecovery(), CORS(), AuthRequired()) - 绝对不要把
CustomRecovery()放在路由组内部(如v1.Use(...)),它必须是全局的 - 如果用了
gin.Default(),先r.Use()自定义 recovery,再r.Use(gin.Recovery())就会冲突——后者会被忽略,但容易误判
容易被忽略的生产细节
panic 不等于业务错误。很多开发者把 panic 当成“快速报错”,结果线上服务频繁触发 recovery,掩盖了本该用 return + c.Error() 处理的可预期错误(比如参数校验失败、数据库查不到记录)。
-
panic应仅用于真正不可恢复的程序异常(如空指针解引用、类型断言失败),而非业务逻辑分支 - 业务错误优先走
c.Error()+ 中间件检查c.Errors,这样既能记录又能区分错误类型 - 自定义 recovery 中的
errMsg字段别硬编码,建议从配置或环境变量读取,便于不同环境差异化控制(如开发环境返回简略提示,生产环境只返回通用文案)











