默认 gin.recovery() 不能替代自定义 panic 捕获中间件,因为它仅 defer 捕获 panic、打印堆栈到 stderr、调用 c.abort(),不写响应体,也不调用 c.abortwithstatusjson(),导致前端收到默认 500 html 页面而非结构化 json。

为什么默认 gin.Recovery() 不能替代自定义 panic 捕获中间件
因为 gin.Recovery() 只做三件事:defer 捕获 panic、打印堆栈到 os.Stderr、调用 c.Abort()。它不写响应体,也不调用 c.AbortWithStatusJSON(),所以前端收到的是浏览器默认 500 HTML 页面,不是可解析的 JSON。这不是 bug,是 Gin 故意把“捕获”和“渲染”解耦——你得自己决定错误长什么样、要不要脱敏、是否触发告警。
如何用自定义中间件捕获 panic 并返回结构化 JSON 错误
必须用 c.AbortWithStatusJSON(),不能用 c.JSON() + c.Abort():后者可能被后续中间件继续执行(比如日志中间件还在跑),导致状态码和 body 不一致;前者原子性终止流程并写入响应。
- 统一用
error类型传参,避免panic("xxx")这种裸字符串,否则err.(type)断言会 panic - 对非
error类型(如int、string)做兜底处理,直接设为"internal server error" - 敏感信息(路径、变量名、SQL 片段)必须过滤,500 响应体里禁止出现任何服务端细节
- 示例核心逻辑:
func CustomRecovery() gin.HandlerFunc { return func(c *gin.Context) { defer func() { if err := recover(); err != nil { var errMsg string switch e := err.(type) { case string: errMsg = "internal server error" case error: errMsg = "internal server error" default: errMsg = "internal server error" } c.AbortWithStatusJSON(500, gin.H{ "code": 500, "msg": errMsg, "data": nil, }) } }() c.Next() } }
异步 goroutine 中的 panic 为什么捕获不到,怎么补救
gin.Recovery() 和所有 HTTP 中间件只作用于请求生命周期内的主 goroutine。你在 go func() {}() 里触发 panic,它完全看不见——主协程照常返回,子协程 panic 后崩溃,还可能泄露连接或资源。
- 绝对不要在 goroutine 里直接调
c.JSON()或c.String(),那会引发竞态或 panic - 真要异步,必须手动加
recover:go func() { defer func() { if r := recover(); r != nil { log.Printf("async panic: %v", r) // 可选:发告警、记录指标 } }() // 业务逻辑 }() - 如果异步任务需反馈结果,改用 channel + select 超时控制,而不是阻塞等待 goroutine
捕获 panic 后还能不能优雅退出服务
能,但必须区分场景:单次 panic 不等于服务不可用,CustomRecovery 的职责只是止损并返回错误;真正需要优雅退出(graceful shutdown)的是进程级异常,比如配置加载失败、DB 连接池初始化失败、监听端口被占用等——这些发生在 main() 函数里,不在 HTTP 请求链中,中间件根本触达不到。
- HTTP 层 panic 捕获后,服务继续运行,不影响其他请求
- 若想在 panic 频发时主动下线(比如 1 分钟内超 5 次),需额外加计数器 + 定时器,配合
http.Server.Shutdown() - 最易忽略的一点:
panic若发生在router.NoRoute()或router.NoMethod()之后注册的 handler 里,CustomRecovery也捕获不到——因为它没被中间件链包裹











