应使用 io.MultiWriter 一次读、双路写,避免先保存再读取算 MD5;后者会多一次磁盘 I/O、引发竞态、文件被删改导致失败,且在 NFS/S3 等场景下延迟和错误率显著升高。

直接用 io.Copy 写文件再单独读一遍算 MD5,既慢又浪费 I/O —— 正确做法是用 io.MultiWriter 一次读、双路写:一边落盘,一边喂给 md5.Hash。
为什么不能先保存再读取算 MD5
看似简单,但实际会多一次磁盘读,对大文件(几百 MB 以上)延迟翻倍;若文件刚写完就被其他进程删/改,二次读可能失败或结果不一致;临时文件路径、权限、并发竞争等问题也会叠加进来。
-
os.Open+io.Copy读取刚写入的文件,不是原子操作,中间存在竞态窗口 - 若目标是网络存储(如 NFS、S3 挂载),二次读的 latency 和失败率显著升高
- 本地 SSD 虽快,但 page cache 不一定命中,I/O 成本仍不可忽略
用 io.MultiWriter 同时写文件和算哈希
核心是把文件写入目标和哈希计算器“合并”成一个写入目标,所有字节流经它时自动分发到两边。无需缓冲、不增加内存压力,且天然保持字节一致性。
- 创建
*os.File和md5.New()两个写入器 - 用
io.MultiWriter(file, hasher)包装成单一io.Writer - 把原始数据源(如
http.Request.Body、*multipart.FileHeader的Open()结果)直接io.Copy过去 - 结束后调用
hasher.Sum(nil)获取结果,别忘了file.Close()
示例关键片段:
立即学习“go语言免费学习笔记(深入)”;
file, _ := os.Create("upload.bin")
hasher := md5.New()
mw := io.MultiWriter(file, hasher)
_, err := io.Copy(mw, srcReader) // srcReader 来自 ctx.Request.Body 或 header.Open()
if err != nil {
// 处理错误
}
file.Close()
sum := hasher.Sum(nil)
fmt.Printf("%x\n", sum)
在 Gin 中处理上传文件时同步算 MD5
Gin 的 ctx.FormFile 返回的是 *multipart.FileHeader,它不含内容,必须调用 header.Open() 才能拿到可读流 —— 而且这个流只能读一次。常见错误就是漏掉 Open() 直接传给 io.Copy,结果 MD5 总是空或固定值。
- 务必检查
err:如果header.Open()失败(如权限不足、路径非法),后续一切无意义 -
header.Open()返回的io.ReadCloser必须显式Close(),否则句柄泄漏 - 不要在
io.Copy后再尝试从同一header.Open()读取 —— 第二次会立即返回EOF - 若还需校验文件大小、类型等,应在
io.Copy前完成,或用io.TeeReader边读边检查
容易被忽略的边界情况
MD5 计算本身很稳定,但真正出问题的地方往往在系统层:空文件、符号链接、只读文件系统、磁盘满、NFS 挂载点中断……这些错误都发生在 os.Create 或 io.Copy 阶段,不会抛出 “MD5 错误”,而是直接 err != nil。
- 空文件能正常算出
d41d8cd98f00b204e9800998ecf8427e,无需特殊跳过 - 符号链接默认被跟随;若需校验链接文件本身(而非目标),得先
os.Lstat判断类型 - 写入失败时,
hasher状态已不可信,不能返回部分哈希值 —— 必须整体失败并清理临时文件 -
md5.Hash不是并发安全的,每个上传请求必须新建实例,别复用


















