sync.waitgroup仅用于等待goroutine完成,不具备错误传播、超时控制、取消响应等任务编排能力;它只是基于计数器的同步原语,硬套任务编排易导致逻辑混乱、错误掩盖和goroutine泄漏。

别用 sync.WaitGroup 做任务编排 —— 它只管“等”,不管“错”“超时”“取消”,硬套只会让逻辑越来越难维护。
为什么 sync.WaitGroup 不适合任务编排
它只是一个计数器同步工具,没有错误传播、不响应 context.Context 取消、也不区分任务成败。常见翻车点:
- 多个 goroutine 并发执行,其中一个返回 error,
wg.Wait()仍会等完所有,你得自己收集、判断、中断其余任务 - 忘记在每个 goroutine 入口调用
wg.Add(1),或在错误路径漏掉wg.Done(),导致wg.Wait()永远阻塞 - 用闭包捕获循环变量(如
for _, u := range urls { go func() { fetch(u) }() }),结果所有 goroutine 都用了最后一个u的值 - 无法统一控制超时:你得自己起 timer + select,还要手动 cancel 正在运行的 HTTP 请求或数据库查询
什么时候该用 errgroup.Group
当你需要并发执行多个任务,并满足以下任意一条:
- 任一任务出错,立刻停止其他正在运行的任务
- 希望
Wait()返回第一个非nil错误,而不是忽略或覆盖 - 整个流程需支持超时或外部取消(比如 HTTP handler 被客户端断开)
- 串行步骤和并行分支混用,但又不想引入重型 workflow 库
必须用 errgroup.WithContext(ctx) 初始化,否则 eg.Go() 启动的 goroutine 不会响应 ctx.Done()。零值 &errgroup.Group{} 是无效的,会静默失效。
errgroup.Group 的典型误用与修正
常见写法错误直接导致错误被吞、goroutine 泄漏或取消失效:
- 传入无返回值函数:
eg.Go(func() { ... })→ 编译失败;必须是func() error - 错误后不 return:
if err != nil { log.Println(err); return nil }→eg.Wait()得到nil,掩盖问题;应return err - 忽略上下文检查:HTTP 请求里没写
select { case ,即使 <code>ctx已取消,请求仍在后台跑 - 想从
eg.Go()拿返回值:它不支持;需用闭包变量 +sync.Once或 channel 显式收集
正确示例片段:
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
eg, ctx := errgroup.WithContext(ctx)
for _, url := range urls {
u := url // 防闭包陷阱
eg.Go(func() error {
req, err := http.NewRequestWithContext(ctx, "GET", u, nil)
if err != nil {
return err
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
resp.Body.Close()
return nil
})
}
if err := eg.Wait(); err != nil {
log.Printf("workflow failed: %v", err) // 这里拿到的就是第一个失败任务的 error
}
串并混合编排:别堆 errgroup,用 channel 协调分支
纯并行或串行+并行组合,errgroup 够用;但一旦出现条件逻辑(比如“B 成功才跑 C,否则跑 D”),就得靠 channel + select 显式建模数据流。
- 每个任务启动时创建自己的
done chan struct{},成功/失败都close(done) - 主流程用
select等待多个donechannel,根据哪个先关闭决定下一步动作 - 所有任务共用同一个
ctx,由主流程统一控制生命周期;串行步骤之间显式传参,不依赖闭包共享状态 - 避免在 goroutine 内部做无超时的阻塞操作(如
time.Sleep、无 buffer 的 channel 发送),否则可能卡死整个编排链
最易被忽略的一点:无论用 errgroup 还是 channel,每个 I/O 操作前都得检查 ctx.Err(),否则取消信号进不来,资源就一直挂着。这不是可选项,是并发安全的底线。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











