应使用 io.Copy 流式追加写入而非内存拼接,以避免 GC 压力或 OOM;需显式排序、正确打开目标文件(os.O_APPEND)、及时关闭源文件句柄,并处理分隔符、编码、校验等细节。

用 io.Copy 逐个追加写入,别用内存拼接
直接把所有小文件读进内存再合并,哪怕总大小才几十 MB,也容易触发 GC 压力或 OOM,尤其在资源受限环境。正确做法是打开目标大文件(os.OpenFile 配合 os.O_CREATE | os.O_WRONLY | os.O_APPEND),然后对每个小文件句柄调用 io.Copy 流式写入。
常见错误是用 os.ReadFile + append([]byte, ...) 拼接,这不仅浪费内存,还让文件内容反复拷贝。真实场景中,小文件可能来自不同目录、命名无序,但顺序很重要——必须按业务要求显式排序后再遍历,io.Copy 本身不保证顺序。
- 确保目标文件以
os.O_APPEND打开,否则每次写入会覆盖开头 - 每个源文件用
os.Open打开后,务必defer src.Close(),漏掉会导致文件句柄泄漏 - 如果小文件含 BOM 或编码不一致,
io.Copy不做转换,合并后可能乱码——这是内容层面问题,需前置清洗
需要保留原始文件名或分隔符?手动写入元数据头
io.Copy 只管二进制流,不处理边界。若后续要拆分,得自己约定格式:比如每段前写 4 字节长度 + 文件名 UTF-8 字节数组,或简单用固定分隔字符串(如 --- FILE: xxx.txt ---\n)。注意分隔符不能出现在原始文件内容里,否则解析会错位。
示例片段:
立即学习“go语言免费学习笔记(深入)”;
header := fmt.Sprintf("--- FILE: %s ---\n", filepath.Base(srcName))
_, _ = dst.Write([]byte(header))
_, _ = io.Copy(dst, src)这种方案适合调试或内部工具;生产环境建议用归档格式(如 tar)替代纯拼接,自带路径、权限、校验信息。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 分隔符必须用
dst.Write显式写入,io.Copy不会帮你加 - 如果文件名含非 ASCII 字符,
filepath.Base返回的仍是原始字节,无需额外编码 - 用
\n而不是\r\n,避免 Windows 下行尾差异影响解析
并发合并反而更慢,除非 I/O 是瓶颈且磁盘支持并行
多数情况下,并发读多个小文件写入同一个大文件,会因磁盘寻道加剧、系统缓冲区竞争而变慢。Go 的 io.Copy 已经做了 buffer 复用和 syscall 优化,单 goroutine 足够应付普通 SSD 或 NVMe。
只有当小文件分散在不同物理磁盘(比如挂载点 /mnt/disk1、/mnt/disk2),且你用多个 os.File 分别写入不同目标文件时,并发才有意义。合并到单个文件时强行开 goroutine,大概率增加锁争用和调度开销。
- 别用
sync.WaitGroup包裹一堆go func() { io.Copy(...) }()—— 这是典型过早优化 - 如果真要提速,优先考虑提升单次
io.Copy的 buffer 大小(通过io.CopyBuffer自定义 64KB 以上 buffer) - 监控
iostat -x 1,若 %util 接近 100% 且 await 很高,说明是磁盘瓶颈,此时并发无用
合并后校验失败?检查是否忽略了文件末尾换行或空字节
常见现象:合并后文件比各小文件大小之和多几个字节,或 md5sum 对不上。原因通常是写入分隔符时多加了 \n,或者某个小文件末尾有 \x00(比如二进制 dump 文件),而你用 string() 处理时截断了。
验证方法:用 xxd -l 32 bigfile.bin 和各小文件对比头部,用 tail -c 16 small1.bin | xxd 看结尾。不要依赖 os.Stat().Size 简单相加——它不含分隔符长度,也不反映实际写入字节数。
- 写入前后都用
dst.Sync()强制刷盘,避免缓存导致校验时机不准 - 如果小文件来自网络或临时生成,确认它们已完整写入磁盘(
src.Close()后再读) - 用
cmp或sha256sum校验,别只比对ls -l显示的大小
实际合并逻辑里最易被忽略的,是文件打开模式与关闭时机的配对,以及分隔符是否真正写入成功——这两处出错,不会 panic,但结果完全不可靠。

















