唯一安全高效的做法是用 io.copy 流式追加,配合 os.openfile 的 os.o_append 标志;覆盖则用 os.create 或 os.o_trunc;os.readfile 和 bytes.buffer 会引发 oom,手动 seek 易错位;必须自然序排序文件名并校验 sha256。

直接用 io.Copy 流式追加,配合 os.OpenFile 带 os.O_APPEND 标志,是唯一既安全又高效的做法;想覆盖重做就改用 os.Create + os.O_TRUNC。
为什么不能用 os.ReadFile 或 bytes.Buffer 合并日志
二进制日志文件(如 app_20240612.bin、app_20240613.bin)一旦达到百 MB 以上,os.ReadFile 就会触发 OOM;bytes.Buffer.Write 把所有内容拼进内存再写入,同样不可行。更危险的是,手动 WriteAt + Seek 容易错位,尤其在 Windows 或旧版 Go 下行为不一致。
真正可靠的路径只有一条:io.Copy 不加载全文、不缓存、不拼接,只做字节流搬运。它内部默认用 32KB 缓冲区,对 SSD/NVMe 已足够快;若需微调,可用 io.CopyBuffer 包一层自定义 buffer。
如何正确打开目标文件:追加 vs 覆盖
日志合并最常踩的坑,就是打开模式写错。行为完全取决于你传给 os.OpenFile 的 flag 组合:
- 想追加到已有日志文件末尾(如轮转日志归档)→ 用
os.O_CREATE | os.O_WRONLY | os.O_APPEND,一定不要加os.O_TRUNC - 想覆盖重做(如重建完整日志副本)→ 用
os.Create(dstPath),或等价的os.OpenFile(dstPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644) - 想确保不误覆盖已有文件 → 加
os.O_EXCL,仅首次创建有效,适合一次性任务校验
特别注意:os.O_APPEND 在 Windows 下会忽略后续 Seek,但某些 syscall 边界行为仍不稳定,所以别手动 Seek,一律交给 io.Copy 处理写入位置。
文件名排序必须是自然序,不是字典序
filepath.Glob("app_*.bin") 返回的顺序是字典序:app_1.bin、app_10.bin、app_2.bin —— 这会导致日志时间线彻底错乱。这不是 Go 的 bug,是字符串排序的通用限制。
解决方法只有两个可靠选项:
- 生成日志时强制零填充命名,例如
fmt.Sprintf("app_%08d.bin", t.Unix())→app_1718208000.bin - 读取前显式排序:用
sort.Slice(files, func(i, j int) bool { return extractNum(files[i]) ,其中 <code>extractNum从文件名中提取数字并转int;别依赖os.ReadDir返回顺序,ext4 和 NTFS 都不保证
如果日志前缀复杂(如 prod-api-20240612-001.bin),就用正则 regexp.MustCompile(\`\d+\`) 提数字,别硬切字符串。
合并后必须校验 sha256.Sum256,不能只比大小
os.Stat().Size() 一致 ≠ 内容正确。I/O 中断、io.Copy 返回值未检查、末尾字节静默截断,都会导致日志损坏但大小不变——PDF 打不开、ZIP 解压报 CRC 错、视频卡顿,往往就源于此。
实操建议:
- 分割前:用
sha256.New()+io.Copy流式计算原始哈希,保存为log.sha256 - 合并后:对新文件做同样哈希计算,对比两个
[32]byte是否相等 - 校验失败必须立刻删掉目标文件和所有参与合并的源分片,留着脏数据比报错更危险
别用 md5,碰撞风险高;也别跳过校验——“写对了却没校验”才是真麻烦。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











