errgroup.Group需显式调用Wait()才能获取错误,否则任务可能未完成或错误被丢弃;Go()函数必须返回error,闭包需捕获循环变量;超时依赖任务内ctx.Done()检查,非errgroup强制终止。

errgroup.Group 的基本用法和常见错误
直接用 errgroup.Group 启动多个 goroutine 时,最容易忽略的是:没调用 Wait() 就结束主流程,导致部分任务根本没执行完,或者错误被静默丢弃。它不是自动阻塞的“等待池”,而是一个带错误传播机制的协作结构。
正确姿势是:所有任务通过 Go() 提交后,必须显式调用 Wait() 才能拿到最终错误。否则 errgroup 不会帮你同步或兜底。
-
Go()提交的任务函数签名必须是func() error,返回非nil错误会立刻被记录并短路后续未启动的任务(取决于是否启用WithContext()) - 如果不用上下文,
Wait()会等所有已启动任务完成;若用了WithContext(ctx),任一任务返回错误或 ctx 被 cancel,其余正在运行的任务不会被强制中断,但新任务不再启动 - 不要在
Go()的闭包里直接引用循环变量(如for i := range tasks),不加val := val捕获会全部指向最后一个值
如何让失败任务不影响其他任务继续执行
默认情况下,errgroup.Group 在第一个错误发生后就停止派发新任务,但已启动的仍在跑。如果你希望「全部任务都尽力执行,最后汇总所有错误」,就不能依赖它的短路逻辑,得自己收集错误。
典型做法是把 errgroup.Group 当作并发控制器,用独立的 sync.Mutex + 切片存错误,而不是靠它的 Err():
var mu sync.Mutex
var errs []error
<p>g := new(errgroup.Group)
for <em>, task := range tasks {
task := task // 防止闭包捕获问题
g.Go(func() error {
if err := task.Run(); err != nil {
mu.Lock()
errs = append(errs, err)
mu.Unlock()
}
return nil // 不返回 err,避免短路
})
}
</em> = g.Wait() // 忽略 group 自身的错误(总是 nil)</p><p>// 最后统一判断
if len(errs) > 0 {
return fmt.Errorf("failed %d tasks: %v", len(errs), errs)
}
</p>
与 context.WithTimeout 配合时的关键行为
用 errgroup.WithContext(ctx) 是最常见也最容易误解的组合。重点不是「超时后自动 kill goroutine」,而是「超时后不再启动新任务,并让 Wait() 返回 ctx.Err()」。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
已运行中的 goroutine 不会被中断 —— Go 没有安全的协程强制终止机制。所以超时处理必须由任务自身配合检查 ctx.Done()。
- 务必在任务内部定期 select
ctx.Done(),尤其是可能阻塞的操作(如 HTTP 请求、channel receive、time.Sleep) -
http.Client等标准库组件要传入带 timeout 的ctx,不能只靠 errgroup 层面的 context - 如果任务忽略了 ctx,在超时后仍会继续跑完,
Wait()虽返回context.DeadlineExceeded,但资源和时间已浪费
为什么有时 Wait() 返回 nil 却仍有任务出错
这是最常被卡住的点:你看到 err == nil,但某些任务明明 panic 或没处理好错误。原因通常是:
- 任务函数里 recover 了 panic 但没返回 error,或者返回了
nil错误 ——errgroup完全感知不到 - 任务中启动了子 goroutine 但没做错误传递,父 goroutine 早早返回
nil,子 goroutine 的错误彻底丢失 - 用了
Go()但函数签名不是func() error,编译不过;或者写了func() { ... }(无返回值),Go 会报类型错误,容易被忽略
调试建议:在每个任务末尾加 log.Printf("task %v done, err: %v", name, err),确认 error 是否真被返回;对关键路径加 defer func() { if r := recover(); r != nil { /* 记录 panic */ } }()。并发错误本身不会消失,只是藏得更深。










