Go 1.21 版本中如何正确使用 errgroup 处理并发任务?

文章导读
在 Go 1.21 版本中使用 errgroup 处理并发任务,必须通过 errgroup.WithContext 初始化并传入带超时或取消能力的 context,子任务内需显式检查 ctx.Err() 以实现错误传播和资源释放。
📋 目录
  1. 快速处理思路
  2. 为什么会这样
  3. 分步处理
  4. 怎么验证是否生效
  5. 常见坑
  6. 常见问题
  7. 参考来源
A A

在 Go 1.21 版本中使用 errgroup 处理并发任务,必须通过 errgroup.WithContext 初始化并传入带超时或取消能力的 context,子任务内需显式检查 ctx.Err() 以实现错误传播和资源释放。

先说结论:errgroup.WithContext 是唯一安全的初始化方式,直接实例化 Group 会导致无法响应取消信号。

  • 适合场景:需要并发执行多个任务且任意出错即取消其余任务的场景。
  • 先看配置:确认 context 是否包含超时设置,避免使用 context.Background() 直接初始化。
  • 建议操作:子任务中优先使用支持 context 的 IO 接口,并手动检查 ctx.Done()。

快速处理思路

以下是 Go 1.21 中标准的 errgroup 并发错误处理代码结构,确保任务可被取消且错误可捕获。

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 {
        // 必须使用支持 context 的请求方法
        req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
        _, err := http.DefaultClient.Do(req)
        return err
    })
}

if err := eg.Wait(); err != nil {
    log.Printf("task failed: %v", err)
}

为什么会这样

errgroup 依赖 context 的 Done 通道传播中断信号,不使用 WithContext 初始化会导致子 goroutine 无法感知取消请求。

直接用 new(errgroup.Group) 或字面量初始化会丢失上下文控制能力,当某个任务出错或超时时,其余任务可能继续运行导致资源泄漏。errgroup.WithContext 返回的 context 是派生上下文,所有子任务共享同一取消信号,确保任意任务返回错误后,其他监听该 context 的任务能及时退出。

分步处理

按以下步骤实施并发任务管理,确保错误处理和资源控制符合预期。

第一步:初始化带超时的 Context
使用 context.WithTimeout 或 context.WithCancel 创建根 context,避免直接使用 context.Background()。确保超时时间覆盖整个任务链,包括网络请求和数据处理。

第二步:使用 WithContext 创建 Group
调用 errgroup.WithContext(ctx) 获取 group 和派生 context。不要复用已取消的 context,否则调用 Go 方法时可能直接 panic。

Go 1.21 版本中如何正确使用 errgroup 处理并发任务?

第三步:提交任务并检查 Context
在 eg.Go 的闭包内,所有阻塞操作必须使用支持 context 的版本。纯 CPU 计算需定期检查 ctx.Err()。循环提交任务时,务必在循环内复制迭代变量,防止闭包捕获同一个变量地址。

第四步:统一等待并处理错误
调用 eg.Wait() 阻塞直到所有任务完成。注意 Wait() 只返回第一个非 nil 错误,后续错误会被丢弃。如需收集全部错误,需在闭包内自行维护错误切片。

怎么验证是否生效

通过日志和错误类型确认并发控制是否按预期工作。

检查取消信号:手动触发取消或等待超时,观察未完成任务是否快速退出而非运行到自然结束。日志中应看到 ctx.Canceled 或 ctx.DeadlineExceeded 错误。

验证错误捕获:故意让其中一个任务返回错误,确认 eg.Wait() 能捕获该错误且程序未挂起。若 Wait() 返回 nil 但业务逻辑未执行,检查是否有 panic 未被 recover。

确认变量独立:在闭包内打印迭代变量值,确认每个任务处理的是正确的数据而非最后一个值。

常见坑

以下场景容易导致并发失控或错误被掩盖,需特别谨慎。

Go 1.21 版本中如何正确使用 errgroup 处理并发任务?

忽略 ctx.Err() 检查:子任务中若不调用支持 context 的 IO 接口且不检查 ctx.Done(),即使主 context 已取消,goroutine 仍会继续运行直到完成。

误以为 Wait 返回 nil 即全成功:eg.Wait() 返回 nil 仅表示所有启动的任务都退出且未报告非 nil 错误,不保证没有 panic 发生。若任务内 panic 且未 recover,程序会直接崩溃。

混淆 Go 和 GoContext 用法:部分版本或扩展库提供 GoContext 方法,若使用标准库 Go 方法,需手动将 ctx 传入闭包。混用可能导致部分任务不响应取消信号。

错误收集误解:errgroup 设计目标是快速失败,只返回首个错误。若业务需要所有任务结果,需自行使用 sync.Mutex 保护错误切片收集。

常见问题

errgroup.Wait() 返回 nil 代表所有任务都成功了吗?

不一定,它只代表所有任务退出且未返回非 nil 错误,若有 panic 发生则不会捕获。

如何让一个任务失败后自动取消其他任务?

必须使用 errgroup.WithContext 初始化,且子任务需监听同一 context 的 Done 信号。

可以在 errgroup 中控制并发数量吗?

errgroup 本身不提供限流,需配合 semaphore 或带缓冲 channel 手动控制并发数。

参考来源

  • golang.org/x/sync/errgroup 官方文档,https://pkg.go.dev/golang.org/x/sync/errgroup
  • Go 语言并发最佳实践技术分析,内部知识库