必须手动设置range头才能实现断点续传:用http.newrequest创建请求并设req.header.set("range", "bytes=offset-"),配合os.openfile不加o_append、file.seek和file.writeat,校验content-range响应头及file.sync确保数据一致。

http.NewRequest 必须手动设 Range 头,否则永远拿不到 206
Go 标准库不自动处理断点续传,http.Get 无法设置自定义 header,硬套它只会返回全量内容。必须用 http.NewRequest("GET", url, nil) 创建请求,再调 req.Header.Set("Range", "bytes="+strconv.FormatInt(offset, 10)+"-")。
- offset 来自
os.Stat获取的本地文件大小,不是猜的、不是写死的 -
Range值格式必须严格为bytes=12345-,结尾短横不能少,也不能多空格或多余横杠 - 某些 CDN 或 Nginx 会伪造
Accept-Ranges: bytes响应头,但实际返回 200;只看 header 不够,得靠真实请求验证 - 请求发出去前,建议设
req.Close = true,避免连接复用干扰续传逻辑
os.OpenFile 不能加 os.O_APPEND,否则 Seek 白做
os.O_APPEND 是断点续传最常见陷阱:它会让所有 Write 强制写到文件末尾,完全忽略之前 Seek 的位置。结果就是数据错位、中间留空、甚至覆盖旧内容。
- 正确打开方式是:
os.OpenFile(path, os.O_WRONLY|os.O_CREATE, 0644),不加os.O_APPEND,也不加os.O_TRUNC - 打开后立即调
file.Seek(offset, io.SeekStart),并检查返回值是否等于offset;不等说明文件被其他进程修改过 - 写入优先用
file.WriteAt(data, offset),它不依赖当前文件指针,比Seek + Write更可靠 - 别用
bufio.Writer包裹*os.File,缓冲会导致实际落盘位置和Seek位置不一致
206 和 200 混用时,仅靠状态码不够判断是否完成
有些 CDN 或反向代理对 Range 请求返回 200 而非 206,且 Content-Length 是完整文件大小,容易误判“已下完”。
- 优先检查响应头:
resp.Header.Get("Content-Range")是否非空;有则一定是 206 - 若为空但
resp.StatusCode == 200,需进一步比对:expected := totalSize - offset,再对比resp.ContentLength - 如果
resp.ContentLength == -1(流式传输),只能靠io.Copy后检查实际写入字节数;若小于expected,大概率服务端不支持Range,应放弃续传、重新从头下载 - 遇到
416 Range Not Satisfiable,别盲目重试;先发HEAD请求获取最新Content-Length,再决定是清空重下还是指数退避
file.Sync 不是可选项,尤其在断电或崩溃场景下
内核缓存可能导致最后几 KB 没真正落盘,下次 os.Stat 读出的大小就比实际小,导致续传错位。
- 写入完成后务必调
file.Sync() - 每次重试前也建议调一次,防止上次中断时缓存未刷盘
- 如果文件系统不支持
Sync(如某些网络挂载),至少确保WriteAt返回值与预期一致
真正难的不是发请求,而是让客户端偏移、服务端响应、本地文件状态三者始终对齐。任何一环松动,比如服务端悄悄删了文件、本地文件被手动截断、或者 CDN 缓存失效却没触发 ETag 校验,都会导致续传错位——而这种错位往往要等到解压或运行时才暴露。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











