默认 recovery 中间件仅打印日志并返回空白500响应,不返回结构化json、暴露敏感信息、无法联动熔断或结构化日志;应手写customrecovery中间件,用defer+recover捕获panic后调用c.abortwithstatusjson统一响应,并配合监控告警与根因修复。

默认 recovery 中间件只打日志、不返回结构化错误,生产环境必须替换它。
为什么 gin.Default() 的 panic 处理不满足生产需求
Gin 内置的 recovery.DefaultRecovery 在 panic 发生后仅向 stderr 打印堆栈,然后调用 c.Abort() 并返回空白 500 响应——它不走你写的中间件链,也不调用 c.JSON() 或 c.AbortWithStatusJSON()。这意味着:
- 前端收不到统一格式的
{"code":500,"msg":"..."},无法做一致错误提示 - 敏感 panic 信息(如路径、变量名)可能直接暴露在响应体中(若未禁用调试模式)
- 无法联动熔断器(如
breaker.MarkFailed())或记录结构化错误日志
如何手写一个能返回 JSON 的 recovery 中间件
核心是用 defer + recover() 捕获 panic,再统一构造响应。注意必须调用 c.AbortWithStatusJSON(),否则后续中间件仍可能执行:
func CustomRecovery() gin.HandlerFunc {
return func(c *gin.Context) {
defer func() {
if err := recover(); err != nil {
// 生产环境务必脱敏:禁止直接 fmt.Sprintf("%v", err)
errMsg := "internal server error"
c.AbortWithStatusJSON(500, gin.H{
"code": 500,
"msg": errMsg,
"data": nil,
})
}
}()
c.Next()
}
}
- 注册时禁用默认 recovery:
r := gin.New(),而非gin.Default() - 中间件顺序很重要:必须在业务路由前注册,且不能放在
c.Next()后面 - panic 类型可能是
string、error或其他,建议统一转为预设字符串,避免类型断言失败
recover 后不能继续执行业务逻辑
一旦 recover() 触发,说明当前 handler 已经处于不可恢复的异常状态。此时:
- 绝不能调用
c.Next()或尝试继续处理请求 - 不能依赖
c.JSON(500, ...)+c.Abort()组合:因为c.Abort()只是标记中断,若后续中间件未检查c.IsAborted(),仍可能误处理 - 必须用
c.AbortWithStatusJSON()—— 它内部已包含c.Abort()且立即写响应头和 body - 注意:
recover()只捕获当前 goroutine 的 panic,Gin 的 handler 是单 goroutine,所以安全
与优雅退出(Graceful Shutdown)协同的关键点
panic 频发时,服务大概率已不稳定。光靠 recovery 拦截无法解决根本问题:
- HTTP 连接可能已半断开,
c.AbortWithStatusJSON()的响应未必能送达客户端 - 资源泄漏(如数据库连接未释放、文件句柄未关闭)不会因 recover 自动修复
- 真正该做的是:recover 后记录关键指标(如 panic 次数/分钟),触发告警,并配合
http.Server.Shutdown()快速下线实例 - 不要在 recovery 中尝试“重试”或“降级逻辑”——这会让错误边界模糊,增加排查难度
最易被忽略的是:recover 只是兜底,不是容错。高频 panic 往往暴露了未校验的输入、空指针访问或并发竞争,得回代码里修根因。











