必须手动解析Range头并返回206状态:先用http.ParseRange解析c.Request.Header.Get("Range"),校验单段合法范围;再Seek后用io.CopyN精确读取,显式设置Accept-Ranges、Content-Range、Content-Length头,并调用WriteHeader(206)。

直接用 c.File 或裸 io.Copy 无法支持断点续传;必须手动解析 Range 头、计算字节范围、控制读取长度,并返回 206 Partial Content 状态。
怎么手动解析 Range 请求头
Go 标准库提供 http.ParseRange,但 Gin 不自动调用它。你得从 c.Request.Header.Get("Range") 拿原始值,再传给 http.ParseRange。
- 只支持单段范围(
bytes=1024-2047或bytes=1024-),返回[]http.HTTPRange,长度必须为 1,否则应返回416 Range Not Satisfiable -
http.ParseRange对非法格式(如bytes=-1024、bytes=a-b)会返回空切片或错误,需判空处理 - 别直接信任客户端传的
Range值——要校验start >= 0、end < size、start <= end,否则同样返回416
为什么不能用 file.Seek + io.Copy
file.Seek(offset, io.SeekStart) 后直接 io.Copy(w, file) 会把文件剩余全部发出去,完全忽略 Range 的右边界(比如 bytes=1024-2047 要求只发 1024 字节,而非从 1024 到 EOF)。
- 正确做法是:先
Seek,再用io.CopyN(w, file, length),其中length = end - start + 1 -
io.CopyN在读不满时返回io.ErrUnexpectedEOF,这不是错误,而是文件已到尾——此时按实际读取字节数设置Content-Length即可(HTTP 允许少发,但头和体必须一致) - 绝对不要用
bufio.Reader包裹文件句柄,Seek会破坏其内部 offset,导致跳转错位
哪些 Header 必须显式设置
http.ServeContent 会自动设好一堆头,但手写 handler 时全得自己来,漏一个就可能让客户端放弃断点逻辑。
-
w.Header().Set("Accept-Ranges", "bytes")—— 没这行,浏览器下次不会发Range头 -
w.Header().Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", start, end, size))—— 格式必须严格,%d-%d/%d不能多空格或少斜杠 -
w.Header().Set("Content-Length", strconv.FormatInt(length, 10))—— 必须等于实际响应体字节数,否则 Safari/iOS 直接拒载 -
w.WriteHeader(206)—— 必须在写 body 前调用,且不能依赖c.Abort()或中间件覆盖
Gin 中怎么避免文件句柄泄漏和内存失控
流式响应不等于安全;没关文件、没校验路径、没设超时,照样崩。
- 用
os.Open前先os.Stat检查存在性和大小,避免open xxx: no such file或后续Seek失败 -
defer f.Close()必须紧跟在os.Open后,不能包在 if 或 goroutine 里,否则可能永远不执行 - 路径要用
filepath.Clean过滤../,防止目录穿越;生产环境禁用相对路径,强制用白名单根目录 - 大文件传输中若客户端断连,
io.CopyN会返回write: broken pipe,Gin 默认不捕获——建议用context.Context控制超时,并监听c.Writer.CloseNotify()(注意:Go 1.22+ 已弃用,改用c.Request.Context().Done())
最易被忽略的是 Accept-Ranges 头和 Content-Length 与实际字节数的一致性:前者缺了,客户端根本不会发起续传;后者错了,进度条卡死、iOS 下载失败、Chrome 自动重试——问题现象分散,但根因就这两处。


















