ErrGroup 是 Go 标准库中用于等待一组 goroutine 完成并汇总首个非 nil 错误的同步原语,解决并发任务中快速取消与统一错误处理问题,需配合 context.Context 使用。

ErrGroup 是什么,它解决什么问题?
Go 标准库 errgroup.Group 不是“传递错误处理函数”的工具,而是用来**等待一组 goroutine 全部完成,并汇总首个非 nil 错误**的同步原语。它不接收、不转发、也不执行你传进去的“错误处理函数”——你常看到的 func(error) {} 类型参数,其实是误用或混淆了它的设计意图。
真正需要的,通常是:启动多个并发任务 → 任一失败就快速取消其余 → 最终拿到第一个错误(或全部错误)。errgroup.Group 正好干这事,但必须配合 context.Context 使用,否则 cancel 机制失效。
为什么直接传 error 处理函数会失效?
常见误区是这样写:
g.Go(func() error {
if err := doWork(); err != nil {
handleErr(err) // ❌ 这里调用自定义函数,但 errgroup 完全不知道
return err
}
return nil
})
errgroup.Group 只关心你返回的 error 值,它不会拦截、包装或触发你自己的 handleErr。一旦你提前调用了 handleErr,又返回了 err,就可能重复处理;如果 handleErr 做了日志或 panic,还可能干扰 g.Wait() 的错误聚合逻辑。
立即学习“go语言免费学习笔记(深入)”;
- 错误处理应放在
g.Wait()之后统一做,而不是在每个 goroutine 内部分散调用 - 若需记录每个子任务的错误细节,得自己用
sync.Mutex+ 切片收集,errgroup不提供该能力 - 传入的函数签名必须是
func() error,不能是func(error)—— 后者根本无法注册到g.Go
正确用法:Context 控制 + Wait 拿错误
标准模式是让所有 goroutine 共享同一个 context.Context,并在工作函数中检查 ctx.Err() 主动退出。错误只由 g.Wait() 返回一次:
g, ctx := errgroup.WithContext(context.Background())
for i := range tasks {
i := i // 防止闭包引用同一变量
g.Go(func() error {
select {
case <-ctx.Done():
return ctx.Err() // 被取消时返回 context.Err()
default:
}
if err := runTask(tasks[i]); err != nil {
return err // ✅ 直接返回,由 Wait 汇总
}
return nil
})
}
if err := g.Wait(); err != nil {
log.Printf("task failed: %v", err) // ✅ 所有错误处理放这里
}
-
errgroup.WithContext创建的 group 自动集成 cancel 逻辑,无需手动调用cancel() - 每个 goroutine 必须主动检查
ctx.Err(),否则可能继续运行直到自然结束(浪费资源) -
g.Wait()返回的是第一个非 nil error,不是所有 error;如需全部错误,得自己维护切片
性能与边界注意点
errgroup.Group 本身开销极小,但实际瓶颈常来自 context 取消传播延迟、goroutine 启动成本、或任务自身阻塞。几个容易被忽略的细节:
- 不要在循环内反复创建
errgroup.Group,复用一个实例即可;频繁 new 会增加 GC 压力 - 如果某个 goroutine panic,
g.Wait()会返回panic: ...包装的 error,但不会自动 recover —— 仍需靠 defer/recover 在 goroutine 内兜底 - Go 1.21+ 支持
g.Go接收带 context 的函数,但标准errgroup还没内置;如需更细粒度控制,考虑golang.org/x/sync/errgroup的最新版(比标准库更新)
真正的“优雅”,不是把错误处理函数塞进 goroutine,而是让并发控制和错误归因清晰分离:启动用 g.Go,取消靠 ctx,收尾靠 g.Wait(),处理靠你自己的日志或重试逻辑。


















