断点续传需服务端显式支持Range请求并返回206状态码及正确Content-Range头,客户端须用WriteAt精准写入、落盘.resume文件记录进度,并校验分片区间与响应一致性。

服务端必须显式支持 Range 请求,否则客户端再努力也没用
断点续传不是客户端单方面能搞定的事。Go 本身不提供“自动续传”能力,http.Client 也不会帮你解析 Content-Range 或重试 206。核心前提是服务端返回 206 Partial Content 并带正确 Content-Range 头,且响应体字节范围严格匹配请求头。
常见失效场景:
- Nginx 默认开启
range on;,但若配置了gzip on;且未加gzip_vary on;,可能返回200全量响应 - CDN(如 Cloudflare)默认清洗
Range头,需在缓存规则中显式开启 “Pass query string & headers” 并放行Range - 自定义 API handler 忘记设置
w.Header().Set("Accept-Ranges", "bytes"),客户端不会发Range请求
验证方式:用 curl -v -H "Range: bytes=1024-" https://your.api/file.zip,检查响应状态码和 Content-Range 字段是否出现且格式为 bytes 1024-9999/10000。
客户端写入必须用 WriteAt,O_APPEND 是隐藏陷阱
os.O_APPEND 看似“追加”,但它会无视 Seek() 结果,所有 Write() 都落到文件末尾——哪怕你刚 f.Seek(1024*1024, io.SeekStart) 成功,f.Write(data) 还是写到末尾,中间留空洞,文件静默损坏。
安全写法只有两种:
- 打开时不加
O_APPEND,用f.Seek(offset, io.SeekStart)定位后调f.WriteAt(data, offset)—— 它不依赖当前文件偏移,精准写入 - 若用并发分块下载,每 goroutine 负责固定区间,必须用
WriteAt,不能共用一个io.Copy流
务必检查 f.Seek() 返回值是否等于目标 offset,不等说明文件已被截断或外部修改,应中止写入。
断点位置必须落盘,内存里存着就等于没存
进程崩溃、机器断电时,内存中的已写长度直接丢失。只靠 os.Stat().Size() 反推 offset 不可靠——文件可能被其他程序覆盖、截断或误删。
推荐方案:为 example.zip 创建同名 example.zip.resume 文件,内容为纯文本:
offset=12345678 url=https://example.com/file.zip etag="abc123"
关键动作:
- 每次成功写入一批数据后,立即用
ioutil.WriteFile(或os.WriteFile)全量覆盖.resume文件 - 恢复时先读
.resume,比对url和etag是否一致,防止用户换链接但没删旧 resume - 下载完成前不删除
.resume,完成后才os.Remove;否则中断后无法判断是否真完成了
并发下载分片时,校验和写入必须一对一绑定
多 goroutine 下载同一文件不同区间能提速,但容易出错:
- 别用
io.Copy直接写入,它不感知偏移;必须用io.CopyN(dst, src, length)控制实际读取字节数,避免读超导致写入越界 - 每个分片的
start/end区间要预先算好,且不能重叠;最后一片end应取min(start+chunkSize, totalSize),避免浮点误差或整数溢出 - 服务端返回的
Content-Range必须与请求的Range头完全匹配,否则说明该分片数据不可信,应丢弃重试
最易被忽略的是:服务端响应的 Content-Length 必须等于 end - start + 1,否则 io.CopyN 会提前 EOF 或读超——这会导致分片错位,合并后文件损坏。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











