errgroup.group 能统一收集首个错误、自动等待所有 goroutine 结束并支持上下文取消,但需避免直接使用 context.background();sync.waitgroup 无法传播错误或中断任务,而 errgroup 具备短路机制。

用 errgroup.Group 控制并发任务,核心就一条:它能统一收集首个错误、自动等待所有 goroutine 结束,且支持上下文取消——但前提是别直接传 context.Background() 而忘了绑定超时或取消信号。
为什么不能直接用 sync.WaitGroup 替代 errgroup.Group
sync.WaitGroup 只管等待,不处理错误传播;一旦某个 goroutine panic 或返回错误,主流程无法及时感知和中断其余任务。而 errgroup.Group 内置错误短路机制:只要一个子任务返回非 nil 错误,后续未启动的任务不会执行(如果用了 Go 方法),已运行的也会在 Wait 时被阻塞并返回该错误。
- 常见错误现象:
WaitGroup等到所有 goroutine 完成才返回,但中间某个任务早失败了,主逻辑还在等“不可能成功”的结果 - 使用场景:批量调用外部 API、并行读多个文件、分片处理数据流
- 性能影响:
errgroup.Group几乎无额外开销,底层仍是sync.WaitGroup+ channel 控制错误传递
errgroup.WithContext 的 context 必须可取消
传入的 context.Context 是整个组的生命周期开关。如果用 context.Background() 或没有设置超时,即使某个任务卡死,Wait 会永远阻塞;若用 context.WithTimeout 或 context.WithCancel,任一子任务出错或超时,所有 goroutine 都能通过 ctx.Err() 检测并主动退出。
- 正确做法:
g, ctx := errgroup.WithContext(context.WithTimeout(context.Background(), 5*time.Second)) - 容易踩的坑:在子函数里忽略
ctx.Done()检查,导致 goroutine 无法响应取消,形成 goroutine 泄漏 - 参数差异:
WithContext返回的是*Group和新 context,不是原 context 的拷贝——子任务必须显式接收并使用这个ctx
如何安全地在循环中启动并发任务
闭包捕获循环变量是 Go 常见陷阱。直接在 for range 中调用 g.Go(func() error {...}),所有 goroutine 共享同一个变量副本,最终可能全处理最后一个元素。
- 正确写法:把循环变量作为参数传入匿名函数,例如
g.Go(func(item string) error { return process(item) }(item)) - 或者提前赋值:
item := item; g.Go(func() error { return process(item) }) - 使用场景:遍历 URL 列表并发请求、处理一组文件路径
- 注意点:如果任务数极大(如上万),建议加限流——
errgroup本身不提供并发数控制,需配合semaphore或手动批处理
Wait 返回错误后,如何判断是哪个任务失败
errgroup.Group.Wait 只返回第一个非 nil 错误,不附带来源信息。如果需要定位具体失败项,得靠任务内部记录日志或返回带上下文的错误(比如用 fmt.Errorf("failed to process %s: %w", item, err))。
- 不要依赖
errgroup自动区分错误来源——它设计目标就是“快速失败”,不是调试工具 - 调试建议:在每个子任务开头打日志,如
log.Printf("start processing %s", item),配合错误日志可对齐时间线 - 兼容性提醒:Go 1.20+ 支持
errors.Join,但errgroup不聚合所有错误,只返回首个;如需全部错误,得自己维护 slice 并同步写入
真正难的不是调用 Go 和 Wait,而是确保每个子任务都尊重传入的 ctx、正确传播错误、不意外捕获循环变量——这些地方一漏,errgroup 就从帮手变成幻觉。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











