multipart/form-data 无法直接支持断点续传,因为其为一次性完整提交,不携带分块序号、总大小、校验摘要等元信息,且HTTP无状态,连接中断即失败;必须绕开Gin默认multipart解析,改用流式读取原始Body并配合自定义分片协议(如Upload-Id、Chunk-Index等Header),服务端自行管理分片存储、校验与合并。

为什么 multipart/form-data 上传无法直接支持断点续传
因为 HTTP 协议本身无状态,multipart/form-data 是一次性提交的完整表单数据,服务端收到的 *multipart.FileHeader 对应的是内存或临时磁盘上的一个完整文件副本——它不携带分块序号、总大小、校验摘要等元信息,更没有“上次传到哪”的上下文。Gin 默认的 c.FormFile() 或 c.MultipartForm() 会直接解析整个请求体,一旦连接中断,整个上传就失败,无法恢复。
所以断点续传必须绕开默认 multipart 解析,改用流式读取原始请求体,并配合客户端约定的分块协议(如 TUS、或自定义 header + 分片参数)。
- 客户端需在每次请求中明确传递:
Upload-Id(唯一上传 ID)、Chunk-Index(当前分片序号)、Total-Size(文件总大小)、Chunk-Size(本片字节数) - 服务端不能调用
c.FormFile(),而要用c.Request.Body直接读取原始字节流 - 必须自行管理分片存储路径、合并逻辑和校验时机,Gin 不提供内置支持
如何用 Gin 接收并校验单个分片的 SHA256 值
校验应在分片写入磁盘前完成,避免写入损坏数据。关键是在读取 c.Request.Body 的同时计算哈希,而不是先存临时文件再读取校验——那样多一次 I/O,且无法防止恶意构造的超大分片耗尽磁盘。
// 示例:校验单个分片的 SHA256
func handleChunk(c *gin.Context) {
uploadID := c.GetHeader("Upload-Id")
chunkIndex := c.GetInt("Chunk-Index")
totalSize := c.GetInt64("Total-Size")
hash := sha256.New()
reader := io.TeeReader(c.Request.Body, hash) // 边读边算哈希
chunkData, err := io.ReadAll(reader)
if err != nil {
c.JSON(400, gin.H{"error": "read chunk failed"})
return
}
clientHash := c.GetHeader("Content-SHA256")
if clientHash != hex.EncodeToString(hash.Sum(nil)) {
c.JSON(400, gin.H{"error": "SHA256 mismatch"})
return
}
// 写入分片文件:/uploads/{uploadID}/{chunkIndex}
dir := filepath.Join("uploads", uploadID)
os.MkdirAll(dir, 0755)
ioutil.WriteFile(filepath.Join(dir, strconv.Itoa(chunkIndex)), chunkData, 0644)
}
-
io.TeeReader是关键:它让读取和哈希计算同步进行,内存友好 - 务必校验
Content-SHA256header 是否与服务端计算一致,而非只信Content-Length - 不要用
c.SaveUploadedFile()——它隐式依赖 multipart 解析,与断点续传冲突
合并分片时如何防止重复写入或并发冲突
当多个分片并发上传、最后触发合并时,若多个请求同时执行合并逻辑,可能造成文件损坏或覆盖。不能靠前端串行控制,必须服务端加锁。
立即学习“go语言免费学习笔记(深入)”;
在 Go 中使用 google/wire 实现编译时依赖注入——wire.NewSet、wire.Build、wire.Bind(接口→实现)、wire.Struct、wire.Value、wire.Interface
最轻量的做法是用基于上传 ID 的文件锁(如 flock),但 Go 标准库不跨平台支持;更稳妥的是用内存互斥 + 原子文件标记:
- 为每个
Upload-Id维护一个sync.Map记录是否正在合并(key: uploadID, value: *sync.Mutex) - 合并前先尝试获取对应 mutex,获取失败则返回 409 Conflict,提示客户端稍后重试
- 合并完成后,写入一个
merged.ok空文件作为原子完成标记,后续请求看到该文件即跳过合并 - 合并过程本身要校验所有分片是否存在、大小是否匹配
Total-Size,再按序拼接
注意:合并不是简单 cat 拼接,要严格按 Chunk-Index 排序读取,否则文件内容错乱。
客户端未发送完整分片时,如何安全清理残留数据
网络中断、客户端崩溃、或用户取消上传,都会留下不完整的分片目录。不能长期堆积,但也不能贸然删除——可能只是暂时卡顿。
推荐两级清理策略:
- 上传 ID 对应的目录下,写入一个
upload.meta文件,记录创建时间、最后更新时间、预期总分片数 - 启动一个后台 goroutine,每 5 分钟扫描
uploads/下所有目录,删除满足以下任一条件的目录:
–upload.meta存在且最后更新时间 > 24 小时,且无merged.ok
– 目录下无任何.bin分片文件 - 清理操作本身要加文件级互斥(如用
os.Rename创建临时锁文件),避免多个清理协程冲突
真正难处理的是“正在上传中但客户端彻底失联”的情况——这时只能靠超时机制兜底,而超时阈值(比如 24 小时)需要根据业务场景权衡:太短误删,太长占空间。

















