errgroup核心作用是统一错误传播与协同取消,必须用errgroup.WithContext()初始化以支持超时和取消,Go()适用于纯CPU计算,GoContext()专用于可取消IO操作。

errgroup 不是用来“管理并发数量”的,而是用来“统一错误传播 + 协同取消”的。如果你只想要并发跑 10 个请求、不关心谁先失败、也不需要超时中断,那直接用 sync.WaitGroup 更轻量;但只要涉及「任一失败就停掉其余」「必须响应超时」「避免 goroutine 泄漏」,errgroup 就是唯一靠谱选择。
为什么不用 &errgroup.Group{} 而必须用 errgroup.WithContext()
手动 new 一个空 errgroup.Group 看似能跑通,但它完全不感知上下文:哪怕你传了超时 context 给子任务,g.Wait() 返回错误后,其他 goroutine 还在继续执行,连接没关、文件没 close、HTTP 请求卡在 read 上 —— 这就是典型的 goroutine 泄漏。只有 errgroup.WithContext(ctx) 创建的 group,才会在首个错误或 context 取消时,自动触发所有子任务共享的 ctx.Done() 信号。
- 错误写法:
g := &errgroup.Group{}→ 后续所有g.Go()都无法被取消 - 正确写法:
g, ctx := errgroup.WithContext(context.WithTimeout(context.Background(), 5*time.Second)) - 关键点:
ctx必须传给每个 IO 操作,比如http.NewRequestWithContext(ctx, ...),不能只靠errgroup自己“生效”
Go() 和 GoContext() 到底该用哪个
别凭感觉选。二者语义完全不同:Go() 接收 func() error,它不碰 context,适合纯 CPU 计算(如 JSON 解析、base64 编码);GoContext() 接收 func(context.Context) error,它内部会监听 ctx.Done() 并提前退出,专为 HTTP/DB/File 等可取消 IO 设计。
- HTTP 请求必须用
GoContext():g.GoContext(func(ctx context.Context) error { return http.GetWithContext(ctx, url) }) - 如果混用:一个
Go()里调time.Sleep(10 * time.Second),而 context 已超时,它会睡满 10 秒,Wait()一直卡住 -
GoContext()不是“自动加 timeout”,它只是给你一个可检查的ctx;你仍需在函数内显式select { case 或传给支持 context 的库
Wait() 返回 nil 就代表全成功?别信
g.Wait() 返回 nil,只说明「所有已启动的 goroutine 都结束了,且没报告非 nil 错误」。但它完全不管 panic、不验证逻辑是否真执行完、也不保证错误类型是你预期的。
- 如果某个
Go()闭包里panic("db connection lost")且没recover,程序直接崩溃,根本走不到Wait() - 5 个任务都失败,
Wait()只返回第一个出错的 error(比如context.DeadlineExceeded),后面 4 个sql.ErrNoRows全丢掉 —— 日志里只看到一个超时,实际是全挂了 - 想收全错误?得自己用
sync.Mutex+[]error收集,或者每个GoContext()里return multierr.Append(err, yourErr) - 验证是否真跑完:在每个任务开头加
log.Printf("start: %s", url),确认每条日志都打出,且无遗漏
常见报错 panic: context canceled 出现在 g.Run() 是怎么回事
errgroup.Run() 是个“硬性守门员”:只要传入的 context 已取消(比如 context.WithTimeout 到期),它立刻 panic,拒绝启动任何新任务。这不是 bug,是设计使然。
- 错误现象:
panic: context canceled报在g.Run()调用行,但你没手动cancel()→ 实际是上游 context 已过期 - 解决方案:调
g.Run()前必须检查if ctx.Err() != nil { return ctx.Err() } - 更稳妥做法:别用
Run(),改用GoContext()+ 手动判断ctx.Err(),实现“尽力执行” - 绝对不要复用已取消的 context 创建新
errgroup;每次并发批次都要配独立 context
真正容易被忽略的点是:errgroup 本身不提供并发控制,也不做错误聚合。它只做两件事——把第一个错误透出来,把取消信号广播出去。剩下的,比如限流要自己加 semaphore,全量错误要自己收集,panic 要自己 recover,IO 调用要自己传 context —— 它不替你做,但帮你把这几件事串成一条可靠链路。

















