errgroup.group 不能直接替代 sync.waitgroup,因其核心职责是等待所有 goroutine 结束并捕获首个非 nil 错误,且依赖 context 主动取消而非强制中断;未响应 ctx.done() 的任务无法真正停止。

为什么 errgroup.Group 不能直接替代 sync.WaitGroup
因为 errgroup.Group 的核心职责不是“等所有 goroutine 结束”,而是“等所有 goroutine 结束 *且* 捕获第一个非 nil 错误”。一旦某个子任务返回错误,后续未启动的任务会被自动取消(如果用了 ctx),已运行中的任务也不会被强制中断——它只负责传播和等待,不负责杀协程。
常见错误现象:errgroup.Group 看似“等完了”,但主流程继续执行时发现部分任务根本没跑完,或者错误没被触发就返回了 nil。这是因为没传入带取消能力的 context.Context,或任务内部压根没检查 ctx.Err()。
- 必须用
eg.Go(func() error { ... })启动任务,不能用go func() {}()手动起 goroutine - 若要实现“任意一个出错就停发新任务 + 中断正在跑的”,任务函数里得主动轮询
ctx.Done()并提前返回 -
eg.Wait()返回的是第一个非nil错误;即使多个任务都失败,你也只能拿到其中一个
怎么让失败任务真正“停下来”,而不是只停发新任务
靠 errgroup.Group 自身做不到。它只提供 ctx 的派生(eg.WithContext(ctx)),但不会帮你做中断逻辑。是否能停、怎么停,完全取决于你写的任务函数有没有响应 ctx.Done()。
典型场景:并发调用 HTTP 接口、批量写文件、数据库批量插入。这些操作本身可能阻塞数秒,不主动检查上下文,就无法及时退出。
立即学习“go语言免费学习笔记(深入)”;
- HTTP 请求要用
http.NewRequestWithContext(ctx, ...),不能用http.NewRequest - 数据库查询要传
ctx到db.QueryContext或tx.StmtContext - 长时间循环中每隔几轮加一次
select { case - 不要在任务里忽略
ctx.Err(),比如写成if err != nil && !errors.Is(err, context.Canceled)却不返回
errgroup.WithContext 的 ctx 该从哪来?别用 context.Background()
用 context.Background() 就等于放弃了超时控制和手动取消能力。批量任务最常需要的是“整体超时”或“用户点击取消”,这时候必须用 context.WithTimeout 或 context.WithCancel 包一层。
性能影响:派生的 ctx 开销极小,但若超时时间设得太短(比如 100ms),可能导致大量任务还没真正开始就被取消;设得太长(比如 30s),又起不到保护作用。
- 推荐模式:
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second),并在defer cancel()(注意不是在eg.Go里 defer) - 如果上层已有
ctx(如 HTTP handler 的r.Context()),直接传进去,再用errgroup.WithContext派生即可 - 别在循环里反复调用
errgroup.WithContext—— 每个errgroup.Group实例只应绑定一个ctx
错误聚合?errgroup.Group 不干这事,得自己补
errgroup.Group 明确只返回第一个错误。如果你需要知道“总共几个失败”“每个失败原因是什么”,它不提供接口,也不存历史错误。这是设计取舍,不是 bug。
容易踩的坑:有人试图在 eg.Go 闭包里把错误 append 到全局 slice,结果遇到竞态——因为多个 goroutine 并发写同一 slice,且没加锁。
- 安全做法:用
sync.Mutex包裹错误收集逻辑,或改用errgroup.Group+ channel 收集(但要注意 channel 容量和关闭时机) - 更轻量方案:用
errgroup.Group做主控,每个任务返回自定义错误类型(含任务 ID 和原始 error),主流程在Wait()后再查日志或监控指标 - 别依赖
eg.Wait()的返回值判断“是否全部成功”——它返回nil只代表“没遇到第一个错误”,不代表所有都成功(比如某个任务 panic 了但没 return error)
复杂点在于:错误处理和任务取消是两层事,errgroup.Group 只管其中一层;另一层得你自己用 context 和显式检查去填。漏掉任意一层,都会让“批量失败”的行为变得不可预测。










