断点续传依赖服务端支持range:必须返回206和content-range;客户端需校验状态码、显式seek定位写入、持久化checkpoint防崩溃丢失。

服务端是否支持 Range 是前提,不是客户端能解决的问题
断点续传本质是 HTTP 协议协作:客户端发 Range 头,服务端必须返回 206 PartialContent 和正确的 Content-Range。如果服务端只返回 200 OK(比如某些 PHP 脚本、自定义 handler 忘记处理 Range),那客户端再怎么设头也没用。上线前务必先用 curl -I -H "Range: bytes=0-999" <url></url> 验证响应头里有没有 Accept-Ranges: bytes 和 206 状态码。
下载时如何安全读取已存在文件的偏移量
不能直接用 os.Stat 后就硬算偏移——文件可能被其他进程写入、或处于写入中途的不一致状态。稳妥做法是:
- 用
os.OpenFile(filepath, os.O_RDWR|os.O_CREATE, 0644)打开,避免只读模式下无法后续追加 - 调用
f.Stat()获取大小,但立刻defer f.Close();不要复用这个 file 句柄做写入,防止并发冲突 - 如果文件为空(
stat.Size() == 0)且存在,建议先f.Truncate(0)清空,避免残留脏数据干扰续传逻辑 - 对临时文件(如
.part后缀)做同样检查,别只看主文件名
发起请求时 Range 头和文件写入模式必须匹配
Range: bytes=12345- 意味着你要从第 12345 字节开始接收数据,但写入本地文件时不能简单用 os.O_APPEND ——它只保证“追加到末尾”,不保证写入位置等于当前文件大小(比如文件被 truncate 过)。正确组合是:
- 请求头设为
req.Header.Set("Range", fmt.Sprintf("bytes=%d-", offset)) - 文件以
os.O_WRONLY | os.O_CREATE打开(不用O_APPEND) - 写入前调用
f.Seek(offset, io.SeekStart)显式定位 - 用
io.Copy(f, resp.Body)或带缓冲的io.CopyBuffer流式写入,避免内存暴涨
漏掉 Seek 是常见坑:表面下载完成,实际数据全叠在文件开头,校验哈希必失败。
响应状态码校验比 Content-Range 解析更关键
很多实现花力气解析 Content-Range: bytes 12345-67890/100000,却忽略最基础的状态码判断。你真正该做的只有三件事:
- 收到
200 OK→ 如果offset > 0,说明服务端根本不支持 Range,应报错退出,而不是继续写(否则会覆盖已有内容) - 收到
206 PartialContent→ 检查响应头中Content-Range是否存在,且起始字节与你请求的offset一致(防中间代理篡改) - 收到
416 Range Not Satisfiable→ 说明文件已被删或大小变更,应清空本地文件重来
别试图自动 fallback 到全量下载——用户明确中断过一次,再默默重下整份文件,体验比失败还糟。
最易被忽略的点:没有持久化记录「本次下载是否已成功完成」。临时文件未重命名为目标文件前崩溃,下次启动仍会按旧偏移续传。务必在 io.Copy 完成后、os.Rename 前,写一个轻量 checkpoint 文件(如 JSON 记录 offset + timestamp),否则断电瞬间就前功尽弃。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











