errgroup.group比sync.waitgroup更适合错误传播,因其内置首次错误即返回机制,自动终止未启动任务,但不强制取消已运行goroutine,需配合context实现可取消并发,且必须显式调用wait()。

errgroup 本身不加速合并,它只协调并发和错误;真正提速靠的是并发读取 + 合理控制 goroutine 数量,否则容易打满磁盘或句柄。
为什么用 errgroup 而不是直接用 sync.WaitGroup
因为 WaitGroup 不管错误:某个文件读失败(比如权限不足、路径不存在),其他 goroutine 还在傻等,主流程得自己查 errs 切片才知道出事了。而 errgroup.Group 能在第一个 Go() 返回非 nil error 时,让 Wait() 立刻返回该错误——省去手动收集判断的样板代码。
- 它默认「短路」:一个任务出错,后续未启动的任务不再派发(但已启动的仍运行)
- 它不自动取消正在跑的 goroutine:没有强制 kill,超时或错误后还在读的文件操作会继续,直到自己结束或主动检查
ctx.Done() - 它必须显式调用
Wait(),否则主 goroutine 可能提前退出,导致部分文件根本没读
如何避免并发读取时的资源打满
开 100 个 goroutine 读 100 个文件?大概率触发 too many open files 或 IO 阻塞。正确做法是用带缓冲 channel 或信号量限流,errgroup 本身不提供限流能力。
- 用
sem := make(chan struct{}, 5)控制最多 5 个并发读取,每个 goroutine 入口先sem ,退出前 <code> - 或者更简洁:用
errgroup.WithContext(ctx)配合外部限流逻辑,把任务拆成批次提交 - 不要把所有文件名一股脑丢给
g.Go()—— 尤其当文件数上千时,先分批(比如每批 20 个),每批用一个errgroup.Group
合并字符串时的内存与顺序问题
并发读取天然无序,如果你依赖「按输入顺序拼接字符串」,不能靠 goroutine 启动顺序保证。需要额外索引或 channel 收集结果。
- 最简方案:用
results := make(chan string, len(filenames)),每个 goroutine 读完后results ,主 goroutine <code>for i := range filenames { s := - 若需保持顺序且不想用 channel 缓存全部内容,可改用切片 +
sync.Mutex:每个 goroutine 计算好位置后写入contents[i] = data,注意初始化切片长度 - 大文件慎用
os.ReadFile:会一次性加载进内存,考虑用bufio.Scanner流式读 + 拼接,或直接写入目标文件避免中间字符串分配
errgroup.WithContext 的超时不是“中断读取”,而是“不再等”
这是最容易误解的一点:context.WithTimeout(ctx, time.Second) 传给 errgroup.WithContext(),并不会让正在读大文件的 goroutine 停下。它只做两件事:
- 超时后,
g.Wait()立即返回context.DeadlineExceeded - 已启动但未完成的任务,仍会继续执行,除非它们自己检查
ctx.Err() != nil并提前退出 - 所以对
os.Open或io.ReadAll这类阻塞调用,必须包装成支持 context 的版本(如用io.Copy配合context.Reader,或自己加 select 检查ctx.Done())
实际项目里,真正影响性能的往往不是并发模型本身,而是打开文件、系统缓存、磁盘寻道这些底层行为。errgroup 是个好帮手,但别指望它单靠换包就让合并快十倍。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











