goroutine 中 panic 不会自动传播到主线程,仅终止当前 goroutine;必须用 defer+recover 主动捕获,推荐封装 goSafe 函数、结合 error channel 和 context 控制错误响应。

goroutine 中 panic 不会自动传播到主线程
Go 的 goroutine 是独立的执行单元,内部发生 panic 默认只会终止当前 goroutine,不会影响其他 goroutine,更不会让主程序崩溃——这看似安全,实则危险:错误被静默吞掉,日志没打,监控收不到,问题难以定位。
常见现象是:程序跑着跑着少了几路数据处理,但 main 还在运行,ps 看进程活着,却查不到任何报错。
- 必须主动捕获每个 goroutine 的
panic,否则等于放弃错误可见性 -
recover()只在defer中有效,且仅对当前 goroutine 生效 - 不能依赖
log.Fatal或os.Exit来“终结全局”,那会直接杀掉整个进程,不是容错而是自毁
用 defer + recover 统一封装 goroutine 启动逻辑
最稳妥的做法是把所有 go func() { ... }() 替换为一个带错误兜底的启动函数,比如 goSafe:
func goSafe(f func()) {
go func() {
defer func() {
if r := recover(); r != nil {
log.Printf("goroutine panic: %v", r)
// 这里可上报 Sentry、推送到 error channel、触发告警等
}
}()
f()
}()
}使用时直接替换原始写法:
立即学习“go语言免费学习笔记(深入)”;
- 原来:
go handleMsg(msg) - 现在:
goSafe(func() { handleMsg(msg) })
注意:不要在 goSafe 内部传入带参数的闭包并捕获外部变量引用错误(比如循环中 for _, v := range list { goSafe(func(){ use(v) }) }),要显式传值或复制变量,否则可能读到错误的 v 值。
通过 channel 集中收集错误并做统一决策
单靠打印日志不够,尤其当并发量大、错误高频出现时,需要聚合、限流、分级响应。推荐定义一个全局错误通道:
var ErrChan = make(chan error, 100) // 缓冲避免阻塞 goroutine
在 goSafe 的 recover 分支里,把错误转成 error 类型后发往该 channel:
ErrChan- 主 goroutine 单独起一个监听协程消费它:
go func() { for err := range ErrChan { /* 上报 / 降级 / 计数 */ } }() - 可配合
sync/atomic统计错误频次,超阈值时触发服务降级(如关闭非核心 goroutine)
别把 ErrChan 设成无缓冲 channel,否则一旦消费者卡住或没启,所有出错的 goroutine 都会在 chan 处永久阻塞。
context.WithCancel 配合 recover 实现“出错即停”控制流
有些场景要求:只要任意一个关键 goroutine panic,就立即停止其余工作(比如批量导入任务)。这时不能只靠 error channel,得联动取消信号:
- 启动前创建
ctx, cancel := context.WithCancel(context.Background()) - 每个 goroutine 接收该
ctx,并在循环或阻塞调用中检查ctx.Err() - 在
recover后立刻调用cancel(),通知其他 goroutine 退出
注意:cancel() 是幂等的,多次调用没问题;但要确保所有 goroutine 都尊重 ctx,否则取消信号形同虚设。尤其是用了第三方库的阻塞 I/O(如某些数据库驱动未适配 context),仍可能继续跑下去。
真正难的不是捕获 panic,而是判断哪些错误该恢复、哪些该透出、哪些要触发熔断——这取决于业务语义,没有银弹。










