
本文介绍如何使用 errgroup 包实现并发任务的优雅终止与错误传播,避免 goroutine 泄漏,确保任意子任务失败时立即取消其余任务并返回首个错误。
本文介绍如何使用 errgroup 包实现并发任务的优雅终止与错误传播,避免 goroutine 泄漏,确保任意子任务失败时立即取消其余任务并返回首个错误。
在 Go 并发编程中,「启动多个 goroutine 并行执行、任一失败即快速终止其余任务」是一个高频场景。若手动管理 sync.WaitGroup 与通道(如原始代码所示),极易因未及时关闭通道、未响应取消信号而导致 goroutine 泄漏——这正是示例中 leaks 映射所揭示的问题。
更优解是采用标准生态推荐的 golang.org/x/sync/errgroup 包。它封装了上下文取消、错误收集与同步等待逻辑,使代码既简洁又健壮。其核心机制如下:
-
errgroup.WithContext(ctx)创建一个带取消能力的组,所有子 goroutine 共享该ctx; - 每次调用
errs.Go(f)启动一个任务,f必须返回error; - 若任一任务返回非
nil错误,errgroup会自动调用ctx.Cancel(),使其余仍在运行的任务能通过检查ctx.Err()主动退出; -
errs.Wait()阻塞至所有任务完成或首个错误发生,并直接返回该错误(无须遍历通道);若全部成功,则返回nil。
以下是重构后的完整实现:
package main
import (
"context"
"fmt"
"math/rand"
"time"
"golang.org/x/sync/errgroup"
)
func fetchAll(ctx context.Context) error {
// 创建带上下文的 errgroup
errs, ctx := errgroup.WithContext(ctx)
// 并发启动 4 个 fetch 任务
for i := 0; i < 4; i++ {
i := i // 避免循环变量捕获问题
errs.Go(func() error {
// 模拟网络请求:随机延迟后返回错误
select {
case <-time.After(time.Duration(rand.Intn(100)) * time.Millisecond):
return fmt.Errorf("goroutine %d failed", i)
case <-ctx.Done():
return ctx.Err() // 响应取消信号
}
})
}
// 等待所有任务完成或首个错误发生
return errs.Wait()
}
func main() {
// 使用 context.Background() 或带超时的 context
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
if err := fetchAll(ctx); err != nil {
fmt.Printf("Operation failed: %v\n", err)
} else {
fmt.Println("All fetches succeeded")
}
}✅ 关键优势说明:
-
零泄漏:
errgroup内部自动管理 goroutine 生命周期,无需手动WaitGroup或close(chan); -
即时取消:首个错误触发
ctx.Cancel(),其余任务可通过ctx.Done()检测并优雅退出; -
语义清晰:
Go(func() error)明确表达“启动一个可返回错误的任务”,Wait()直观表达“等待结果”; -
可扩展性强:天然支持超时(
context.WithTimeout)、截止时间(context.WithDeadline)及手动取消(cancel())。
⚠️ 注意事项:
- 务必在闭包中显式复制循环变量(如
i := i),否则所有 goroutine 可能共享同一变量值; - 任务函数内需主动检查
ctx.Err()(尤其在长时间阻塞操作前),否则无法及时响应取消; -
errgroup不替代业务级重试逻辑——它只负责“失败即止”,重试应由上层或任务内部实现。
综上,errgroup 是 Go 官方推荐的、处理“并行执行 + 快速失败”模式的惯用方案,兼顾简洁性、安全性与可维护性。对于新项目,应优先选用此模式替代手写通道与 WaitGroup 组合。

















