必须先用 http.head 请求验证服务端是否支持断点续传,检查响应头含 accept-ranges: bytes 且记录 content-length;若返回 200 ok 而非 206 partial content,则不支持 range,应报错退出而非覆盖重下。

如何用 http.Head 验证服务端是否支持断点续传
不验证就发 Range 头,大概率白忙活。很多 PHP、Node.js 自定义 handler 默认不处理 Range,返回 200 OK 而非 206 Partial Content,客户端写入时会覆盖已有数据。
- 必须先发
HEAD请求,检查响应头是否含Accept-Ranges: bytes - 同时记录
Content-Length,用于后续分块或校验总大小 - curl 快速验证命令:
curl -I -H "Range: bytes=0-999" https://example.com/file.zip,看返回是否有206和Accept-Ranges - Go 中用
http.Head即可,别复用同一*http.Client的 Transport 设置(如MaxIdleConnsPerHost过低会影响并发)
Range 头构造与请求状态码判断必须同步做
只设头不判状态,等于把逻辑交给运气。常见错误是收到 200 OK 还继续写入,结果已下载部分被全量覆盖。
- 若本地文件偏移
offset > 0,但响应是200 OK→ 说明服务端不支持Range,应报错退出,而不是清空重下(用户可能明确想续传) - 收到
206 Partial Content后,必须检查响应头Content-Range是否存在,且起始字节等于你请求的offset(防代理篡改或 CDN 缓存污染) - 收到
416 Range Not Satisfiable→ 文件已被删或大小变更,此时应删除本地文件并全量重试,不能忽略 - 构造头时用
req.Header.Set("Range", "bytes="+strconv.FormatInt(offset, 10)+"-"),结尾短横不能丢,也不能写成bytes=offset-end(除非你知道总长且有意切片)
本地文件写入必须用 Seek,O_APPEND 是陷阱
O_APPEND 看似省事,但和 Range 语义冲突:它只保证“追加到当前末尾”,而断点续传要求“精确写入 offset 位置”。文件若被 Truncate 过,O_APPEND 写的位置 ≠ offset。
- 打开文件用
os.OpenFile(path, os.O_WRONLY|os.O_CREATE, 0644),不用O_APPEND - 写入前必须调用
f.Seek(offset, io.SeekStart),否则数据全堆在开头,哈希校验必失败 - 写入后建议调用
f.Sync()(尤其对 SSD 或 NFS),防止内核缓存未刷盘导致崩溃后丢失最后几 KB - 临时文件名推荐带
.part后缀(如file.zip.part),下载完成再os.Rename()替换主文件,避免中途失败留下脏文件
并发分块下载时,Range 区间划分和写入顺序不能错
并发本身不难,难点在多 goroutine 写同一个文件时的定位和合并。直接让每个协程自己 Seek + Write 容易因竞争或系统调用顺序错乱导致数据错位。
- 先用
HEAD拿到Content-Length,按块大小(如 1MB)算出每段[start, end],注意最后一块end应为total-1 - 每个 goroutine 单独打开文件(不是共享句柄),用
os.O_WRONLY | os.O_CREATE打开,Seek(start, io.SeekStart)后写入对应区间 - 不要依赖
io.Copy的原子性——它内部可能多次Read/Write,需配合io.CopyN或手动控制读取长度,确保只写end-start+1字节 - 所有 goroutine 完成后,无需额外合并;只要每段写入位置准确,文件就是完整的
Content-Range 值可能被反向代理截断或改写,尤其在使用 Cloudflare、Nginx 未显式配置 add_header Accept-Ranges bytes; 时。上线前务必用真实 URL + 真实网络环境跑一次完整流程,别只测 localhost。golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











