核心是“别翻车”:内存不爆、顺序不错、错误可查、磁盘不卡死;应采用 io.copy + worker pool 流式处理,显式排序文件名、固定 worker 数、带缓冲 channel 传递任务与结果。

Go 里并发处理海量小字符串文件(比如每份几百 KB 的日志、配置、文本分片),核心不是“怎么快”,而是“别翻车”:内存不爆、顺序不错、错误可查、磁盘不卡死。直接起上万个 goroutine 读文件再拼接,十有八九在 GC 或 syscall 上挂掉。
为什么不能用 os.ReadFile + channel 收集再合并
看似简单,实则三处硬伤:
-
os.ReadFile把整个文件加载进内存,1000 个 500KB 文件 = 500MB 堆分配,GC 频繁,runtime 可能直接out of memory - 无缓冲
chan []byte在高吞吐下变成性能瓶颈,goroutine 阻塞在 send 上,实际并发度归零 - 文件名按字典序返回(
filepath.Glob("*.txt")→part_1.txt,part_10.txt,part_2.txt),不显式排序,合并后数据错位,二进制文件直接损坏
io.Copy + worker pool 是唯一稳态路径
流式读写 + 固定 worker 数量,才是生产环境可行方案。关键动作:
- 目标文件用
os.OpenFile(dst, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)—— 覆盖写入,避免旧内容残留 - 每个 worker 拿到一个文件路径后,
os.Open→io.Copy(dst, src)→src.Close(),全程不落地内存 - worker 数设为
runtime.NumCPU()或实测最优值(SSD 通常 4–8,HDD 控制在 2–4) - 任务 channel 必须带缓冲:
tasks := make(chan string, 100),防主 goroutine 在发送时阻塞
排序必须在合并前做,且不能依赖文件系统顺序
合并顺序错了,结果就废了。尤其分片文件如 data_1.txt, data_10.txt, data_2.txt:
- 别用
sort.Strings(files)—— 字符串排序是字典序,"10"排在"2"前面 - 正确做法:提取数字,再
sort.Slice(files, func(i, j int) bool { return idx[i] - 更稳妥的是生成时就零填充:
fmt.Sprintf("data_%04d.txt", i),运行时省去解析逻辑 -
os.ReadDir不保证顺序,filepath.Walk更不可靠,所有排序逻辑必须显式控制
结果收集要带上下文,不能只打 log
并发中 log.Print 会混在一起,失败项无法定位重试:
- 定义结构体:
type MergeResult { Path string; Success bool; Err error } - 用带缓冲 channel 接收结果:
results := make(chan MergeResult, 100) - 主 goroutine 关闭
tasks后,用for i := 0; i 等齐所有结果 - 失败路径单独写入
failed_paths.log,方便后续grep -f failed_paths.log *.txt | xargs -I{} go run merge.go -file {}补跑
真正容易被忽略的点,是排序逻辑和合并逻辑必须用同一套索引提取规则——哪怕只是多一个 strings.TrimPrefix(f.Name(), "part_") 里的下划线漏写,整个合并流程就 silently 错位。这不是性能问题,是数据正确性红线。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











