Go 标准库通过 http.ParseRange 解析 Range 请求头,返回单段 HTTPRange;http.ServeContent 自动处理断点续传,但需正确设置 modtime、固定 size、显式声明 Accept-Ranges: bytes;自定义 handler 应用 io.CopyN 精确读取指定字节范围,避免 seek 后直接 Copy 或使用 bufio.Reader。

Go 标准库 http.Range 怎么解析断点续传请求头
客户端发起断点续传时,核心是发送 Range: bytes=1024- 或 Range: bytes=1024-2047 头。Go 的 http.ServeContent 会自动识别并处理,但前提是你要正确构造 http.ServeContent 所需的参数——尤其是 modtime 和 size。
常见错误是直接用 os.Stat().ModTime(),但若文件被修改过而 HTTP 缓存未失效,会导致 412 Precondition Failed;另一个坑是传入的 size 不等于文件实际字节长度(比如用 os.File.Stat().Size() 前没 Seek(0, io.SeekEnd) 确认),会导致 416 Range Not Satisfiable。
-
http.ServeContent要求size必须是文件总长度,且不能动态变化 - 必须显式设置
w.Header().Set("Accept-Ranges", "bytes"),否则客户端可能不发Range - 如果用自定义 handler 替代
http.ServeContent,需手动解析r.Header.Get("Range"),再用http.ParseRange解析,它返回[]http.HTTPRange,注意只支持单段(len(ranges) == 1)
用 io.CopyN 还是 io.Copy 控制分块读取
断点续传本质是跳过前 N 字节再读取指定长度。别用 file.Seek(offset, io.SeekStart) 后直接 io.Copy(w, file)——这会把整个剩余文件都发出去,不符合 Range 的右边界限制(如 bytes=1024-2047 只要 1024 字节)。
正确做法是:先 file.Seek(offset, io.SeekStart),再用 io.CopyN(w, file, length),其中 length = end - start + 1。注意 io.CopyN 在读不满时会返回 io.ErrUnexpectedEOF,这不是错误,而是表示文件尾已到,此时应按实际字节数返回(HTTP 允许响应体比 Content-Range 声明的少,只要 Content-Length 匹配即可)。
立即学习“go语言免费学习笔记(深入)”;
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 避免用
bufio.Reader包裹文件——seek 会破坏 buffer 内部 offset,导致跳转错位 - 大文件场景下,
io.CopyN内部默认 32KB 缓冲,够用;若需更高吞吐,可传入自定义make([]byte, 1 的缓冲区给 <code>io.CopyN第三个参数 - 不要在循环里反复
Seek+CopyN分片——HTTP 断点续传是单次请求单次响应,不是长连接分帧
Gin/Echo 框架中怎么透传 http.ServeContent 的行为
Gin 和 Echo 默认不暴露底层 http.ResponseWriter 的全部能力,比如 Gin 的 c.Data 会覆盖状态码和 header,无法配合 http.ServeContent 自动处理 206/416。最稳妥的方式是绕过框架封装,直接操作原始 http.ResponseWriter。
在 Gin 中:用 c.Writer.ResponseWriter(注意是嵌套字段)拿到原生 http.ResponseWriter;在 Echo 中:用 c.Response().Writer。然后调用 http.ServeContent,传入该 response writer、request、modtime、size 和 reader 函数。
- reader 函数必须每次调用都返回一个新的
io.ReadSeeker(例如每次 newos.Open文件,或用io.NewSectionReader(file, 0, size)封装) - 框架中间件(如日志、CORS)可能提前写 header,导致
http.ServeContentpanic:“header already written”,务必确保断点逻辑在中间件链最外层或禁用干扰中间件 - 别用
c.File()(Gin)或c.Attachment()(Echo)——它们内部调用的是http.ServeFile,不支持 Range
并发下载多个 Range 段时为什么文件句柄会耗尽
HTTP/1.1 允许客户端开多个 TCP 连接并发请求不同 Range,但如果每个请求都 os.Open 一次文件,又没及时 Close,Linux 默认的 1024 文件描述符上限很快就会打满,出现 too many open files 错误。
根本解法不是调高 ulimit,而是复用文件句柄:用 sync.Pool 缓存 *os.File,或更推荐——用单个 *os.File 配合 io.NewSectionReader 创建轻量 io.ReadSeeker 实例。后者不持有 fd,只记录 offset/length,真正读取时才通过底层 file 的 ReadAt 调用系统 read。
- 切忌在 handler 里
defer file.Close()——这会让文件在响应结束前一直被占用 - 如果必须多路复用同一文件,用
os.OpenFile(path, os.O_RDONLY, 0)打开一次,全局复用,所有 handler 共享这个*os.File - 注意
*os.File是线程安全的,ReadAt不修改内部 offset,适合并发
断点续传真正的复杂点不在协议解析,而在文件生命周期管理:什么时候打开、是否复用、何时关闭、如何应对客户端中断连接(context.Done() 触发时需主动 close file)。这些细节不写进 handler 主逻辑,很容易在线上压测时突然崩掉。

















