goroutine panic 不会传播到主 goroutine,仅终止自身并打印 stack trace 到 stderr;必须在每个 goroutine 内部用 defer+recover 捕获,且 recover 仅对同 goroutine 有效。

Go 中 goroutine panic 后会自动终止,但不会传播到主 goroutine
这是最常被误解的一点:启动的 goroutine 里发生 panic,主程序照常运行,甚至可能悄无声息地“漏掉”错误。你看到程序没报错、也没按预期结束,大概率就是某个子 goroutine panic 了但没人 recover。
根本原因在于 Go 的 panic 是 goroutine 局部的 —— 它不会跨 goroutine 传播,也不会触发 os.Exit 或中断其他 goroutine。
- 常见错误现象:
panic: runtime error: invalid memory address出现在日志里,但主流程仍在跑,HTTP 服务没挂,定时任务还在执行 - 典型场景:HTTP handler 里启了个匿名 goroutine 做异步写日志,里面用了未初始化的指针;或
time.AfterFunc回调里访问了已释放的 struct 字段 - 不加 recover 的 goroutine panic 只会打印 stack trace 到 stderr(如果没重定向,你可能根本看不到)
用 defer + recover 捕获 goroutine 内 panic
必须在每个可能 panic 的 goroutine 内部做 recover,不能指望外面包一层。这是唯一可靠的方式。
注意:recover 只在 defer 函数中有效,且只对同 goroutine 的 panic 生效。
立即学习“go语言免费学习笔记(深入)”;
- 正确写法是把
defer func() { if r := recover(); r != nil { /* 记录/上报 */ } }()放在 goroutine 函数体第一行 - 别写成
go func() { defer recover() }()——recover不是函数调用,单独写没意义 - 别在外部 goroutine 里对另一个 goroutine 调用
recover—— 它永远返回nil - 示例:
go func() { defer func() { if r := recover(); r != nil { log.Printf("goroutine panic: %v", r) } }() // 可能 panic 的逻辑,比如 map 并发读写、空指针解引用等 doSomethingRisky() }()
结合 context 实现带超时/取消的 goroutine 安全退出
recover 解决 panic,但解决不了“卡死”或“该停不停”。真正安全的退出,得靠 context.Context 主动控制生命周期。
panic 是异常路径,context.Cancel 是正常路径 —— 两者要分开处理,不能互相替代。
- goroutine 启动时必须接收
ctx context.Context参数,并在关键阻塞点(如ch 、<code>、<code>http.Do)检查ctx.Done() - 不要用
select { case 包裹整个 goroutine —— 这样 panic 发生时还是逃不出去 - 推荐结构:外层 defer recover,内层 select 处理 ctx 和业务 channel,panic 和 cancel 都导向 cleanup
- 性能影响:
ctx.Done()是无锁 channel 操作,开销极小;但频繁 select 多个 channel 会略微增加调度负担,合理合并条件即可
log.Fatal / os.Exit 在 goroutine 里会杀掉整个进程
这是个隐蔽但致命的坑:有人在 goroutine 里写 log.Fatal("xxx"),以为只是“记录并退出当前 goroutine”,结果整个程序退出了。
因为 log.Fatal 底层调的是 os.Exit(1),它不区分 goroutine,直接终止进程。
- 错误写法:
go func() { if err != nil { log.Fatal(err) } }()—— 一个 goroutine 错误,全服务崩 - 正确替代:
log.Printf("error: %v", err)+return,或封装为带 error 返回的函数,由调用方决定是否终止 - 如果你真需要“某类错误必须终止服务”,应该让错误透传回 main 函数,由 main 统一调用
os.Exit - 兼容性注意:所有标准库的
log.Fatal*、log.Panic*、fmt.Fprint*ln(os.Stderr, ...); os.Exit行为一致,别心存侥幸
真正难的不是写 recover,而是判断哪些 goroutine 值得加、哪些该用 context 控制、哪些压根不该起。很多人加了一堆 defer recover,却忘了 goroutine 本身是不是必要 —— 很多“并发”其实只是顺序逻辑套了 go 关键字而已。










