秒传必须在上传前完成可靠哈希校验,应使用os.open + io.copy(h, file)流式计算sha256,避免os.readfile或md5.sum导致oom或卡死;需校验io.copy错误、小写比对哈希、存binary(32)原始值、用hmac token绑定防篡改。

秒传不是跳过计算,而是必须在上传前完成可靠哈希校验;用 io.Copy 流式喂给 sha256.New(),否则大文件会卡住或 OOM。
为什么不能用 os.ReadFile 或 md5.Sum 算大文件哈希
这两个操作都会把整个文件加载进内存:os.ReadFile 返回 []byte,1GB 文件直接吃光服务内存;md5.Sum([]byte) 是为小切片设计的,不支持流式更新,且返回的是固定数组 [16]byte,无法复用或增量计算。实际上传中,你根本等不到结果——goroutine 卡在读取上,连接池迅速耗尽。
-
os.Open+io.Copy(h, file)是唯一推荐路径:全程无中间内存分配,io.Copy内部复用 32KB buffer - 别手动
h.Write(buf)分块读——除非你要插日志或进度回调,否则纯属引入 bug 风险 - 前端传来的哈希字符串(如
X-File-Hash)必须和服务端算出的hex.EncodeToString(h.Sum(nil))小写比对,大小写不一致就会误判
HTTP multipart 场景下如何边上传边算哈希
multipart 文件流(*multipart.Part)不可 rewind,你只有一次读取机会。必须在数据流向存储目标的同时,同步喂给哈希器——靠 io.TeeReader 实现“分叉”:
- 构造
teeReader := io.TeeReader(part, hasher),其中hasher是sha256.New() - 后续所有写入(如
io.Copy(dst, teeReader))都会实时更新哈希状态 - 务必确保下游 reader 完整读完,否则
hasher.Sum(nil)结果不完整 - 别用
part.Read()手动循环——容易漏掉最后一段不足 buffer 的数据
服务端查重必须用 BINARY(32) 存原始哈希值
把 sha256 结果存成 hex 字符串(64 字符)再查 MySQL,单次查询要 10ms+,并发一高就拖垮判断链路。真正高效的做法是存原始字节:
- 入库前用
hex.DecodeString(s)转成[]byte,字段类型设为BINARY(32) - 查询时也传字节数组,不要转成字符串再
LIKE或=匹配 - 加唯一索引:
CREATE UNIQUE INDEX idx_hash ON files(hash),避免重复插入 - Redis 更适合做首层缓存:
SET hash_key "1" EX 3600,O(1) 判断,命中即秒传
客户端哈希不能直接信任,必须带 token 绑定校验
如果客户端算完哈希直接发 GET /check?hash=xxx,中间人截获后换一个已存在的哈希,就能让服务端跳过上传——实际存的是恶意内容。
- 服务端不提供裸查接口,改为接收哈希后返回
{exists: true, token: "hmac_sig"} -
token必须用 HMAC 签名生成,绑定客户端 IP、User-Agent、时间戳和哈希值,有效期 ≤60 秒 - 后续上传请求(如分片初始化)必须携带该 token,服务端验证签名 + 时效性 + 是否匹配当前文件哈希
- 哪怕哈希一致,token 过期或签名不匹配,也必须走完整上传流程
最容易被忽略的点:哈希计算过程中的错误被静默吞掉。必须检查 io.Copy 的 err,而不是只看最终字符串是否非空;空文件、磁盘权限变化、网络中断都可能让 n == 0 或 err != nil,此时秒传逻辑必须终止并报错,不能假装成功。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











