errgroup是错误传播与协同取消协调器,非并发控制器;必须用WithContext初始化,否则goroutine泄漏、超时失效;GoContext()用于可取消IO,Go()仅适用于无阻塞CPU计算;Wait()只返回首个错误且不捕获panic。

errgroup 不是并发数量控制器,而是错误传播与协同取消的协调器。它只在你明确需要「任一失败就中断全部 + 及时释放资源」时才该用;否则,sync.WaitGroup 更轻量、更直接。
必须用 errgroup.WithContext 初始化,零值或裸 new 会泄漏 goroutine
直接写 var g errgroup.Group 或 g := &errgroup.Group{} 是危险操作:内部 sync.Mutex 未初始化,调用 Go() 或 Wait() 会 panic;更重要的是,它完全不响应 context 取消信号——哪怕超时已到,正在跑的 HTTP 请求、文件读取、数据库查询仍继续阻塞,连接不关、goroutine 不退。
- 正确做法:
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)→defer cancel()→g, ctx := errgroup.WithContext(ctx) - 超时时间要覆盖整个链路:比如请求 + 解析 JSON + 写磁盘,不能只包 HTTP 那一步
- 别复用已取消的
ctx创建新errgroup;每个并发批次应配独立上下文
Go() 和 GoContext() 到底选哪个?看任务是否支持主动中断
Go() 接收 func() error,完全不感知 context;GoContext() 接收 func(context.Context) error,内部自动监听 ctx.Done() 并提前退出。二者语义完全不同,混用等于自埋雷。
- IO 类任务(HTTP/DB/File)→ 必须用
GoContext(),并确保底层调用支持 context,例如:http.DefaultClient.Do(req.WithContext(ctx)),而不是http.Get(url) - CPU 密集型短任务(JSON 解析、base64 编码、简单计算)→ 可用
Go(),但得确认它真不阻塞、不依赖外部状态、执行时间可控 - 如果写了
Go()却在里面调time.Sleep(10 * time.Second),而ctx已超时,它照样睡满 10 秒,Wait()一直卡住
Wait() 只返回第一个非 nil 错误,且不捕获 panic
Wait() 的设计目标是“快速失败”,不是“汇总错误”。哪怕 10 个 goroutine 全部出错,它也只返回最先完成的那个 error 值;其余错误被丢弃,连日志都不会留。
立即学习“go语言免费学习笔记(深入)”;
- 若需全量错误,必须自己维护
sync.Mutex+[]error,或用带缓冲的chan error(容量 ≥ 任务数) -
Wait()返回nil≠ 全成功:某个任务里panic("db down")且没recover,程序直接崩溃,根本走不到Wait() - 闭包末尾必须显式
return err;只log.Printf然后return nil,等于掩盖错误,Wait()永远返回nil
子任务里不检查 ctx.Err(),cancel 就是摆设
errgroup.WithContext 提供的是一个可传播取消信号的 ctx,但它不会自动中断你的代码。所有阻塞调用(尤其是不支持 context 的旧版库)都必须手动检查。
- HTTP/DB 等 IO:优先用支持 context 的版本,如
http.NewRequestWithContext(ctx, ...)、db.QueryRowContext(ctx, ...) - 纯计算逻辑:定期插入
if ctx.Err() != nil { return ctx.Err() } - 阻塞操作(如
time.Sleep、自定义 channel 等):必须包在select里监听ctx.Done() - 常见错误写法:
time.Sleep(10 * time.Second)后再return errors.New("done")—— 外部已超时 3 秒,它还在睡,期间无法被中断
真正难的不是启动并发,而是让每个子任务都尊重取消信号、不泄漏资源、不掩盖错误。很多问题不是 errgroup 本身的问题,而是子任务里漏掉了对 ctx.Err() 的判断,或者用了不支持 context 的底层调用。


















