io.copy是唯一靠谱的合并方式,因它不加载全文进内存、不缓存、不拼接,只做字节流搬运;需用os.create覆盖目标文件、显式数字排序分片、校验sha256而非仅比大小。

用 io.Copy 流式合并最稳,别碰 os.ReadFile 或手动拼接;顺序错、校验漏、打开模式错——这三处一出问题,合并出来的文件基本就废了。
为什么 io.Copy 是唯一靠谱的合并方式
它不加载全文进内存,不缓存,不拼接,只做字节流搬运。GB 级文件合并卡死、OOM、PDF 打不开、ZIP 解压报 CRC 错,90% 出在用了 os.ReadFile、io.ReadAll 或 bytes.Buffer.Write。
- 目标文件必须用
os.Create(dstPath)—— 自动清空并覆盖,不是os.OpenFile(..., os.O_APPEND),后者首次创建时行为等价于普通写入,但后续追加逻辑混乱 - 每个分片用
os.Open(srcPath)后直接传给io.Copy(dst, src),中间不经过[]byte缓冲 - 缓冲区大小无需手动设:
io.Copy内部默认 32KB,对 NVMe/SSD 已足够;真要调优,用io.CopyBuffer包一层自定义 buffer - 别信
os.ReadDir返回顺序 —— Linux ext4 和 Windows NTFS 都不保证,必须显式排序
文件名排序必须提取数字,不能靠字符串字典序
filepath.Glob("part_*") 返回 part_1、part_10、part_2 是正常现象,但按这个顺序合并,数据必然错位。这不是 Go 的 bug,是所有字符串排序的通病。
- 生成分片时强制零填充:
fmt.Sprintf("part_%03d", i)→part_001、part_002 - 读取前必须排序:用
sort.Slice(files, func(i, j int) bool { return extractNum(files[i]) ,其中 <code>extractNum从文件名中提整数 - 别依赖时间戳或随机字符串排序 —— 它们无法表达逻辑先后
- 若前缀复杂(如
backup_20240501_part_042),先strings.Split或正则提取数字再转int
合并后必须校验 sha256.Sum256,不能只比文件大小
os.Stat().Size 一致 ≠ 内容正确。末尾几个字节丢失、中间某块被截断、I/O 中断未写满 —— 这些都可能导致大小不变但内容损坏,尤其 ZIP、视频、数据库 dump 文件会直接不可用。
- 分割前:用
sha256.New()+io.Copy流式算原始哈希,不加载全文 - 合并后:对新文件做同样哈希计算,对比两个
[32]byte值是否相等 - 别用
md5—— 碰撞风险高;也别跳过校验 —— “写对了却没校验”才是真麻烦 - 校验失败必须立刻删掉目标文件和所有分片,留着脏数据比报错更危险
服务端合并分片时,打开模式和并发控制不能松懈
多个请求并发写同一上传任务的分片,不加约束就会丢数据;临时分片不清理,几天就能占满磁盘。
- 每个上传会话用 UUID 建独立临时目录:
/tmp/upload_abc123/part_001 - 写入分片前用
os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_EXCL),失败说明已存在,避免覆盖 - 合并前再次
os.Stat检查每片是否存在且非零字节,跳过损坏或空文件 - 收到最后一片不等于能立即合并 —— 要确认
sync.Map或 Redis 中记录的已收索引集合长度 == total_chunks,且最大索引 + 1 == 总数
真正容易被忽略的是:合并不是“把一堆文件 cat 起来”,而是“在字节层面严丝合缝地还原”。哪怕一个 os.O_APPEND 写错、一个文件名没排序、一次 io.Copy 返回值没检查,都可能让最终文件静默损坏,而你只在解压或播放时才发现。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











