gin默认recovery()中间件无法捕获子协程panic,因其仅作用于http请求主goroutine;所有显式go启动的协程必须手动添加defer/recover并记录日志,否则panic将导致服务崩溃。

gin 默认的 Recovery() 中间件只捕获主协程 panic,不处理子协程 panic —— 这是线上服务突然崩溃最常见的原因。
为什么 Recovery() 中间件有时“失效”
当你用 gin.Default() 启动服务时,Recovery() 确实已自动注册,但它只包裹路由处理函数的**主执行流**。一旦你在 handler 里启了 goroutine(比如异步发消息、调第三方接口、延迟清理),里面的 panic 就完全逃逸出 gin 的恢复范围。
常见错误现象:
- 接口返回 200,但后台日志没打印,请求却“消失”了
- 服务运行几分钟后突然退出,
fatal error: all goroutines are asleep - deadlock!或直接无日志终止 -
go func() { panic("db timeout") }()导致整个进程挂掉
根本原因:goroutine 中的 panic 不会传播到启动它的那个 goroutine,而 Recovery() 只在当前 HTTP 请求 goroutine 中 defer/recover。
手动在 goroutine 内部加 defer/recover
这是最直接、最可控的做法。不能依赖框架,必须自己兜底。
实操建议:
- 所有显式
go启动的匿名函数,开头就写defer func() { if r := recover(); r != nil { log.Printf("goroutine panic: %v", r) } }() - 不要只
recover()而不记录 —— 生产环境必须留痕,否则等于没做 - 避免在 recover 块里调用可能再 panic 的逻辑(比如未判空的
c.JSON()),因为此时*gin.Context已不可用 - 如果需要上报(如 Sentry),用独立 channel 或异步 logger,别阻塞当前 goroutine
示例:
r.GET("/notify", func(c *gin.Context) {
go func() {
defer func() {
if r := recover(); r != nil {
log.Printf("[notify goroutine] panic recovered: %v", r)
// 可选:上报监控
reportPanic(r)
}
}()
// 模拟可能 panic 的操作
riskyOperation()
}()
c.Status(http.StatusOK)
})
自定义全局 Recovery 中间件替代默认行为
如果你需要统一控制 panic 日志格式、状态码、响应体或集成告警,可以替换默认 Recovery()。
关键点:
- 用
gin.New()替代gin.Default(),避免自带Recovery() - 自己实现中间件,传入自定义
recoveryFunc,比如:
func CustomRecovery() gin.HandlerFunc {
return func(c *gin.Context) {
defer func() {
if err := recover(); err != nil {
log.Printf("HTTP panic at %s %s: %v", c.Request.Method, c.Request.URL.Path, err)
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{
"code": 500,
"msg": "Internal error",
})
}
}()
c.Next()
}
}
// 注册:r.Use(CustomRecovery(), gin.Logger())
注意:CustomRecovery 依然只管主 goroutine,它不是对子协程的解决方案,而是对主流程 panic 的增强控制。
哪些地方最容易漏掉 goroutine panic 防护
这些位置常被忽略,但恰恰是 panic 高发区:
-
time.AfterFunc()或time.Tick()回调里直接写业务逻辑 - 使用
sync.PoolGet/put 时,对象 Reset 方法里有未校验的指针解引用 - 数据库连接池回调(如
sql.Register的 driver hook) - WebSocket 连接中起的读/写 goroutine(
conn.ReadMessage循环) - 任何带
select+default的非阻塞 goroutine,里面调用了可能 panic 的函数
真正难的不是写 recover,而是意识到“这里起了 goroutine,它得自己负责自己的 panic”。这个意识比代码更重要。











