断点续传需严格校验服务端range支持、正确设置range头、精准计算偏移量、避免os.o_append陷阱、实测响应有效性、本地文件完整性校验及写入同步。

Go 本身不提供断点续传的封装逻辑,所有关键行为都得手动控制:Range 头怎么设、文件怎么写、服务端是否真支持——这三个环节任一出错,续传就会静默退化成全量重下或写入错位。
怎么发一个真正有效的 Range 请求
不是加了 Range 头就叫断点续传。服务端可能忽略它,也可能返回 200 OK 而非 206 Partial Content,此时你还在往文件末尾追加,实际却在重复写开头内容。
- 先用
HEAD请求检查响应头:resp.Header.Get("Accept-Ranges")必须是"bytes",但这只是初步信号,不能当真 - 构造
Range值必须严格为bytes=12345-(末尾带短横),不能多空格、不能漏等号、不能写成bytes=12345-12345 - 起始偏移量必须来自
os.Stat(localFile).Size(),而不是硬编码或靠上次记录的变量——文件可能被外部修改或截断 - 一定要校验
resp.StatusCode == http.StatusPartialContent;如果不是,立刻放弃续传逻辑,清空文件走全量下载
为什么不能用 os.O_APPEND 写断点文件
os.O_APPEND 是个常见陷阱:它会让所有 Write() 强制跳到文件末尾,哪怕你之前调用了 file.Seek(),也完全无效。断点续传需要从指定 offset 开始覆盖/填充,不是“追加”。
- 正确打开方式是:
os.OpenFile(path, os.O_WRONLY|os.O_CREATE, 0644) - 紧接着必须调用
file.Seek(offset, io.SeekStart),并检查返回值是否等于offset,不等说明文件长度已被其他进程改过 - 写入前建议先
file.Truncate(expectedTotalSize),避免稀疏文件导致后续读取返回零字节 - 别用
bufio.Writer包裹文件句柄——缓冲会打乱Seek和实际落盘位置的对应关系
如何判断服务端是不是“假装支持”Range
有些 CDN 或 Nginx 配置会伪造 Accept-Ranges: bytes 响应头,但实际对 Range 请求返回 200 或直接 500。光看 header 不靠谱,得实测。
- 发一个试探请求:
GET /file.zip+Range: bytes=0-1023 - 只接受同时满足三个条件的响应:
206状态码 +Content-Rangeheader 存在 + 解析出的总长度(如bytes 0-1023/12345678中的12345678)与HEAD拿到的Content-Length一致 - 若返回
416,不一定失败——可能是文件太小(比如目标只有 500 字节,你却请求0-1023),此时应降级为HEAD获取真实大小后重试 - 阿里云 OSS 等对象存储对小于 1MB 的文件可能不返回
Content-Range,但依然支持Range,这种要单独适配
中断恢复时最易忽略的校验点
用户点击“继续”,程序读取本地文件长度作为 offset,然后发请求——这一步看似自然,但风险极高。文件可能被篡改、写入中途崩溃残留脏数据、甚至被杀毒软件锁定修改时间。
- 仅比对文件长度远远不够;生产环境建议对已下载部分做轻量哈希(如每 1MB 计算一次 SHA256 前 8 字节),和服务端返回的
Content-Range+ 实际响应体校验 - 写入完成后,不要立即信任文件长度;用
file.Sync()强刷磁盘,否则断电可能导致最后几 KB 丢失 - 并发下载同一文件时,
Seek+Write非原子,必须用单 goroutine 串行写,或多进程场景下用文件锁(flock)保护 - 如果服务端返回
Content-Range: bytes 1024-9999/10000,但你本地 offset 是 1023,说明上一次写入少写了 1 字节——这时不该续传,而该回退重下最后一块
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











