断点续传管理端不能只靠 gin.context.request.body 直接读取,因为 http 大文件上传常采用 multipart/form-data 或 content-range 分块方式,而 request.body 是一次性不可 seek 的流,gin 默认不解析 range/content-range 等头字段、不维护上传状态;必须手动校验偏移、持久化进度、用 os.o_append 追加写入,并配合 md5 校验与超时清理确保完整性。

断点续传管理端为什么不能只靠 gin.Context.Request.Body 直接读取?
因为 HTTP 上传请求(尤其是大文件)常以 multipart/form-data 或分块 Content-Range 方式发送,gin.Context.Request.Body 是一次性流,无法随机 seek,也无法在中断后从指定 offset 继续读——Gin 默认不解析断点续传所需的 Range、If-Range、Content-Range 等头字段,也不维护上传状态。
真正需要的是:服务端主动识别续传意图 + 持久化记录已接收字节 + 校验文件唯一性 + 安全地跳过已传部分。
- 必须手动检查
ctx.Request.Header.Get("Range")和ctx.Request.Header.Get("Content-Range")判断是否为续传请求 - 不能依赖
ctx.SaveUploadedFile(),它会覆盖写入,且无偏移控制 - 文件唯一性建议用
md5(filename + filesize)或客户端传来的X-Upload-ID,避免同名覆盖冲突
如何用 Gin 实现支持 PUT + Content-Range 的续传接口?
Gin 本身不拦截或重写 Body,所以得自己包装 http.Request.Body 并配合 os.OpenFile(..., os.O_APPEND) 追加写入。关键逻辑是:先查已有文件大小 → 校验 Content-Range 起始 offset 是否匹配 → 不匹配则返回 416 Range Not Satisfiable。
初始化并管理阿里云SDK Go 客户端,涵盖 sync.Once 单例、goroutine 安全、endpoint 与 region 配置、VPC 端点及调试。
// 示例:处理单个分块上传
func handleResumeUpload(ctx *gin.Context) {
uploadID := ctx.Param("id")
rangeHdr := ctx.Request.Header.Get("Content-Range") // e.g. "bytes 1024-2047/5000"
if rangeHdr == "" {
ctx.AbortWithStatusJSON(400, gin.H{"error": "missing Content-Range"})
return
}
filePath := fmt.Sprintf("./uploads/%s", uploadID)
f, err := os.OpenFile(filePath, os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
ctx.AbortWithStatusJSON(500, gin.H{"error": "open file failed"})
return
}
defer f.Close()
// 获取当前文件长度(即已传字节数)
existSize, _ := f.Stat()
offset := parseRangeOffset(rangeHdr) // 自定义函数,提取 '1024' 这个值
if offset != existSize.Size() {
ctx.AbortWithStatusJSON(416, gin.H{"error": "offset mismatch", "expected": existSize.Size(), "got": offset})
return
}
// 追加写入
_, err = io.Copy(f, ctx.Request.Body)
if err != nil {
ctx.AbortWithStatusJSON(500, gin.H{"error": "write failed"})
return
}
ctx.Status(200)
}
- 务必用
os.O_WRONLY | os.O_APPEND打开文件,而不是os.O_TRUNC -
Content-Range解析要严格,推荐用正则^bytes (\d+)-(\d+)/(\*|\d+)$提取 offset - 生产环境需加锁(如
sync.Map记录uploadID → *sync.Mutex),防止同一 ID 并发写入损坏文件
怎么让前端知道“该从哪续”?用 HEAD 接口暴露已传进度
前端在开始上传前,会先发 HEAD /upload/:id 查询当前状态。Gin 需返回 Content-Length(即已传大小)和自定义头如 X-Uploaded-Bytes,否则前端无法判断是否要续传。
router.HEAD("/upload/:id", func(c *gin.Context) {
id := c.Param("id")
path := "./uploads/" + id
if _, err := os.Stat(path); os.IsNotExist(err) {
c.Status(404)
return
}
s, _ := os.Stat(path)
c.Header("X-Uploaded-Bytes", strconv.FormatInt(s.Size(), 10))
c.Header("Content-Length", strconv.FormatInt(s.Size(), 10))
c.Status(200)
})
- 不要返回
200 OKbody,HEAD响应必须无 body - 如果文件正在被写入,
Stat()可能短暂不准;高并发下建议搭配 Redis 存储进度(GET upload:progress:<id></id>) - 前端拿到
X-Uploaded-Bytes后,应设置headers["Content-Range"] = "bytes " + uploaded + "-"开始续传
为什么上传完成后还要校验文件完整性?
网络丢包、客户端异常中断、并发写入错位都可能导致文件内容损坏。仅靠字节数匹配不能保证正确性。最轻量的方案是在上传完成时由客户端传一个 X-File-MD5 头,服务端计算本地文件 MD5 并比对。
- MD5 计算不能用
io.Copy后再读一遍——大文件重复 IO 开销大;应在写入时同步hash.Hash.Write() - 若使用分块上传(非单次
PUT),需在所有块收齐后合并并校验,不能每块单独校验 - 注意:Gin 的
ctx.Request.Body是io.ReadCloser,一旦读完就关闭,不可重复读;需用io.TeeReader边读边哈希
实际部署时,/upload/:id/complete 接口才是最终落盘和校验点,不是每次 PUT 都触发 MD5 计算。最容易被忽略的是:没做上传超时清理——临时文件可能长期滞留磁盘,得配定时任务或用 time.AfterFunc 延迟清理未完成上传。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










