必须用http.NewRequest手动构造Range头,格式为"bytes=offset-"且结尾短横不可省略;需新建请求对象避免Header残留,配合HEAD试探和206状态码校验服务端真实支持。

怎么用 http.NewRequest 正确发带 Range 的请求
Go 的 http.Get 不支持自定义 Header,断点续传必须用 http.NewRequest 手动构造。核心是:Range 头格式不能错、不能漏、不能多空格。
-
offset := fileInfo.Size()是起始字节,req.Header.Set("Range", fmt.Sprintf("bytes=%d-", offset))——结尾短横-必须存在,写成"bytes=1024"或"bytes=1024-2047"(没确认总长时)都可能触发 416 - 别复用旧的
*http.Request对象:每次续传都要新建http.NewRequest,否则 Header 可能残留旧值 - 某些 CDN 会清洗
Range头,建议先用curl -v -H "Range: bytes=0-1023" URL验证服务端真实行为,而不是只看Accept-Ranges: bytes响应头
为什么不能用 os.O_APPEND 写文件
os.O_APPEND 表面看是“追加”,但实际会忽略所有 file.Seek() 调用,强制写到当前文件末尾。断点续传要求从指定 offset 开始写,一旦用错,轻则数据错位,重则中间留空或覆盖已下好的内容。
- 正确打开方式:
os.OpenFile(path, os.O_WRONLY|os.O_CREATE, 0644),不加os.O_APPEND,也不加os.O_TRUNC - 打开后立刻
file.Seek(offset, io.SeekStart),并检查返回值是否等于offset—— 若不等,说明文件被截断过,需清空重下 - 更稳妥的写法是用
file.WriteAt(data, offset),它不依赖当前文件指针,避免 seek + write 的耦合风险
怎么判断服务端真支持断点续传
光看响应头有 Accept-Ranges: bytes 没用,Nginx 和部分 CDN 会伪造这个 header。真正可靠的方式是发一次试探请求,并校验响应状态和 body 范围。
- 发一个
HEAD请求,读取Content-Length和ETag;再发一个GET请求,带Range: bytes=0-1023 - 只接受
resp.StatusCode == http.StatusPartialContent且resp.Header.Get("Content-Range")匹配bytes 0-1023/xxxx—— 数字必须对得上 - 若返回
200 OK,说明服务端完全忽略Range,后续所有续传逻辑应跳过,直接全量下载 - 若返回
416 Range Not Satisfiable,不是网络问题,而是服务端文件状态已变(如被删、ETag 更新),此时应重新HEAD获取最新Content-Length,再决定是否重下
写完记得 file.Sync(),不然断电就丢进度
Linux/Unix 系统默认使用页缓存,Write 或 WriteAt 后数据可能还在内核缓冲区里。如果进程崩溃或机器断电,os.Stat() 下次读出的文件大小仍是旧值,导致续传从错误位置开始。
立即学习“go语言免费学习笔记(深入)”;
- 每次成功写入一段后,立即调用
file.Sync()—— 它强制刷盘,代价小但关键 - 别在循环里反复
Seek+WriteAt,HTTP 断点续传是单次请求单次响应,不是流式分帧;并发分片下载需额外加锁或用WriteAt隔离区间 - 不要用
bufio.Writer包裹*os.File:缓冲会破坏Seek与实际落盘位置的一致性,这是静默 bug 的高发区


















