不能直接用md5.Sum读完整文件校验大文件,因其需一次性加载全部字节到内存导致OOM;应使用hash.Hash接口配合io.Copy流式计算,确保内存恒定且校验整个文件字节流一致性。

为什么不能直接用 md5.Sum 读完整文件再校验
大文件(比如 2GB 视频)一次性读入内存会 OOM,md5.Sum 的输入是 []byte,不适合流式处理。真正该用的是 hash.Hash 接口配合 io.Copy 或分块 Write —— 它支持边读边算,内存占用恒定在几 KB。
常见错误是把分片上传的每个 chunk 单独哈希,最后拼接或异或,这完全破坏哈希语义;断点续传必须保证「整个文件字节流」的哈希结果和客户端一致,否则校验无意义。
- 服务端必须按原始文件字节顺序拼接所有 chunk,再计算整体哈希(不是 chunk 哈希的组合)
- 客户端需在上传前预先计算并携带完整文件的
Content-MD5或X-File-Hashheader - 若用
sha256,优先选crypto/sha256.New()而非sha256.Sum256,前者是流式接口
如何用 io.MultiReader 拼接已上传分片并流式哈希
断点续传中,已存的分片通常按序存在磁盘或对象存储(如 /uploads/{id}/part_0001、/uploads/{id}/part_0002)。要校验整体哈希,不能先合并成一个大文件(浪费 I/O 和空间),而应构造一个虚拟的「顺序字节流」。
io.MultiReader 正是为此设计:它接受多个 io.Reader,按顺序串联读取,底层不复制数据,内存零拷贝。
立即学习“go语言免费学习笔记(深入)”;
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
hasher := sha256.New()
parts := []io.Reader{}
for i := 0; i < totalParts; i++ {
f, _ := os.Open(fmt.Sprintf("/uploads/%s/part_%04d", uploadID, i))
parts = append(parts, f)
}
// 注意:MultiReader 返回的 Reader 必须在使用后 close 各个 part 文件
_, _ = io.Copy(hasher, io.MultiReader(parts...))
finalHash := fmt.Sprintf("%x", hasher.Sum(nil))
- 务必在
io.Copy后显式Close()每个打开的文件,否则句柄泄漏 - 如果分片存在对象存储(如 S3),用
GetObject返回的io.ReadCloser替代os.File,同样可塞进MultiReader - 若分片数量极大(>1000),避免一次性全加载到内存,改用惰性 reader 切片或 channel 流式组装
multipart/form-data 分片上传时如何防止哈希被绕过
前端常通过 fetch + FormData 上传分片,但默认不带文件原始哈希。若仅靠服务端对每个分片做独立哈希,攻击者可篡改任意分片内容而不被发现——因为最终校验的是拼接后整体哈希,而非分片级签名。
安全做法是:客户端上传每个分片时,必须携带该分片在原始文件中的偏移(X-Part-Offset)和长度(X-Part-Length),服务端据此校验分片是否写入正确位置,且最终拼接顺序不可乱序。
- 拒绝接收
X-Part-Offset不匹配当前期望位置的请求(防重放/错序) - 对每个分片做局部哈希(如
sha256of raw bytes),存入元数据,用于后续 debug 或审计,但不替代全局哈希 - 若用
net/http解析multipart,注意Request.MultipartReader无法随机访问,必须边解析边写入临时分片文件,再由后续步骤哈希
并发分片上传下如何避免哈希计算竞争
当多个分片并行写入同一上传任务目录时,最终哈希计算可能发生在部分分片尚未落盘完成的时刻。典型现象是 io.MultiReader 读到空文件或截断内容,导致哈希不一致。
关键不是加锁整个哈希流程,而是确保「哈希触发时机」与「分片就绪状态」强一致。推荐用原子标记文件(如 /uploads/{id}/.ready)代替轮询或 sleep。
- 每个分片写完后,执行
os.WriteFile(fmt.Sprintf("/uploads/%s/.part_%04d.done", id, n), nil, 0644) - 哈希前检查所有
.part_*.done文件是否存在,且总数等于totalParts - 不要依赖
os.Stat查文件大小判断写完——NFS 或某些云存储有缓存延迟,.done 文件才是可靠信号
哈希本身是 CPU-bound 操作,单 goroutine 足够;并发做哈希反而因 cache line 竞争降低吞吐,别为了“快”而并发。

















