gin默认recovery中间件不返回自定义json错误,因其仅捕获panic并打印堆栈后直接返回空白500响应,既不调用c.abortwithstatusjson(),也不触发任何自定义错误处理逻辑,且执行完即终止流程,不给后续中间件留执行机会。

为什么 Gin 的默认 recovery 中间件不返回你定义的 JSON 错误
Gin 的 gin.Default() 自带 recovery.Recovery(),但它只做两件事:打印 panic 堆栈、返回空白 500 响应。它不调用 c.AbortWithStatusJSON(),也不走你写的任何错误处理逻辑——哪怕你注册了自定义中间件,在 panic 发生后也完全失效。
根本原因在于 panic 发生在 handler 执行链中,而默认 recovery 是“捕获 + 打印 + 立即结束响应”,不给后续中间件或业务代码留执行机会。
所以别试图在默认 recovery 后再加一层拦截,直接禁用它,自己写。
- 用
gin.New()替代gin.Default() - 手动注册你写的 recovery 中间件,且必须放在所有其他中间件之前(
router.Use(YourRecovery())) - 确保你的 recovery 中间件里调用的是
c.AbortWithStatusJSON(),不是c.JSON()+c.Abort(),后者可能被后续中间件覆盖
如何写一个真正能返回结构化错误的 recovery 中间件
核心是 defer + recover() + c.AbortWithStatusJSON(),但有三个关键细节不能漏:
- 必须检查
c.Writer.Written(),避免重复写响应(比如 panic 前已有中间件写了 200,这时再写 500 会 panic) - 生产环境禁止直接返回
debug.Stack(),可记录日志但响应体要脱敏;开发环境可加"stack": string(debug.Stack())字段供调试 - panic 值类型不确定(可能是
string、error或其他),别直接fmt.Sprintf("%v", err),统一转成人话提示,比如"internal server error"
示例片段:
func CustomRecovery() gin.HandlerFunc {
return func(c *gin.Context) {
defer func() {
if err := recover(); err != nil {
if c.Writer.Written() {
return
}
log.Printf("[PANIC] %v\n%s", err, debug.Stack())
c.AbortWithStatusJSON(500, gin.H{
"code": 500,
"message": "服务暂时不可用",
"request_id": c.GetString("request_id"), // 若你已注入 request_id
})
}
}()
c.Next()
}
}
c.Error() 和 c.AbortWithError() 到底该用哪个
c.Error() 只是把错误塞进 Gin 的 error slice,不中断执行;c.AbortWithError() 才真正终止 handler 链并写响应体。很多人误以为调了 c.Error() 就等于返回了,结果后面又执行 c.JSON(200, ...),导致状态码和 body 对不上。
- 参数校验失败、权限不足等需立即返回时,用
c.AbortWithError(400, err),第二个参数必须是error类型 -
c.Error()仅适合日志中间件或监控中间件收集上下文错误,比如鉴权失败时记一笔c.Error(authErr),但不阻断流程 - 别传字符串给
c.AbortWithError(),会 panic;要用errors.New("xxx")或自定义*AppError
异步 goroutine 中的 panic 为什么 recovery 捕不到
Gin 的 recovery 中间件只作用于 HTTP 请求生命周期所在的主 goroutine。你在 go func() {}() 里触发的 panic,它根本看不见——这不是 bug,是设计使然。
- 绝对不要在异步 goroutine 里直接调
c.JSON()、c.String()等响应方法,那会 panic 且无法被捕获 - 真要异步,必须自己加
defer/recover,例如:go func() { defer func() { if r := recover(); r != nil { log.Printf("async panic: %v", r) } }() // 业务逻辑 }() - 如果异步任务需要通知主流程出错,建议用 channel + context 控制,而不是靠 panic 传递
最常被忽略的一点:panic 不等于业务错误。recover 是兜底,不是错误处理主干。业务逻辑该用 return + c.AbortWithError() 的地方,别依赖 recover 来“擦屁股”。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











