gin.recovery()对goroutine panic无效,因其仅作用于http主goroutine,子goroutine panic独立发生且无法跨协程捕获;必须在每个子goroutine内手动添加defer+recover,并禁止操作请求上下文对象。

gin.Recovery() 为什么对 goroutine panic 完全无效
因为 gin.Recovery() 只作用于 HTTP 请求主 goroutine,它注册的 defer 闭包根本不在你 go func() {}() 启动的子 goroutine 里。子 goroutine panic 后直接崩溃、打印堆栈、退出,中间件完全看不见——这不是 bug,是 Go 并发模型的必然结果。
- 常见错误:在 handler 里开 goroutine 做异步日志、消息推送、缓存更新,然后在里面调用
c.JSON()或解包空指针,panic 立刻逃逸出 gin 控制范围 - 更隐蔽的问题:子 goroutine 中调用第三方库(如
json.Unmarshal())失败触发 panic,你以为有全局 recovery 就安全了,其实没有 - 注意:
recover()只能在当前 goroutine 的 defer 中生效,跨 goroutine 调用recover()永远返回nil
如何在 goroutine 内部安全 recover
必须在每个可能 panic 的子 goroutine 内部手动加 defer + recover,且不能碰任何属于原请求上下文的对象(比如 c、w、r)。
- 绝对禁止在子 goroutine 里调用
c.JSON()、c.String()、c.Abort()等——这些操作会引发 panic 且无法被拦截 - 正确写法是独立封装一个带 recover 的执行函数,例如:
go func() { defer func() { if r := recover(); r != nil { log.Printf("async panic: %v", r) // 这里可上报监控、发告警,但绝不操作响应 } }() // 只做纯业务逻辑:DB 查询、HTTP 调用、计算等 doAsyncWork() }() - 如果需要向主流程反馈错误,用带缓冲的
chan error或context.WithCancel配合sync.WaitGroup,而不是靠 panic 传递
goroutine panic 后要不要继续服务
recover 到 panic 只是防止进程退出,不代表服务状态正常。高频 panic 往往意味着资源泄漏、连接池耗尽或数据竞争,硬扛可能让问题雪球越滚越大。
- 简单场景(如单次异步通知):recover 后记录日志即可,不影响主请求
- 关键路径(如支付回调后发 MQ):建议结合熔断器(如
gobreaker),连续失败几次就暂时拒绝新任务 - 致命 panic(如
runtime.StackOverflow、runtime.ErrWriteAfterFlush)应直接记录并退出该 goroutine,不尝试恢复 - 注意:recover 后原 goroutine 已终止,不会“继续执行”,别指望它能 cleanup 临时文件或关闭未完成的 HTTP client
为什么 defer 放错位置会导致 recover 失效
很多人把 defer 写在 c.Next() 之后,或者漏写 c.Next(),导致 panic 发生时根本没进入 defer 作用域。
- 正确结构只有一种:
defer func(){...}()必须在c.Next()之前定义,且c.Next()必须被包裹在 defer 函数的作用范围内 - 错误示例:
func BadRecovery() gin.HandlerFunc { return func(c *gin.Context) { c.Next() // panic 发生在这里 → 已经跳出函数,defer 来不及执行 defer func() { if r := recover(); r != nil { /* never called */ } }() } } - 更隐蔽的坑:在
c.Next()前做了可能 panic 的操作(如user := c.MustGet("user").(*User)),这时 panic 发生在c.Next()外,同样逃逸
真正难处理的不是怎么写 recover,而是判断哪些 panic 值得 recover —— 向已关闭 channel 发数据、切片越界、空指针解引用可以拦;而栈溢出、内存耗尽、os.Exit() 触发的终止,拦了也没意义,还可能掩盖更严重的问题。











