errgroup.group 更可靠,因其自动取消失败后的其余 goroutine 并避免竞态;手动 wait+error 易漏判、覆盖或阻塞;必须用 eg.go 启动任务、返回 error、配合 context 控制超时与中断。

errgroup.Group 为什么比手动 wait + error 处理更可靠
因为 errgroup.Group 在任意子 goroutine 返回非 nil 错误时,会自动取消其余未完成的 goroutine(通过 context),避免资源浪费和逻辑错乱。手动用 sync.WaitGroup + 全局 error 变量容易漏判、竞争或阻塞——比如一个 goroutine panic 后其他还在跑,或者多个 error 同时写入导致覆盖。
常见错误现象:context.DeadlineExceeded 或 context.Canceled 被当成业务错误返回;没调用 eg.Wait() 就直接读 error;多个 goroutine 都返回 error,但只拿到第一个。
- 必须用
eg.Go(func() error { ... })启动任务,不能用go func() {...}()手动起 goroutine - 所有子函数必须返回
error类型,哪怕只是return nil - 如果需要超时控制,用
errgroup.WithContext(context.WithTimeout(...)),别在每个子函数里自己建 context
并发 HTTP 请求失败后如何快速中断其余请求
典型场景:同时调用 5 个下游 API,只要有一个失败(比如返回 500 或超时),就立刻停止剩下还没响应的请求。靠 errgroup + 带 cancel 的 context 最自然。
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
eg, ctx := errgroup.WithContext(ctx)
for _, url := range urls {
url := url // 注意变量捕获
eg.Go(func() error {
req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err // 包括 context.Canceled / context.DeadlineExceeded
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
return fmt.Errorf("HTTP %d for %s", resp.StatusCode, url)
}
return nil
})
}
if err := eg.Wait(); err != nil {
// 这里 err 是第一个非 nil 错误,其余已自动取消
log.Printf("failed: %v", err)
}
关键点:所有 http.Client.Do 都传入同一个 ctx,这样一旦 eg.Wait() 返回 error,cancel 被触发,其余 pending 请求立刻收到 context.Canceled 并退出。
容易踩的坑:http.DefaultClient 默认不设置 timeout,必须靠外层 context 控制;忘记对 url 变量做闭包捕获,导致所有 goroutine 用最后一个值。
eg.Go 中 panic 会导致 Wait 返回什么 error
会返回 panic 的 recover 信息,格式为 "panic: xxx",类型是 *errors.errorString,不是 context.Canceled 或其他标准 error。这意味着你无法靠 errors.Is(err, context.Canceled) 判断是否因取消退出。
- 如果业务逻辑可能 panic(比如索引越界、nil 指针解引用),应在
eg.Go内部加 defer-recover -
eg.Wait()返回的 error 如果是以"panic:"开头,说明某个子 goroutine 崩溃了,不是正常错误流 - 不要依赖
eg.Wait()的 error 类型做 switch 分支,优先用errors.Is或strings.HasPrefix(err.Error(), "panic:")
多个依赖操作串行+并行混合时怎么用 errgroup
比如:先查用户信息(必须串行),再并发查该用户的订单、积分、通知三条数据。不能把查用户也塞进 eg.Go,否则可能并发查出不同用户的混乱状态。
正确做法:串行部分单独做,成功后再初始化新的 errgroup.Group 并发后续任务。
// 1. 串行获取 user
user, err := getUserByID(ctx, userID)
if err != nil {
return err
}
// 2. 并发查关联数据
eg, ctx := errgroup.WithContext(ctx)
eg.Go(func() error { return fetchOrders(ctx, user.ID) })
eg.Go(func() error { return fetchPoints(ctx, user.ID) })
eg.Go(func() error { return fetchNotifications(ctx, user.ID) })
return eg.Wait() // 任一失败则整体失败
复杂点在于:串行步骤的 error 不应被 errgroup “吞掉”;而并行步骤之间要真正隔离失败影响。很多人试图把全部逻辑塞进一个 errgroup,结果串行步骤失败后并行任务还启动了——这既浪费资源,又可能触发副作用(比如发了不该发的通知)。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











