Gin 的 recovery 中间件不能捕获所有 panic,因为它仅作用于 HTTP handler 所在的主 goroutine,而子 goroutine、定时任务或第三方库启动的协程中发生的 panic 无法被其捕获,必须手动在每个 goroutine 入口用 defer recover() 处理。

为什么 Gin 的 recovery 中间件不能捕获所有 panic?
因为 Go 的 panic 只能在当前 goroutine 内被 recover 捕获,而 Gin 默认的 recovery 中间件只作用于 HTTP handler 所在的主 goroutine。一旦 panic 发生在子 goroutine(比如 go func() { ... }() 里)、定时任务、或第三方库启动的协程中,recovery 就完全失效,进程直接崩溃。
常见错误现象:panic: runtime error: invalid memory address... 直接打印到终端并退出,日志里没走任何自定义错误处理逻辑。
- 使用场景:后台异步任务、WebSocket 连接管理、数据库连接池回调、长轮询 handler 启动的 goroutine
- 根本原因:Gin 的
recovery是基于中间件链的,它不监听全局 goroutine 崩溃 - 不能靠“加更多中间件”解决——中间件对非 handler goroutine 无效
如何安全地捕获子 goroutine 中的 panic?
必须手动在每个可能 panic 的 goroutine 入口包一层 defer recover(),没有捷径。Go 不提供类似 Node.js 的 uncaughtException 全局钩子。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 永远不要裸写
go doSomething(),改用封装好的启动函数,例如:func Go(f func()) { go func() { defer func() { if r := recover(); r != nil { log.Printf("goroutine panic: %v", r) // 这里可上报 Sentry、发告警、写入错误指标 } }() f() }() } - 注意:
recover()必须紧跟在defer后面,且只能在 defer 函数内调用 - 避免在 defer 中做耗时操作(如 HTTP 请求),否则会阻塞 goroutine 退出
- 如果用
errgroup.Group,需在每个Go子任务里单独加 recover
Gin recovery 中间件怎么定制才真正有用?
默认的 gin.Recovery() 只打印堆栈到标准输出,不记录结构化日志、不触发告警、不返回友好响应,线上基本等于摆设。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 替换为自定义 recovery,例如:
func CustomRecovery() gin.HandlerFunc { return func(c *gin.Context) { defer func() { if err := recover(); err != nil { // 记录结构化日志(含 traceID、path、method) log.Error("http panic", "path", c.Request.URL.Path, "err", err) // 上报监控(如 Prometheus counter) httpPanicCounter.Inc() // 返回统一错误响应 c.AbortWithStatusJSON(500, gin.H{"error": "internal server error"}) } }() c.Next() } } - 确保它在
router.Use()中处于最外层中间件(即最早注册),否则中间件链中更早 panic 的环节会绕过它 - 不要在 recovery 里尝试“恢复业务状态”——panic 已破坏运行时一致性,唯一安全动作是记录 + 响应 + 终止该请求
为什么 http.Server 的 ErrorHandler 不管用?
Go 标准库 http.Server 并没有 ErrorHandler 字段。很多人误以为设置 server.ErrorLog 或包装 Handler 就能捕获 panic,其实不能。
真实情况:
-
server.ErrorLog只记录底层 TCP 错误、TLS 握手失败等,不涉及 handler panic -
http.TimeoutHandler或http.StripPrefix等 wrapper 也不拦截 panic,它们只是转发请求 - 唯一可控入口仍是 Gin 的中间件链(handler goroutine)和你显式启动的 goroutine(子协程)
- 若用
net/http原生服务,也得自己 wrap handler + defer recover,和 Gin 逻辑一致
复杂点在于:同一个 panic 可能出现在多个层级——HTTP handler、handler 内部启动的 goroutine、中间件自身、甚至 Gin 的日志写入器。每个地方都得独立防御,没法一劳永逸。










