errgroup.group 更可靠,因其通过 context 自动取消后续 goroutine,避免资源浪费和状态不一致;手动实现易漏 cancel 传播、忽略 ctx.err() 或引发竞态。

errgroup.Group 为什么比自己用 sync.WaitGroup + channel 捕错更可靠
因为 errgroup.Group 在首次遇到非 nil 错误时会自动取消后续 goroutine(通过内置的 context.Context),避免“错误已发生却还在跑”的资源浪费和状态不一致。自己手撸 WaitGroup 容易漏掉 cancel 传播、忘记判断 ctx.Err() != nil 提前退出,或在多个 goroutine 同时写 error channel 时出现竞态。
- 默认使用
context.Background(),但建议显式传入带超时的 context(如context.WithTimeout) - 所有子任务必须检查
ctx.Err()并及时返回,否则 cancel 不生效 - 一旦任一子任务返回非
nilerror,g.Wait()就立即返回该 error,其余仍在运行的任务会被 context 取消(前提是它们响应了 cancel)
如何正确启动并等待一组并发 HTTP 请求
典型场景:批量调用外部 API,任一失败即整体失败,且需控制超时和并发数。关键点不是“怎么发请求”,而是“怎么让每个请求尊重上下文、及时退出、不泄露 goroutine”。
- 用
errgroup.WithContext(ctx)初始化,而非零值errgroup.Group{} - 每个请求函数应接收
ctx context.Context参数,并在http.NewRequestWithContext(ctx, ...)中传入 - 务必在
http.Do()后检查resp, err := client.Do(req)的err,再检查resp.StatusCode;不要只依赖 status 判定失败 - 若需限制并发数(如最多 5 路并发),用
semaphore包或简单 channel 控制,errgroup本身不提供限流
g, ctx := errgroup.WithContext(context.WithTimeout(context.Background(), 5*time.Second))
urls := []string{"https://a.com", "https://b.com", "https://c.com"}
for _, u := range urls {
url := u // 避免循环变量捕获
g.Go(func() error {
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return err
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
return fmt.Errorf("HTTP %d for %s", resp.StatusCode, url)
}
return nil
})
}
if err := g.Wait(); err != nil {
log.Printf("batch failed: %v", err) // 任一失败即到这里
}
常见 panic 场景:nil pointer 或 context canceled 被忽略
最常踩的坑是子任务里用了未初始化的对象(比如 client 为 nil),或对 ctx.Err() 视而不见,导致 goroutine 卡住、Wait 不返回、甚至 panic。
- panic 示例:
panic: runtime error: invalid memory address or nil pointer dereference—— 检查所有传入的 client、config、logger 是否已初始化 - 静默失败示例:子任务中没检查
ctx.Err(),超时后仍继续执行 DB 查询或文件读取,造成资源堆积 - 错误覆盖问题:多个 goroutine 同时返回不同 error,
errgroup只返回第一个,后续 error 被丢弃(这是设计行为,不是 bug) - 不要在
g.Go()闭包里直接调用log.Fatal或os.Exit,这会让整个进程退出,绕过g.Wait()的错误聚合
什么时候不该用 errgroup.Group
它适合“全成功才成功”的场景。如果需要收集全部错误(比如批量校验字段,要返回所有失败项)、或允许部分失败继续执行(如“尽力上传 10 个文件,成功几个算几个”),errgroup 就不合适。
立即学习“go语言免费学习笔记(深入)”;
- 替代方案:用
sync.WaitGroup+sync.Mutex+[]error手动收集 - 或改用
errgroup.WithContext但把 error 存到共享切片里,最后统一处理(需注意并发写安全) - 若任务间有依赖顺序(B 必须等 A 成功后才启动),errgroup 不适用,应考虑
pipeline模式或状态机
真正难的从来不是并发启动,而是定义清楚“错误意味着什么”——是立刻终止?还是记录后继续?errgroup 假设你选的是前者,用之前先确认这个假设成立。










