判断大文件是否已上传一部分的核心是比对客户端与服务端的已上传字节偏移量,即客户端通过head请求携带唯一标识(如upload_id或md5+size组合)查询服务端记录的uploaded_bytes,而非依赖文件名或全量哈希校验。

如何判断大文件是否已上传过一部分
核心是比对客户端与服务端的已上传字节偏移量,而不是靠文件名或哈希全量校验——后者在断点续传场景下既低效又不可靠。
实际做法是让客户端在每次上传前,先发一个 HEAD 请求到服务端的上传地址,携带唯一标识(如 upload_id 或文件 md5 + size 组合),服务端查存储(本地磁盘、对象存储元数据、或专用数据库)返回已接收的字节数 uploaded_bytes。
- 不要用文件名当 key:重名文件、不同用户同名文件会冲突
- 推荐组合 key:
md5(file_content[:1024]) + file_size + user_id,兼顾唯一性与计算开销 - 对象存储(如 S3、MinIO)可用
HeadObject查Content-Length,但注意它返回的是完整文件大小,不是已上传部分——需额外维护一个upload_state表或前缀为uploads/{id}/.state的元数据对象
Go 中怎么安全读取并校验分片上传的偏移位置
HTTP 分块上传时,客户端通常用 Content-Range 头标明当前分片范围(如 bytes 1024-2047/1048576),服务端必须严格校验该范围是否与已记录的 uploaded_bytes 对齐,否则直接拒绝。
Go 标准库不自动解析 Content-Range,需手动提取:
rangeHeader := r.Header.Get("Content-Range")
if rangeHeader == "" {
http.Error(w, "missing Content-Range", http.StatusBadRequest)
return
}
// 解析出 start, end, total
re := regexp.MustCompile(`bytes (\d+)-(\d+)/(\d+)`)
matches := re.FindStringSubmatch([]byte(rangeHeader))
if len(matches) != 4 {
http.Error(w, "invalid Content-Range", http.StatusBadRequest)
return
}
start, _ := strconv.ParseInt(string(matches[1]), 10, 64)
end, _ := strconv.ParseInt(string(matches[2]), 10, 64)
total, _ := strconv.ParseInt(string(matches[3]), 10, 64)
- 必须检查
start == uploaded_bytes,否则是跳传或乱序,不能接受 - 写入文件时用
os.OpenFile(..., os.O_WRONLY|os.O_APPEND)不够——它不保证偏移,要用file.Seek(start, 0)+io.CopyN精确写入 - 并发上传同一文件时,
uploaded_bytes查询和更新必须加锁(如sync.Map或 DB 的SELECT FOR UPDATE),否则可能覆盖已写内容
为什么用 MD5 做分片校验容易出错
MD5 是对整个文件计算的,无法验证单个分片完整性;而断点续传中,每个分片都可能被篡改、截断或网络丢包,必须独立校验。
- 正确做法:客户端上传每个分片时,附带该分片的
X-Part-MD5头,服务端用hash/md5实时计算接收流并比对 - 别把整个文件 MD5 存 DB 后就不管分片——它只用于最终合并后的一次性校验
- 注意 Go 的
io.Copy不校验内容,要用hash.Hash.Write()包裹 reader,且必须在Write返回后检查err,否则校验可能漏失败 - 小分片(如 1MB)用 MD5 没问题;大分片(>10MB)建议换
sha256,防碰撞更稳
合并分片时如何避免磁盘爆满或 IO 阻塞
直接 cat part_1 part_2 > final.file 在 Go 里等价于顺序读所有分片再写一次,对 10GB 文件意味着至少 20GB IO 和内存缓冲风险。
- 用
os.Rename替代复制:如果第一个分片就是从 offset 0 开始的,且存储支持原子重命名(如本地 ext4、XFS),可直接将首个分片 rename 成目标文件,其余分片Seek+Write追加 - 合并前先
os.Stat检查剩余磁盘空间,阈值设为total_size * 1.2(预留日志、碎片空间) - 别用
os.Create再io.Copy:它默认创建新 inode,旧分片删不删都占空间;应os.OpenFile(final, os.O_CREATE|os.O_WRONLY, 0644)后 Seek 到对应位置写 - 合并过程要设 context timeout,防止卡死;完成后立刻
os.Remove所有临时分片,别依赖定时清理
断点续传真正难的不是算法,而是偏移对齐、并发控制、存储一致性这三处细节——任一环节松动,就会出现“看起来续上了,其实中间缺了一段”。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











