合法range请求需严格格式bytes=start-,用head预检accept-ranges和content-length,再以get+bytes=0-1试探206响应;写入时禁用os.o_append,须seek后校验偏移量。

怎么用 http.Client 发起合法的 Range 请求
Go 的 http.Client 本身不感知断点逻辑,但完全支持手动构造 Range 头——关键不是“能不能发”,而是“发得对不对”。服务端只认严格格式的 bytes=start-(末尾必须带短横),写成 bytes=start-end 或 bytes=-1024 都可能被忽略或返回 416 Requested Range Not Satisfiable。
实操建议:
- 先用
os.Stat()获取本地文件当前长度,作为续传起点offset - 构造头:
req.Header.Set("Range", "bytes="+strconv.FormatInt(offset, 10)+"-"),避免字符串拼接漏掉短横 - 务必用
HEAD请求提前获取服务端真实Content-Length和Accept-Ranges响应头——但别轻信Accept-Ranges: bytes,有些 CDN 会伪造 - 更稳妥的做法是发试探请求:
GET+Range: bytes=0-1023,仅当响应码为206且Content-Range匹配(如bytes 0-1023/12345678)才启用续传
为什么不能用 os.O_APPEND 写入断点文件
os.O_APPEND 是断点续传里最隐蔽的坑:它会让 Write() 忽略之前所有 Seek() 调用,强制写到文件末尾。哪怕你 Seek(1024, io.SeekStart) 了,最终数据还是追加在尾巴上,导致中间出现空白或覆盖错位。
正确做法是:
- 用
os.OpenFile(path, os.O_WRONLY|os.O_CREATE, 0644)打开文件(不加os.O_TRUNC) - 立即调用
f.Seek(offset, io.SeekStart),并检查返回值是否等于offset——不等说明文件被其他进程修改过 - 写入前可选
f.Truncate(offset)清除上次中断残留的脏数据(尤其当文件系统支持稀疏文件时) - 别用
bufio.Writer包裹,缓冲区会破坏 Seek 定位精度
如何判断服务端是否真正支持 Range
光看响应头有 Accept-Ranges: bytes 不够。Nginx 默认开启该头,但若没配 enable_sendfile off 或禁用了 range 指令,实际请求仍返回 200 或 500。
验证必须走真实流量:
- 首次下载前,发一个最小范围请求:
HEAD /large.zip得总长,再发GET /large.zip+Range: bytes=0-1 - 只接受
206 Partial Content状态码;若返回200,说明服务端不支持,应清空本地文件重下 - 若返回
416,需结合HEAD返回的Content-Length判断是否范围越界——比如文件实际只有 512 字节,你却请求bytes=1024- - 注意对象存储(如阿里云 OSS)对小文件(Content-Range,但依然支持
Range,此时靠状态码和 body 长度交叉验证
流式写入时怎么防错位和丢数据
断点续传不是“接着写就行”,而是“精准定位+原子写入+校验兜底”。网络中断、服务端重启、并发写入都可能导致 offset 错乱或最后一块丢失。
关键控制点:
- 写入必须用
io.CopyN(dst, src, expectedBytes)或io.ReadFull()配合固定 buffer,防止粘包或提前 EOF 导致写入不全 - 每写完一块,立即调用
f.Sync()(尤其小文件或关键业务),否则断电可能丢失最后几 KB - 并发场景下,单个文件必须由单个 goroutine 顺序写入;多个分片上传则需用
os.O_EXCL防止覆盖同一 chunk 文件 - 应用层建议加轻量校验:客户端上传前对已传部分算 SHA256 分块哈希,服务端收到续传请求时比对上一块哈希,不一致就返回
416并附带正确 offset
最常被忽略的是:服务端未禁用 r.ParseMultipartForm(32 * 1024 * 1024),它会把整个请求体读进内存或临时文件,破坏流式写入能力——必须在 handler 开头调用 r.ParseMultipartForm(0) 显式禁用。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











