Gin 的 c.File() 不支持断点续传,因其内部调用 http.ServeFile,不解析 Range 请求头、不返回 206 响应,也不设置 Accept-Ranges: bytes;必须使用 http.ServeContent 配合原生 ResponseWriter 和 io.ReadSeeker 才能正确处理分段下载。

直接用 c.File() 或 c.Data() 做大文件下载,不可能支持断点续传——HTTP Range 请求会被忽略,客户端发了 Range: bytes=1024- 你也收不到,更不会返回 206 Partial Content。必须绕过 Gin 封装,把底层 http.ResponseWriter 和 *http.Request 交还给标准库处理。
为什么 Gin 的 c.File() 不支持断点续传
Gin 的 c.File() 内部调用的是 http.ServeFile,它不解析 Range 头,也不设置 Accept-Ranges: bytes,更不会根据请求范围读取文件片段。它只做一件事:把整个文件按 200 OK 发出去。哪怕你手动加了 Header("Accept-Ranges", "bytes"),c.File() 本身不响应 Range,客户端下次还是会从头拉。
-
c.File()是“全量发送”封装,和断点续传逻辑天然冲突 -
c.Data()同样绕不开状态码和 header 覆盖问题,无法配合http.ServeContent - 所有 Gin 的响应快捷方法(
c.JSON、c.String等)都会提前写 header,导致后续http.ServeContent失败
正确做法:用 http.ServeContent + 原生 ResponseWriter
你需要把 Gin 的上下文“降级”为原生 HTTP handler,让 http.ServeContent 完全控制响应流程。关键点不是“怎么写”,而是“怎么不写错”:
- 先用
c.Writer.ResponseWriter获取原始http.ResponseWriter,注意它可能被 Gin 包装过,但只要没写过 body 就可用 - 必须显式设置
w.Header().Set("Accept-Ranges", "bytes"),否则浏览器根本不会发 Range 请求 -
size参数必须等于文件真实字节长度,推荐用os.Stat().Size(),但要确保文件没被并发修改;若文件可能变动,需加锁或用 inode + size 双校验 -
modtime不能简单用os.Stat().ModTime(),否则文件内容变了但时间戳没更新时,会触发412 Precondition Failed;建议用filepath.Base(filePath)+size构造 ETag,再在serveContent前手动比对 - 调用
http.ServeContent(w, r, filename, modtime, reader),其中reader必须是支持io.ReadSeeker的对象(如*os.File)
自定义 handler 时怎么安全读取 Range 片段
如果不想用 http.ServeContent(比如需要加密传输、动态拼接、或文件不在本地磁盘),就得自己解析 Range 并精确读取。这时候最容易踩的坑是 seek + copy 错配:
- 用
http.ParseRange(r.Header.Get("Range"), size)解析,它返回[]http.HTTPRange,但标准 HTTP 只允许单段,所以必须检查len(ranges) == 1,否则返回416 - 别用
file.Seek(start, io.SeekStart)后跟io.Copy(w, file)——这会把从 start 到 EOF 全部发出去,超出bytes=start-end范围 - 正确姿势:先
file.Seek(start, io.SeekStart),再用io.CopyN(w, file, int64(end-start+1)),第三个参数是确切字节数 -
io.CopyN遇到 EOF 会返回io.ErrUnexpectedEOF,这不是错误,你要捕获它并正常结束响应;此时Content-Length应设为实际读取字节数 - 绝对不要用
bufio.Reader包裹*os.File——Seek会破坏 buffer 内部 offset,导致跳转错位、数据错乱
Gin 中分片上传与断点续传是两件事
很多人混淆“下载断点续传”和“上传断点续传”。Gin 本身不提供上传层的断点能力,它只负责接收每个分片请求。真正的断点续传逻辑在客户端和服务端协同实现:
- 上传侧的“断点续传”靠的是分片 + 文件唯一标识(如 MD5/SHA256)+ 分片状态记录(数据库或 Redis)
- 客户端上传前先 GET
/upload/status?file_id=xxx,服务端返回已成功上传的分片序号列表 - Gin handler 接收单个分片时,只校验
chunk_index、total_chunks、file_id,然后存到临时目录(如/tmp/uploads/{file_id}/{index}.bin) - 合并操作由独立 endpoint 触发,且必须加锁防止并发合并同一文件
- 下载侧的断点续传(本文重点)和上传侧的断点续传,共享同一份文件存储,但协议、流程、错误码完全隔离
最常被忽略的一点:http.ServeContent 要求 size 在整个响应生命周期内恒定。如果你在 handler 里边读边生成文件(比如从数据库流式拼接),就无法用它——必须先落盘、再 stat、再 serve。临时文件清理、并发访问控制、ETag 生成策略,这些都不是可选优化,而是边界条件下的必答题。


















