Go中实现HTTP断点续传需手动设置Range头并校验206响应,用Seek定位写入而非O_APPEND,通过试探请求验证服务端支持,并用单goroutine有序写入防并发冲突。

Go 里怎么用 http.Range 请求文件分片
断点续传本质是 HTTP 协议支持的字节范围请求,Go 的 http.Client 本身不自动处理断点,得手动加 Range 头。关键不是“怎么发请求”,而是“发什么范围”——这取决于本地已下载多少字节。
常见错误是直接写 Range: bytes=0- 或硬编码起始位置,结果服务端返回 200 而非 206,或者范围越界导致 416 错误。
- 先用
os.Stat检查本地文件是否存在、大小是否 > 0;若存在,起始偏移量设为fileInfo.Size() - 构造 header:
req.Header.Set("Range", fmt.Sprintf("bytes=%d-", offset)) - 必须检查响应状态码:只接受
206 Partial Content;200表示服务端不支持范围请求,需清空本地文件重下 - 注意部分 CDN 或 Nginx 默认禁用
Range,需确认服务端配置了Accept-Ranges: bytes
Go 中如何安全追加写入已存在的文件
用 os.OpenFile 打开文件时,os.O_APPEND 是陷阱——它会忽略 offset,强制写到末尾,但断点续传需要从指定位置开始写,不能依赖追加模式。
正确做法是用 os.O_WRONLY | os.O_CREATE 打开,再调用 file.Seek(offset, io.SeekStart) 定位,最后写入。否则会出现数据覆盖错位或空白填充。
- 打开文件必须带
os.O_CREATE,避免文件不存在时报错中断 -
Seek后要检查返回值,某些文件系统(如 NFS)可能不支持随机写 - 写入前建议用
file.Truncate(size)确保文件长度至少为预期值,防止稀疏文件问题 - 别用
bufio.Writer包裹,缓冲会导致实际写入位置和Seek位置不一致
如何判断服务端是否真正支持断点续传
光看响应头有 Accept-Ranges: bytes 不够,有些代理或静态服务器会伪造这个 header,但实际对 Range 请求返回 200 或 500。
最稳妥的方式是发一个试探性请求:取文件开头一小段(比如 bytes=0-1023),检查响应状态码和 Content-Range header 是否匹配。
- 如果返回
206且Content-Range是bytes 0-1023/123456,说明支持 - 如果返回
200,说明不支持,后续请求应放弃Range头 - 如果返回
416,可能是文件太小(Range超出文件长度),也可能是服务端实现有 bug - 注意:某些对象存储(如阿里云 OSS)对小于 1MB 的文件不返回
Content-Range,但依然支持Range
并发下载多个分片时要注意啥
多 goroutine 并发写同一文件很容易出问题:写入位置冲突、Seek 被覆盖、文件句柄竞争。Go 标准库的 *os.File 不是线程安全的写操作目标。
要么每个分片单独写临时文件再合并,要么用单个 goroutine 统一写入,其他 goroutine 只负责下载并 channel 传递数据块。
- 推荐后者:开一个 writer goroutine,接收
struct{ offset int64; data []byte }类型的 channel 消息,按offset排序后写入 - 避免用
sync.Mutex锁整个写过程——性能差,且无法解决Seek+Write的原子性问题 - 如果选分片写临时文件,合并时用
io.Copy而非os.Rename,否则 Windows 下可能失败 - 记得在所有分片完成后再调用
file.Sync(),否则断电可能导致最后几 KB 丢失
断点续传真正的复杂点不在 Go 语法,而在服务端行为不可控、网络中断时机不确定、以及本地文件系统对稀疏写和并发写的隐式假设。每次写入前确认 offset、每次响应后校验状态码、每次下载完做局部 hash 校验——这些动作看着啰嗦,但少一步就可能下出一个损坏文件。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











