Go 的 http.Client 默认不支持断点续传,需手动设置 Range 头、处理 206/200/416 状态码、用 os.O_APPEND 追加写入,并避免多 goroutine 写同一文件句柄导致竞态。

Go 的 http.Client 默认不支持断点续传,必须手动加 Range 头
Go 标准库的 http.Get 或 http.DefaultClient.Do 发起请求时,不会自动检查本地文件是否已存在、也不会读取已下载字节长度并设置 Range。断点续传不是开箱即用的功能,得自己拼请求头、处理状态码、追加写入。
常见错误现象:416 Requested Range Not Satisfiable —— 通常是本地文件大小算错了,或者服务端不支持 Range(比如 Nginx 没开 accept_ranges on),但你仍发了 Range: bytes=1024-。
- 先用
os.Stat获取本地文件当前大小,作为Range起始偏移 - 请求头必须显式设置:
req.Header.Set("Range", fmt.Sprintf("bytes=%d-", offset)) - 服务端返回
206 Partial Content才表示支持;如果返回200,说明它忽略Range,此时应放弃续传、重新下载 - 用
os.OpenFile(..., os.O_WRONLY|os.O_APPEND)追加写入,别用O_TRUNC
io.Copy 不能直接用于续传:要跳过响应 body 前 N 字节再写
HTTP 响应体从第一个字节开始就是实际数据,而断点续传时你已经存了前一段,io.Copy 会把整段响应体原样追加,导致重复写入已存在的部分。
正确做法是跳过响应 body 中「本不该再写的头部」,但 Go 的 http.Response.Body 是流式读取的,没法随机 seek。所以得自己控制读写节奏:
立即学习“go语言免费学习笔记(深入)”;
- 用
io.ReadFull或循环Read读取响应 body,每次只取需要的部分 - 更稳妥的是用
io.MultiReader包一层:先丢弃前expectedSkip字节(比如用io.Discard),再接上真正的写入逻辑 - 别依赖
Content-Length做校验——分块传输或压缩响应里它可能不存在或不准;优先用Content-Range头里的bytes 1024-2047/10000解析总长和当前范围
服务端不返回 Accept-Ranges: bytes 时,别硬续传
这个响应头是服务端明确声明支持范围请求的信号。没有它,Range 头大概率被忽略,或者直接返回 416。有些 CDN 或静态托管(如 GitHub Pages)默认禁用 Range。
实操建议:
- 首次请求先发一个带
Range: bytes=0-0的试探请求,看返回状态码和头信息 - 如果返回
206且有Content-Range,说明支持;若返回200或416,就该走全量下载路径 - 注意:某些服务(如部分对象存储)对小文件不返回
Accept-Ranges,但实际支持Range—— 所以不能只看这个头,要结合响应码和Content-Range综合判断
并发下载多个分片时,os.File 写入位置容易错乱
多个 goroutine 同时往同一个 *os.File 写,即使用了 Seek,也极易因竞态导致数据覆盖或空洞。Go 的文件句柄不是线程安全的写入目标。
解决方式很直接:
- 每个分片用独立的临时文件(如
file.part.001),下载完再按顺序合并 - 或者用
sync.Mutex保护WriteAt调用,但要注意:必须用WriteAt(而非Write),否则文件偏移由内部维护,锁也没用 - 更推荐前者——临时文件方案天然隔离,合并时用
io.Copy顺序拼接,出错也容易清理 - 别忘了设
file.Chmod(0644),有些系统默认创建的临时文件权限太严,后续无法读










