必须将自定义 recover 中间件置于 r.Use() 最外层,用 gin.RecoveryWithWriter() 替代裸 defer+recover,确保 panic 发生在 c.Next() 期间且仅捕获当前 goroutine 异常。

直接用 gin.Recovery() 捕不到你想要的 panic 响应体,它只打日志、返回空 {"error": "Internal Server Error"} —— 你得自己接管恢复逻辑,且必须确保它在中间件链最外层。
为什么自定义 recover 中间件总不生效
常见现象是:写了 defer func() { recover() }(),但 panic 还是导致服务崩溃或返回空白 500;或者日志里看到 panic,但前端收不到 JSON 错误响应。
-
recover()只能捕获当前 goroutine 的 panic,且仅在c.Next()执行期间发生的 panic 才有效 —— 如果 panic 出现在c.Next()之前(比如中间件开头解包空指针),根本不会被拦截 - 中间件注册顺序错了:
gin.Recovery()或你的自定义 recovery 必须是r.Use()的第一个参数,否则 panic 发生在它之前就逃逸了 - 自己写的
defer+recover和 Gin 内置的gin.Recovery()冲突:两者都注册时,Gin 默认 recovery 可能先执行并吞掉 panic,你的逻辑压根没机会运行
正确写法:用 gin.RecoveryWithWriter() 替代默认 recovery
这是 Gin 提供的官方扩展点,允许你完全控制 panic 恢复后的响应内容和日志行为,避免手动管理 http.ResponseWriter 状态导致的 http: multiple response.WriteHeader calls 错误。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 不要自己写裸
defer+recover包裹c.Next(),容易忽略 header 写入时机、body 已关闭等边界问题 - 传入自定义
io.Writer(比如os.Stderr)用于日志,再配合c.AbortWithStatusJSON()写响应 - 必须在
c.Next()后立即检查recover()结果,不能延迟到后续中间件中处理
func CustomRecovery() gin.HandlerFunc {
return func(c *gin.Context) {
defer func() {
if err := recover(); err != nil {
// 记录带堆栈的日志
log.Printf("panic recovered: %v\n%s", err, debug.Stack())
// 主动终止流程并写响应
c.AbortWithStatusJSON(500, gin.H{
"code": 500,
"msg": "服务内部错误,请稍后重试",
"data": nil,
})
}
}()
c.Next()
}
}
业务 error 和 panic 必须分开处理
recover() 只管 panic,不管业务错误;而 c.Error() 上报的 error 不会触发 recover(),也不会自动中断流程 —— 它们走的是两条完全不同的通道。
立即学习“go语言免费学习笔记(深入)”;
- 数据库查询失败、参数校验不通过这类情况,应该用
c.Error(err)+c.Abort(),然后由下游统一错误中间件读取c.Errors处理 - panic 是程序级异常(如空指针、类型断言失败、切片越界),必须靠
recover()拦截,不能靠c.Error() - 别在 handler 里
panic("xxx")模拟业务错误 —— 这会让错误分类混乱,监控难区分是 bug 还是预期异常
最容易被忽略的细节
恢复 panic 后,c.Request.Body 很可能已关闭或读空,不能再调用 c.ShouldBindJSON();同时,c.Writer 的状态可能已部分写出,直接调用 c.JSON() 会 panic。这些不是理论风险,是上线后真实踩过的坑。
- 所有对
c.Request和c.Writer的操作,必须在recover()前完成,或确保它们仍处于可用状态 - 如果你用了
gin.BasicAuth、JWT 验证等前置中间件,确保CustomRecovery在它们之前注册,否则 auth 过程中的 panic 会被漏掉 - goroutine 内部的 panic(比如异步写日志、发 MQ)无法被 HTTP 中间件捕获,需要单独加
defer recover()并上报监控

















