合并文件关键在正确打开模式:日志用os.O_CREATE|os.O_WRONLY|os.O_APPEND;二进制文件需按意图选O_TRUNC或O_APPEND;须自然排序源文件、自定义缓冲区并校验。

直接用 io.Copy 配合 os.OpenFile 就能合并,关键不是“怎么写”,而是“怎么开文件”——开错模式会清空目标、追加错位、覆盖失败,90% 的问题都出在这一步。
合并文本日志:必须用 os.O_APPEND,别碰 os.O_TRUNC
日志类文件(如 .log)天然适合追加。错误做法是用 os.Create 或 os.O_CREATE | os.O_WRONLY,这会导致每次运行都清空已有内容。
- 正确打开方式:
os.OpenFile("merged.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) - 如果目标文件已存在但没加
os.O_APPEND,io.Copy会从开头覆写,旧日志瞬间丢失 - Windows 下
os.O_APPEND保证写入位置始终是末尾,无需手动Seek,也别用WriteAt - 每追加完一个源文件后,建议调一次
dst.Sync(),防止断电或崩溃时最后几 KB 没落盘
合并二进制文件(PDF/ZIP/图片):先明确意图,再选标志位
二进制文件对字节顺序敏感,误追加 = 文件损坏。不能靠“试试看”,必须在代码里显式区分场景:
- 想覆盖重做 → 用
os.O_CREATE | os.O_WRONLY | os.O_TRUNC,然后循环io.Copy - 想追加到已有文件末尾 → 用
os.O_WRONLY | os.O_APPEND,且确保文件已存在(否则报ENOENT) - 想确保不覆盖已有文件 → 加
os.O_EXCL,仅首次创建有效,第二次运行直接失败 - 合并 PDF 时,
unipdf必须提前调license.NewLicenseFromBytes(),否则 panic 报 “license not set”;pdfcpu虽免 license,但完全丢弃书签
源文件顺序乱?别信 filepath.Glob 或 os.ReadDir 的默认顺序
文件系统返回的顺序不保证自然排序,log_1.txt、log_10.txt、log_2.txt 默认按字典序排成 1→10→2,明显错乱。
立即学习“go语言免费学习笔记(深入)”;
- 用
sort.Slice(files, func(i, j int) bool { return naturalLess(files[i].Name(), files[j].Name()) }),其中naturalLess需自己实现或引入golang.org/x/exp/slices - 简单替代法:提取数字前缀,转成
int比较,比如log_10.txt提取10,log_2.txt提取2 -
filepath.Walk返回顺序也不稳定,不要依赖它控制写入次序
大文件合并卡住或内存爆掉?缓冲区和校验得跟上
合并几十 GB 文件时,io.Copy 默认 32KB 缓冲区可能太小,频繁 syscall 拖慢速度;而跳过校验又容易把损坏文件也塞进去。
- 自定义缓冲区:
buf := make([]byte, 1,再传给 <code>io.CopyBuffer(dst, src, buf) - 合并前用
os.Stat(path).Size()过滤空文件,避免无意义Open开销 - 对 PDF 等格式,用
pdfcpu.ValidateFile()或unipdf.NewReader()主动检查,别等io.Copy报 EOF 才发现 - 合并 CSV 且已排序时,别全读进内存,用类似归并排序的双游标流式读取,内存占用恒定
真正容易被忽略的是:合并多个 PDF 时,即使代码逻辑完全正确,若输入文件本身带加密或跨页引用异常,unipdf 可能静默跳过某页——必须在合并前单独验证每个输入文件的完整性,而不是只看 os.Stat 返回的大小。


















