应自定义 Recovery 中间件:defer 中用 debug.Stack() 记日志(含 path、method、ip、err)、检查 c.Writer.Written() 避重复写头、统一调用 c.AbortWithStatusJSON(500, response.Fail(code, msg)) 保证响应格式与业务一致。

为什么 gin.Default() 的 Recovery 中间件不能直接用
它只打印堆栈到标准输出,返回的 {"error": "Internal Server Error"} 没有业务字段,前端无法解析 code 和 msg,日志也缺少请求上下文(如 path、method、ip)。更麻烦的是,它不调用 c.AbortWithStatusJSON(),而是直接 c.Abort() + 空响应,后续中间件仍可能执行,导致重复写 header 或 panic 二次触发。
如何让 recover 中间件同时记录日志并返回结构化错误
关键不是“捕获 panic”,而是“在 panic 发生时,仍能拿到完整请求上下文并安全写出响应”。必须在 defer 里做三件事:记录带 trace 的日志、检查是否已写响应、调用 c.AbortWithStatusJSON()。
-
c.Writer.Written()必须判断——避免http: multiple response.WriteHeader calls错误 - 堆栈用
debug.Stack()获取,但生产环境别塞进响应体,只存日志 - 统一用
c.AbortWithStatusJSON(500, ...),不要c.JSON() + c.Abort(),否则c.Next()后续逻辑仍会跑 - 日志至少包含:
c.Request.URL.Path、c.ClientIP()、err类型和 message、debug.Stack()
怎么把日志和错误响应格式对齐
如果你已经定义了全局响应结构体(比如 response.Success() / response.Fail()),recover 中间件返回的错误就必须走同一套序列化逻辑,否则前端要处理两种 JSON 格式。别混用 gin.H{} 和你自己的 Response{}。
- 错误响应字段名必须和成功响应一致:比如都用
code和msg,不用message或error - HTTP 状态码和业务 code 要分离:
c.AbortWithStatusJSON(500, response.Fail(50001, "系统繁忙")) - 日志中的 error 字段建议打成 map:{"path":"/api/user", "method":"POST", "code":50001, "err":"panic: nil pointer"}
容易被忽略的 panic 来源和兼容性坑
很多 panic 不来自 handler,而是中间件本身(比如自定义 auth 中间件里 token.Claims["uid"] 没判空)、或 defer 里调用的函数(比如 close 文件句柄失败)。这些 panic 同样会被 recover 捕获,但你未必希望它们都返回 500。
- 区分 panic 类型:用
err.(type)判断是string、error还是自定义*BusinessError,后者可降级为 4xx 响应 - Gin v1.9+ 的
gin.RecoveryWithWriter()允许自定义 writer,但默认仍不支持结构化输出,不如自己重写 - 如果用了
gin.New()(没开默认中间件),记得手动r.Use(middleware.Recovery()),别漏掉 - 注意 defer 执行顺序:recover 中间件的 defer 在最外层,但若其他中间件也有 defer,panic 可能被更内层的 recover 先截走


















