用 io.multireader 可避免内存爆炸和文件描述符耗尽:它按需转发读取流,不预加载内容;需分批打开文件并及时关闭,而非复用 *os.file 或全量读入内存。

用 io.MultiReader 避免内存爆炸
合并几千个小文件时,最常见错误是把每个文件全读进内存再拼接,结果 runtime: out of memory 直接崩掉。小文件虽单个几 KB,但几千个加起来可能几百 MB,还带大量 GC 压力。io.MultiReader 是标准库里专为这种场景设计的——它不加载内容,只按需转发读取流。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 按文件路径顺序构造
[]io.Reader切片,每个元素是os.Open()返回的*os.File(注意别漏defer f.Close()) - 传给
io.MultiReader得到一个统一io.Reader,再用io.Copy写入目标文件,全程零内存缓冲 - 不要用
strings.Join或bytes.Buffer.Write拼接字符串,那等于主动申请大内存
文件打开太多导致 too many open files
Linux 默认单进程最多打开 1024 个文件描述符,几千个文件挨个 os.Open 不关,到几百个就触发 open /path/to/file: too many open files。这不是代码逻辑错,是资源管理没跟上。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 用
sync.Pool复用*os.File实例?不行——*os.File不可复用,且Close()后不能再用 - 正确做法:分批处理,比如每 200 个文件为一组,读完一批立刻全部
Close(),再开下一批 - 或改用
os.ReadDir+os.ReadFile(适合单次读小文件),它内部自动Close,但要注意ReadFile会把整个文件读进内存,仅限确认文件确实都很小(
跨平台换行和编码问题怎么处理
Windows 的 \r\n、macOS/Linux 的 \n 混在一起时,直接拼接会导致行尾混乱;更隐蔽的是 UTF-8 BOM —— 某些编辑器保存的小文件开头带 \uFEFF,连着读就会在中间冒出乱码。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 如果业务允许,统一用
\n分隔文件内容:每次读完一个文件后,手动写一个\n(用io.WriteString),而不是依赖原文件末尾自带 - BOM 检测很简单:读取前几个字节,检查是否等于
[]byte{0xEF, 0xBB, 0xBF},是的话跳过这 3 字节再读余下内容 - 不推荐在合并时做编码转换(如 GBK → UTF-8),容易出错;应提前确保所有源文件已是 UTF-8 无 BOM
为什么不用 exec.Command("cat", ...)
有人图省事想调系统 cat,但实际会踩三个坑:一是路径含空格或特殊字符时参数易被 shell 解析错;二是 Windows 没 cat,得切逻辑;三是子进程启动开销大,几千次调用比纯 Go 实现慢数倍。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 坚持用 Go 原生 I/O,
os.Open+io.Copy+io.MultiReader组合已足够快,实测万级小文件合并耗时通常在秒级 - 若真要调外部命令,必须用
exec.Command而非exec.CommandContext(避免超时中断导致部分文件漏读),且所有路径用filepath.Clean预处理 - 注意
cat在 macOS 和 Linux 行为一致,但某些嵌入式环境或容器里可能没有,Go 二进制自带运行时更可靠
真正卡住的往往不是读写速度,而是忘记及时 Close 导致 fd 耗尽,或者默认用 ReadFile 把所有内容塞进内存——这两个点,看日志报错最明显,但调试时最容易忽略。










