goroutine内panic无法被外层recover捕获,必须在每个goroutine内部用defer/recover处理;recover后应立即return或通过channel通知主goroutine,避免状态不一致。

goroutine里panic不会被外层recover捕获
Go的panic只在当前goroutine内传播,主goroutine调用recover对其他goroutine里的panic完全无效。这是最常踩的坑——以为在main函数里包一层defer recover()就能兜住所有并发异常。
实际必须在每个可能出错的goroutine内部单独做defer/recover,否则panic会直接终止该goroutine(不崩溃进程,但可能丢任务、漏日志)。
- 错误写法:
go func() { /* 可能panic的代码 */ }()外面包defer recover() - 正确写法:每个goroutine自己处理,例如
go func() { defer func() { if r := recover(); r != nil { log.Println("panic:", r) } }(); /* 业务逻辑 */ }() - 注意:recover只能捕获同一goroutine内、且尚未返回的panic;一旦goroutine已退出,recover无意义
recover后如何安全退出goroutine
recover只是阻止panic向上冒泡,不代表goroutine能继续执行后续逻辑。尤其涉及资源释放、状态更新时,盲目继续可能引发数据竞争或状态不一致。
典型做法是recover后立即return,或通过channel通知主goroutine该任务失败:
立即学习“go语言免费学习笔记(深入)”;
- 简单场景直接return:
defer func() { if r := recover(); r != nil { log.Printf("worker panic: %v", r); return } }() - 需要反馈结果时,用带缓冲channel避免阻塞:
done := make(chan error, 1); go func() { defer func() { if r := recover(); r != nil { done - 切勿在recover后继续使用已panic路径中可能损坏的变量(如部分初始化的struct、未关闭的file等)
使用errgroup或sync.WaitGroup时panic处理要同步
用errgroup.Group或sync.WaitGroup管理并发时,某个goroutine panic会导致等待逻辑卡死或结果丢失——因为panic的goroutine没走完,WaitGroup计数没减,或者errgroup没收到error。
- errgroup:必须确保每个子goroutine都把error发给
g.Go()包装的函数,否则panic会被吞掉;推荐用g.Go(func() error { ... })而非裸go启动 - WaitGroup:在recover后务必调用
wg.Done(),否则main goroutine永远阻塞在wg.Wait() - 示例片段:
go func() { defer wg.Done(); defer func() { if r := recover(); r != nil { mu.Lock(); err = fmt.Errorf("panic: %v", r); mu.Unlock() } }(); /* 业务 */ }()
log.Fatal和os.Exit在goroutine里要特别小心
log.Fatal和os.Exit不是panic,它们会直接终止整个进程,不受任何recover影响。如果在goroutine里误用,会导致整个服务宕机,比panic更危险。
并发场景下必须用log.Printf或自定义error channel上报,而不是log.Fatal:
- 禁止:
go func() { if err != nil { log.Fatal(err) } }() - 允许:
go func() { if err != nil { log.Printf("worker error: %v", err); return } }() - 若需全局退出(如配置加载失败),应由主goroutine统一判断,而非任一worker自行调用
os.Exit
recover本身不能跨goroutine起作用,而log.Fatal这类进程级退出在并发里又极易误用——这两点是线上Go服务因并发panic导致雪崩的最常见根源。










