Gin 的 Recovery() 中间件无法捕获 Goroutine 内的 panic,因其仅作用于请求 Goroutine;每个 go 启动的协程必须自行 defer/recover,并结合 context 防止泄漏和挂起。

直接说结论:Gin 默认的 Recovery() 中间件 **捕获不到 Goroutine 里发生的 panic**,这是服务因异步逻辑崩溃的根源。必须在每个启动 Goroutine 的地方手动加 defer/recover,不能只靠全局中间件。
为什么 Recovery 中间件对 Goroutine 无效?
Gin 的 Recovery() 是绑定在 HTTP 请求生命周期里的——它只在当前 Goroutine(即处理请求的那个)中 defer 执行。而你用 go func() { ... }() 启的 Goroutine 是全新调度单元,和请求 Goroutine 没有调用栈继承关系,panic 发生时根本不会触发任何上层 defer。
常见错误写法:
func riskyHandler(c *gin.Context) {
go func() {
// 这里 panic → 整个进程崩溃,Recovery 完全没机会介入
panic("database timeout")
}()
c.JSON(200, gin.H{"ok": true})
}这种写法上线等于裸奔。
每个 Goroutine 都要自己 recover
不是“加一次中间件就完事”,而是谁启 Goroutine,谁负责兜底。关键点:
- 所有
go func() { ... }()内部第一行必须是defer func() { if r := recover(); r != nil { /* 记录日志 */ } }() - 不要只打印日志,至少调用
log.Printf("[goroutine] panic: %v", r),否则问题无迹可寻 - 避免在 recover 里再 panic 或调用未初始化变量,否则二次崩溃
- 如果 Goroutine 依赖 context,recover 后应检查
ctx.Err() != nil,防止重复执行已取消任务
用 context.WithCancel + recover 组合防泄漏+防崩溃
单纯 recover 只能止血,但 Goroutine 可能卡死在 channel receive 或 http.Do 上。必须配合 context 控制生命周期:
func safeAsyncTask(ctx context.Context, userID int) {
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
<pre class='brush:php;toolbar:false;'>go func() {
defer func() {
if r := recover(); r != nil {
log.Printf("async task panic: %v", r)
}
}()
select {
case <-ctx.Done():
return // 超时或取消,干净退出
default:
// 实际业务,比如调第三方 API
resp, _ := http.DefaultClient.Do(req.WithContext(ctx))
// ...
}
}()}
这样既防 panic 扩散,又防 Goroutine 永久挂起。
别忽略中间件里的 goroutine
很多开发者记得 handler 里加 recover,却忘了中间件也可能启 goroutine。比如鉴权中间件里异步查 Redis、日志中间件里异步上报指标——这些地方同样要加:
- 检查所有
go关键字出现的位置,包括middleware/目录下的文件 - 尤其注意
c.Copy()后启的 goroutine,它复制了请求上下文,但不自动继承 recovery - 第三方中间件(如 JWT 验证、限流器)若支持异步模式,务必查阅其文档是否内置 recover,否则需自行包装
最易被忽略的是:你以为只是“记录日志”,结果日志库内部用了 goroutine 异步刷盘,而它没做 recover —— 你的服务就可能因日志模块崩溃。


















