go 的 http.servecontent 对大文件分片下载不友好,因其默认全量读入内存且不解析 range 头或设置 content-range 响应头,需手动实现 range 解析、206 状态码返回、响应头组装及 seek+copyn 分段读取,并确保 accept-ranges、content-range、content-Length 三者准确自洽。

Go 的 http.ServeContent 为什么对大文件分片下载不友好
它默认把整个文件读进内存做 io.Copy,遇到几十 GB 的文件,直接 OOM;更麻烦的是,它不主动解析 Range 请求头,也不设置 Content-Range 响应头,完全交由上层自己处理分片逻辑。
实际开发中,你得绕开 http.ServeContent,手动实现 Range 解析、状态码返回、响应头组装和分段读取 —— 这才是可控的起点。
- 必须检查请求是否带
Range头,没带就走完整响应流程(200 + 全量内容) -
Content-Length在分片时不能设为文件总大小,而得是当前片段字节数 - 响应状态码必须是
206 Partial Content,不是200 OK,否则浏览器/客户端不会续传 - 别用
os.Open后直接io.Copy,要用os.OpenFile配合file.Seek跳转到起始偏移再读
如何安全解析 HTTP Range 请求头并校验边界
Range 头格式是 bytes=0-1023 或 bytes=500- 或 bytes=-500,但客户端可能乱填:负数起始、超长结束、非法字符、多个 range(multipart 不在本文讨论范围)。
Go 标准库没有现成解析函数,得自己写。核心是提取两个数字,再和文件大小比对,避免越界或 panic。
立即学习“go语言免费学习笔记(深入)”;
- 用
strings.SplitN(header, "=", 2)拆出 range 值,再按-拆两端 - 空开头(如
-500)表示“末尾 N 字节”,需换算成fileSize - 500 - 空结尾(如
500-)表示“从 500 到末尾”,结束偏移就是fileSize - 1 - 最终得到的
start 才合法,否则返回 <code>416 Range Not Satisfiable
用 io.CopyN + file.Seek 实现高效分片读取
大文件不能一次性 io.Copy,也不能用 bufio.Reader 缓存全量——缓存大小难控,且 Seek 后缓冲区失效。最稳的方式是定位 + 定长读取。
io.CopyN 是关键:它能精确拷贝 N 字节,并自动处理底层 Reader 的多次 Read 调用,比手写循环更简洁可靠。
- 先调
file.Seek(start, io.SeekStart)定位到分片起点 - 计算要读的字节数:
end - start + 1 - 用
io.CopyN(w, file, int64(length))写入响应体,w是http.ResponseWriter - 注意:如果文件被并发修改(如日志轮转),
file.Stat().Size()和实际读取时长度可能不一致,建议加锁或用只读快照
chrome / curl 下载中断后不续传?检查这三个响应头
即使 Range 解析和读取都对,客户端仍可能忽略断点续传,常见原因是响应头缺失或错误。
浏览器判断能否续传,只看三个头:Accept-Ranges、Content-Range、Content-Length,缺一不可,且值必须自洽。
-
w.Header().Set("Accept-Ranges", "bytes")必须显式设置,不能省略 -
Content-Range格式必须是bytes 0-1023/10000(注意空格和斜杠),不能写成bytes 0-1023/或漏掉总大小 -
Content-Length必须等于当前片段字节数(即end - start + 1),不是文件总大小 - 别在分片响应里写
Cache-Control: no-cache,某些旧版客户端会因此拒绝续传
最易被忽略的是:当文件大小动态变化(比如实时日志),Content-Range 中的总长度必须反映「本次响应时」的真实大小,而不是初始 stat 结果 —— 否则客户端校验失败就重下。