应弃用 gin.Default(),改用 gin.New() 手动注册中间件,将自定义 panic 捕获中间件置于首位,使用 c.AbortWithStatusJSON() 统一响应,区分业务 error 与 panic,避免中间件内未 recover 导致崩溃。

默认的 gin.Default() 会注册 Recovery() 中间件,但它只打印 panic 日志、返回空白 500 响应,不走你写的错误处理逻辑,也不返回结构化 JSON —— 想靠它实现友好错误提示,基本没用。
别用 gin.Default(),改用 gin.New() + 手动注册中间件
因为 gin.Default() 内置的 Recovery() 是“静默吞 panic”型,它在你自定义中间件之前就执行并中止流程,导致你写的 defer+recover 根本捕不到。必须从零构建引擎,自己控制中间件顺序:
-
gin.New()创建空引擎,不带任何默认中间件 - 第一个
r.Use()必须是你自己的 panic 捕获中间件(确保它在最前) - 后续再加
gin.Logger()、鉴权、业务中间件等 - 绝对不要在链里同时放
gin.Recovery()和你自己的 recovery —— 会冲突或漏捕
自定义 recovery 中间件必须调用 c.AbortWithStatusJSON()
只写 c.JSON(500, ...) + c.Abort() 是错的:前者可能已触发 header 写入,后者无法阻止后续中间件继续执行,容易造成 “multiple response.WriteHeader” panic 或响应体拼接混乱。正确做法是原子操作:
- 用
c.AbortWithStatusJSON(500, yourStruct)一步完成状态码、body、中断流程 - panic 后不能再调用
c.Next(),recover 块里必须立即响应并 return - 生产环境禁止透出
debug.Stack()给前端,但日志里必须保留(用于排查) - error 类型判断只是防崩,实际建议统一用自定义 error 类型(如
*app.PanicError),避免 switch 分支膨胀
中间件内部的 panic 也要单独 recover
Gin 的 Recovery() 只捕获 handler 执行时的 panic,对中间件内部(比如鉴权中间件里解析 token 失败 panic)无效 —— 因为中间件也是独立函数调用,且可能被包裹在 goroutine 中(虽然 Gin 默认非并发执行中间件,但第三方库或手动启 goroutine 时风险真实存在):
- 每个可能 panic 的中间件(尤其是涉及 JSON 解析、DB 查询、第三方调用的)都应自带 defer+recover
- 中间件内 recover 后必须调
c.Abort(),否则流程还会往下走,可能重复写响应 - 注意:中间件里的
c.Abort()只中断后续中间件,当前函数仍会继续执行,所以 recover 后要 return,避免后续代码误执行 - 别在中间件 recover 里再 panic,否则会二次崩溃
业务 error 和 panic 要分两层处理
Gin 不自动把 return err 转成 HTTP 响应,panic 和 error 是两类问题,不能混为一谈:
- panic 是程序异常中断,靠 recovery 中间件兜底
- 业务 error(如参数校验失败、DB not found)应显式调
c.Error(err),再return,由后续 ErrorHandler 中间件读取c.Errors统一响应 - ErrorHandler 必须放在 recovery 之后、业务中间件之前,才能读到所有
c.Error()注入的错误 - HTTP 状态码(如 400/401/500)和业务码(如 1001/2001)要分离,前端靠业务码做 UI 分支,网关靠 HTTP 码做重试/熔断
真正难的不是写 recovery,而是让 panic 不发生 —— 比如空指针、未判空的 map 访问、未关闭的 io.Reader。recover 是最后一道闸,不是安全带。上线前务必关掉 log.Fatal() 类强制退出调用,它们会让服务直接消失,比 panic 还致命。


















