多个goroutine并发时错误不能靠return传递,需用errgroup.Group统一收集首个错误或手动用chan error+sync.WaitGroup聚合所有错误,注意循环变量捕获、缓冲通道、正确关闭和上下文取消。

多个goroutine并发执行时,错误不能只靠return传递
Go 中 goroutine 是异步的,go func() {...}() 启动后立即返回,主 goroutine 不会等待它结束,更无法直接捕获其 return 的错误。想让多个 goroutine 的错误“回传”给调用方,必须显式设计通信路径。
用 errgroup.Group 统一收集并传播第一个错误
标准库 golang.org/x/sync/errgroup 是最常用、最稳妥的选择。它内部基于 sync.WaitGroup 和 chan error,自动处理“任意一个出错就取消其余任务”的常见需求。
使用要点:
-
errgroup.Group的Go方法接收func() error,不是无参函数;返回非nil错误时,会自动取消其他正在运行的 goroutine(需配合ctx) - 必须调用
Wait()才能获取最终错误;若未出错,返回nil - 默认行为是“首次错误即停止”,适合多数场景;如需等全部完成再汇总错误,得自己实现
import (
"context"
"fmt"
"golang.org/x/sync/errgroup"
)
func fetchAll(ctx context.Context) error {
g, ctx := errgroup.WithContext(ctx)
urls := []string{"https://a.com", "https://b.com", "https://c.com"}
for _, url := range urls {
url := url // 避免循环变量复用
g.Go(func() error {
return doFetch(ctx, url) // 返回 error
})
}
return g.Wait() // 阻塞直到全部完成或首个 error 返回}
立即学习“go语言免费学习笔记(深入)”;
手动用 chan error + sync.WaitGroup 更灵活但易出错
当需要自定义错误聚合逻辑(比如收集所有错误、忽略某些类型),可手动管理通道和等待组。但要注意几个硬伤:
- 必须设置带缓冲的
chan error,否则某个 goroutine 出错后写入阻塞,导致其他 goroutine 无法退出 - 不能在 goroutine 内直接
close(ch),必须由主 goroutine 在Wait()后关闭,否则 panic - 主 goroutine 要遍历 channel 拿完所有错误,不能只读一次就认为结束
func fetchAllManual() []error {
var wg sync.WaitGroup
ch := make(chan error, 10) // 缓冲大小 ≥ goroutine 数量
urls := []string{"https://a.com", "https://b.com"}
for _, url := range urls {
wg.Add(1)
go func(u string) {
defer wg.Done()
if err := doFetch(context.Background(), u); err != nil {
ch <- err
}
}(url)
}
go func() {
wg.Wait()
close(ch)
}()
var errs []error
for err := range ch {
errs = append(errs, err)
}
return errs}
立即学习“go语言免费学习笔记(深入)”;
别踩这些坑
实际写的时候,这几个点最容易翻车:
- 忘记对循环变量做闭包捕获(
url := url),导致所有 goroutine 用的是最后一个值 - 用无缓冲
chan error,一旦有错误就卡死,整个程序 hang 住 - 在 goroutine 里
panic却没 recover,错误完全丢失,且可能 crash - 用
select+default非阻塞读 channel,漏掉部分错误 - 把
context.WithTimeout的cancel()放在 goroutine 里调用,导致主流程无法控制超时
复杂点在于:错误传播策略(立即失败 vs 全部尝试)和上下文取消的耦合度很高,选错模式会导致调试困难或资源泄漏。动手前先想清楚——你到底要“尽快止损”,还是“尽力完成”。










