panic不能跨goroutine传播,因goroutine相互隔离且无隐式错误冒泡机制;需通过带缓冲的chan error等显式通信方式传递错误,并确保发送与接收配对。

为什么 panic 不能跨 goroutine 传播
Go 的 panic 只在当前 goroutine 内生效,一旦在子 goroutine 中触发,不会自动中断父 goroutine 或其他协程。这是设计使然——goroutine 是隔离的执行单元,没有隐式错误冒泡机制。常见现象是:子 goroutine panic 后程序没崩溃,但错误被静默吞掉,主流程继续运行,结果数据异常或逻辑错乱。
真正要传递错误,得靠显式通信。核心思路只有两个:channel 或 error 返回值 + 同步等待(如 sync.WaitGroup 或 context)。别指望语言替你做跨协程错误转发。
用 chan error 接收单个 goroutine 的错误
最轻量、最可控的方式:为每个启动的 goroutine 配一个 chan error,并在其结束时发送错误(或 nil)。主 goroutine 用 select 或直接接收来判断是否出错。
- 必须用带缓冲的 channel(如
make(chan error, 1)),否则 goroutine 可能因发送阻塞而卡死 - 务必确保 goroutine 结束前一定发一次 error(哪怕为
nil),否则主 goroutine 接收会永久阻塞 - 不要在 goroutine 内部 recover 后忽略 error——recover 只是拦截 panic,不等于错误已处理
errCh := make(chan error, 1)
go func() {
defer close(errCh) // 确保 channel 关闭,避免主 goroutine 永久等待
if err := doSomething(); err != nil {
errCh <- err
return
}
errCh <- nil
}()
if err := <-errCh; err != nil {
log.Fatal(err)
}多个 goroutine 并发时用 errgroup.Group 统一收集
标准库 golang.org/x/sync/errgroup 是专为此场景设计的:它封装了 sync.WaitGroup 和 context,支持“任一错误即终止”或“等全部完成再汇总”。比手写 channel 更健壮,尤其适合 HTTP handler、批量任务等场景。
- 默认行为是“第一个非 nil error 返回”,后续 goroutine 会被取消(需配合
ctx使用) - 若想等全部完成再看错误,调用
eg.Wait()后检查eg.Err();但注意这不返回所有错误,只返回第一个 - 不要混用多个
errgroup.Group实例去管理同一组 goroutine,会导致 wait 逻辑错乱
g, ctx := errgroup.WithContext(context.Background())
for i := 0; i < 3; i++ {
i := i // 避免闭包变量复用
g.Go(func() error {
select {
case <-time.After(time.Second):
return fmt.Errorf("task %d failed", i)
case <-ctx.Done():
return ctx.Err()
}
})
}
if err := g.Wait(); err != nil {
log.Printf("At least one task failed: %v", err)
}用 context.Context 主动取消正在运行的 goroutine
错误传递不只是“通知发生了什么”,更是“让无关操作及时停手”。比如一个上传任务中某环节出错,后续压缩、校验、通知等 goroutine 应立刻退出,而不是等超时或做完无用功。
关键点在于:所有子 goroutine 必须监听 ctx.Done(),并在收到信号后清理资源、返回 ctx.Err()。仅靠 channel 发送 error 不足以实现主动中断。
- 永远用
ctx.WithCancel/ctx.WithTimeout创建子 context,不要传入context.Background()后再 cancel - 在 goroutine 内部,
select中必须包含case 分支 - 不要在 goroutine 里调用
cancel()——那是发起方的责任;子 goroutine 只负责响应
跨协程错误传递的本质,是把“错误发生”这个事件,转化为可监听、可响应、可组合的控制流信号。越早把 error 和 context、channel 绑定在一起设计,后期越不容易掉进静默失败的坑。


















