Go中实现断点续传需手动用http.NewRequest设置Range头(如"bytes=1024-"),结合os.Stat获取已下载偏移量、os.OpenFile+Seek精确定位写入,服务端须返回206及Content-Range,否则应降级为全量下载。

如何用 http.NewRequest 发起带 Range 请求头的下载
断点续传本质是让服务端只返回文件的一部分,关键在客户端主动带上 Range 请求头。Go 标准库不自动处理这个,必须手动构造 *http.Request。
- 先用
os.Stat检查本地已存在文件,获取当前长度作为start;若文件不存在,start = 0 - 用
http.NewRequest("GET", url, nil)创建请求,再调用req.Header.Set("Range", "bytes="+strconv.FormatInt(start, 10)+"-") - 注意:不要用
http.Get(),它无法设置自定义 header;也不要漏掉req.Close = true,避免连接复用干扰续传逻辑 - 服务端必须支持
Accept-Ranges: bytes,否则会返回 200 而非 206,此时需提前校验响应状态码是否为206 Partial Content
如何安全地追加写入已下载部分
直接 os.OpenFile(..., os.O_APPEND) 不行——它会在文件末尾追加,但 HTTP Range 返回的是从指定偏移开始的连续字节,必须精确写入对应位置。
- 用
os.OpenFile(path, os.O_WRONLY|os.O_CREATE, 0644)打开文件,不加O_APPEND - 调用
file.Seek(start, 0)移动到已下载末尾位置,再io.Copy写入响应体 - 务必检查
Seek返回值,某些文件系统(如 FAT32)可能不支持大偏移量,会报syscall.EINVAL - 写入完成后建议调用
file.Sync(),防止断电或崩溃导致最后几 KB 丢失
如何判断下载完成并处理 200/206 状态混用
有些 CDN 或反向代理对 Range 请求返回 200 而非 206,且 Content-Length 是完整文件大小,容易误判“已下完”。不能只看状态码。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 优先检查响应头:
resp.Header.Get("Content-Range")是否非空,有则一定是 206;为空但resp.StatusCode == 200,需进一步比对 - 计算本次应写入长度:
expected := fileSize - start(若已知总大小),再对比resp.ContentLength;若后者为 -1(流式传输),只能靠读取结束判断 - 更稳妥的方式:用
io.Copy后检查实际写入字节数,若小于expected且resp.ContentLength == -1,大概率是服务端不支持 Range,应放弃续传、重新从头下载
并发下载分块时如何避免文件错位和竞争
多 goroutine 分段下载本身能提速,但写文件时极易错乱——每个 goroutine 都要独立 seek 并写入,且不能依赖全局文件句柄共享。
- 每个分块 goroutine 必须打开自己的
*os.File(os.OpenFile),而非共用一个句柄;否则Seek相互覆盖 - 写入前先
file.Truncate(0)清空临时文件(如part_001.tmp),再Seek(0, 0)写入该段,合并时用io.Copy拼接 - 避免用
os.Rename原地替换主文件——Windows 下会失败;应先写入临时名,再os.Remove原文件 +os.Rename - 合并阶段无需加锁,但最终
os.Chmod和os.Chtimes应放在所有分块写完之后统一执行
最常被忽略的是服务端对 Range 的实现差异:Nginx 默认支持,但 Caddy、某些 Go Web 框架需显式启用 http.ServeContent;没配好就会静默退化成全量响应,而客户端还在按断点逻辑处理,结果文件损坏。调试时先用 curl -v -H "Range: bytes=1000-2000" 手动验证服务端行为。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










