recover必须在每个goroutine内部调用才有效,主goroutine的defer+recover对子goroutine panic无效;子goroutine panic后静默退出易致资源泄漏,须在每个go函数内前置defer+recover并立即return,推荐用safeGo封装兜底。

recover 必须在每个 goroutine 内部调用才有效
主 goroutine 的 defer + recover() 对子 goroutine 的 panic 完全无效。这不是 bug,是 Go 的明确设计:panic 和 recover 绑定在单个 goroutine 生命周期内。你看到服务没崩,只是因为子 goroutine 崩溃后静默退出,但资源(如未关闭的 net.Conn、*os.File、未释放的 sync.Mutex)可能已泄漏。
常见错误写法:
go func() { panic("boom") }()
defer func() { recover() }() // 这个 recover 属于主 goroutine,永远拿不到上面那个 panic
正确做法是每个可能 panic 的 goroutine 入口处自己配 defer + recover():
- 必须写在
go启动的函数内部,不能外包 - 必须在可能 panic 的代码执行前注册
defer(即defer语句要出现在panic或易 panic 调用之前) - recover 后应立即
return,避免继续使用可能已损坏的状态(比如空指针刚 panic 过,再解引用还会 crash)
用 safeGo 封装避免重复写 defer/recover
手动给每个 go 前加 defer + recover() 易漏、难维护。封装一个通用函数能强制兜底,同时保留堆栈和上下文。
立即学习“go语言免费学习笔记(深入)”;
推荐实现:
func safeGo(f func()) {
go func() {
defer func() {
if r := recover(); r != nil {
log.Printf("panic in goroutine: %v\n%s", r, debug.Stack())
}
}()
f()
}()
}
注意闭包变量捕获问题:
- 如果
f依赖外部变量(如ctx、reqID),需显式传入,例如safeGo(func() { handle(ctx, reqID) }) - 不要直接写
safeGo(handle),否则可能捕获到错误的变量快照 - 该封装不处理返回值;若需 error 传递,应改用
errgroup配合外围 recover
errgroup.Group 的 recover 行为与陷阱
errgroup.Group 内部确实有 recover,但它只记录第一个 panic,并在 g.Wait() 时重新抛出——这意味着你仍需在调用 Wait() 处加 defer + recover(),否则整个程序会崩溃。
典型翻车场景:
eg := errgroup.Group{}
eg.Go(func() error { panic("oops") })
if err := eg.Wait(); err != nil { // 这里会 panic,不是返回 error
fmt.Println(err)
}
安全用法:
- 调用
Wait()前必须加defer func() { recover() }() - 多个 goroutine 同时 panic 时,只有第一个被记录,其余静默丢失——这对可观测性是硬伤
- 建议配合结构化日志 + traceID,在每个
eg.Go内部也做一次defer+recover()打点,确保每次 panic 都有独立记录
recover 捕获值的类型断言必须谨慎
recover() 返回 interface{},实际类型取决于 panic() 传入的参数。可能是 string、*runtime.Error、自定义 struct,甚至 int。直接用 err.(error) 断言会 panic,尤其当 panic 来自 runtime(如 index out of range)时,其底层类型未导出。
稳妥做法:
- 先用
fmt.Sprintf("%v", r)获取可读字符串用于日志 - 如需结构化处理,用
switch r := r.(type)分支判断 - 不要在 recover 后继续执行业务逻辑,状态已不可信;打完日志就
return - 避免把 recover 当作常规错误处理机制——它只适合兜底,不该替代
if err != nil


















