用 io.copy 流式合并最稳最快,避免 os.readfile 导致 oom 或损坏;需自然序排序文件名,零填充或提取数字排序防错位。

用 io.Copy 流式合并,别碰 os.ReadFile
大文件合并失败或卡死,90% 是因为试图把整个文件读进内存。Go 没有“一键合并”函数,但 io.Copy 配合 os.Open 和 os.Create 就是最快最稳的组合——它不缓存、不拼接、不加载全文,只做字节流搬运。
常见错误现象:
-
runtime: out of memory—— 用了os.ReadFile或io.ReadAll处理 GB 级文件 - 合并后文件损坏 —— 手动
Write+Seek错位,或碎片文件顺序未排序 - 性能比 shell
cat还慢 —— 用了bufio.Scanner或逐行ReadString,破坏二进制完整性
实操建议:
- 目标文件统一用
os.Create(dstPath),自动清空并覆盖,避免os.O_APPEND误用于首次写入 - 每个源文件用
os.Open(srcPath),直接传给io.Copy(dst, src),不用中间[]byte缓冲 - 若需追加(如日志轮转),才改用
os.OpenFile(dstPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) - 缓冲区大小无需手动设 ——
io.Copy内部默认用 32KB,对 SSD/NVMe 已足够;若跑在低 I/O 设备上,可传自定义io.CopyBuffer
文件名排序必须用自然序,filepath.Glob 默认是字典序
合并 part_1.bin、part_10.bin、part_2.bin 时,filepath.Glob("part_*.bin") 返回顺序是 1→10→2,导致数据错位。这不是 Go 的 bug,是所有字符串排序的通病。
实操建议:
- 生成分片时强制零填充:
fmt.Sprintf("part_%03d.bin", i)→part_001.bin - 读取前先排序:用
sort.Slice(files, func(i, j int) bool { return extractNum(files[i]) ,其中 <code>extractNum提取文件名中数字并转int - 别依赖
os.ReadDir返回顺序 —— 它不保证任何排序,Linux ext4 和 Windows NTFS 行为也不一致
二进制合并必须校验哈希,os.Stat().Size 不等于内容正确
合并完发现 PDF 打不开、ZIP 解压报 CRC 错误、视频播放卡顿,大概率是静默截断或末尾多写了几字节。文件系统对齐、I/O 中断、io.Copy 返回值未检查,都可能导致这种问题。
实操建议:
- 分割前计算原始哈希:
h := sha256.New(); io.Copy(h, srcFile); originalSum := h.Sum(nil) - 合并后立即重算:
dstFile.Seek(0, 0); io.Copy(sha256.New(), dstFile) - 必须对比完整
[32]byte,别用==比较切片(会比较指针) - 跳过空文件:
if fi, _ := os.Stat(path); fi.Size() == 0 { continue },省去无效 open/copy 开销
PDF 合并不能只靠 io.Copy,得看库和许可证
直接用 io.Copy 合并多个 PDF 文件,结果是“能打开但缺页、无书签、文字乱码”,因为 PDF 是结构化容器格式,不是纯字节流。强行拼接会破坏 cross-reference table 和 object stream。
实操建议:
- 要保留书签/注释/加密 → 用
unidoc/unipdf/v4,但必须调用license.NewLicenseFromBytes()加载许可,否则第 4 页就 panic - 只要快速拼文本类 PDF(如日志归档)→ 用
pdfcpu.MergeFiles(),零依赖、无 license,但不支持 outline - 合并前必做两件事:
pdfcpu.ValidateFile()校验有效性,pdfcpu.GetNumPages()确认页数,避免静默空输出 - 图像密集型 PDF(扫描件、报表)优先选 unipdf;纯文本类用 pdfcpu 内存更省
真正容易被忽略的是:合并后书签层级超过 3 级,macOS 预览会自动折叠,不是代码问题,是阅读器实现限制 —— 验证时得换 Chrome 或 Acrobat。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











