go中实现http断点续传需服务端支持range请求,客户端手动构造range头、校验206状态码、用writeat并发写入并双重校验文件完整性。

Go 中用 http.Range 请求部分文件内容
断点续传本质是服务端支持 Range 请求,客户端按需拉取未下载完的字节段。Go 的 http.Client 本身不自动处理断点,但你可以手动构造带 Range 头的请求。
关键点在于:先检查本地文件已存在多少字节,再用 fmt.Sprintf("bytes=%d-", offset) 设置请求头。服务端返回状态码必须是 206 Partial Content,否则说明不支持断点(比如 Nginx 默认关掉 range,需确认配置里有 range on;)。
- 用
os.Stat()获取本地文件长度作为offset - 务必检查响应状态码:
resp.StatusCode == http.StatusPartialContent,不是200 - 不要直接复用
http.Get(),得用http.NewRequest()手动加Range头 - 某些 CDN 或反向代理会忽略或清洗
Range头,可先用curl -v -H "Range: bytes=100-" http://x验证
io.Copy 写入时跳过已有数据,避免覆盖
不能用 os.Create() 覆盖文件,得用 os.OpenFile() 以 os.O_WRONLY | os.O_CREATE 模式打开,并用 file.Seek(offset, io.SeekStart) 定位到末尾再写。
常见错误是把 io.Copy(dst, resp.Body) 直接套在新打开的文件上——这会从头写,把前面下好的全冲掉。必须确保写入起点和 Range 请求范围严格对齐。
- 打开文件后立即
file.Seek(offset, 0),0是io.SeekStart的简写 -
io.Copy内部不校验偏移,它只管“从resp.Body读多少,就往file当前位置写多少” - 写完记得
file.Sync(),尤其在断电/崩溃场景下,避免缓存丢失导致后续续传错位
如何安全判断是否需要续传(而不是重下)
不能只看文件是否存在,得结合服务端响应的 Content-Range 和本地文件大小做双重校验。例如本地有 1024 字节,但服务端返回 Content-Range: bytes 512-2047/3072,说明上次中断在 512 处,当前文件已损坏,应删掉重来。
更稳妥的做法是:先发一个 HEAD 请求,读取 Content-Length,再比对本地文件大小。若本地大小 ≥ 总长,说明已完成;若小于且服务端支持 Accept-Ranges: bytes,才走续传流程。
- HEAD 请求用
client.Do(req),req.Method = "HEAD" - 检查响应 Header 是否含
resp.Header.Get("Accept-Ranges") == "bytes" - 若本地大小 > 响应
Content-Length,大概率文件被篡改或写坏,建议清理后重下
并发续传多个分片时要注意文件竞争和校验
单文件分多段并发下载(如把 100MB 分成 4 段)能提速,但必须保证每个 goroutine 写自己负责的区间,且不互相干扰。不能所有 goroutine 都用同一个 *os.File 句柄——Seek + Write 不是原子操作,会写乱。
正确做法是每个分片单独打开文件(os.OpenFile(..., os.O_WRONLY, 0)),用 file.WriteAt(data, offset) 替代 Seek+Write。这样无需加锁,也规避了光标竞争。
-
WriteAt是线程安全的,但要求传入的[]byte是各自 goroutine 独占的,别共用底层数组 - 所有分片写完后,建议用
sha256校验整个文件,而非只校验各段——因为写入顺序不确定,可能某段写失败但没报错 - 临时文件名推荐加
.part后缀,完成后再os.Rename(),防止程序崩溃时留下不完整文件被误用
最易被忽略的是服务端实际行为和文档不符:有些 HTTP 服务声称支持 Range,但遇到边界超出(如 bytes=999999999-)直接返回 200 全量内容,导致客户端误以为续传成功,结果文件重复拼接。务必在每次请求后解析 Content-Range 并验证起始偏移是否匹配预期。











