必须手动构造Range请求头并校验206状态码:先用os.Stat获取本地文件大小作为offset,设置req.Header.Set("Range", "bytes="+strconv.FormatInt(offset,10)+"-"),只接受206响应且Content-Range匹配才续传,否则降级全量下载;写入用file.WriteAt避免错位。

怎么发一个真正有效的 Range 请求
Go 的 http.Client 不会自动加 Range 头,必须手动构造且格式严格——错一个字符就返回 416 Range Not Satisfiable 或被服务端静默忽略。
- 先用
os.Stat()获取本地文件大小,作为起始偏移:offset := fi.Size() - 构造头时用
req.Header.Set("Range", "bytes="+strconv.FormatInt(offset, 10)+"-"),结尾必须是短横-,不能写成bytes=1024-2047(除非你知道总长且只下一段) - 别信响应头里的
Accept-Ranges: bytes——Nginx、CDN 常伪造它;必须发一次试探请求(比如GET带Range: bytes=0-1023),只接受206 Partial Content且Content-Range匹配才启用续传 - 如果服务端返回
200 OK,说明它根本不支持范围请求,得清空本地文件重下
本地文件怎么写才不会错位或覆盖
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位置不一致
如何处理 416 和连接中断重试
416 Range Not Satisfiable 不代表网络问题,而是服务端状态已变:文件被删、ETag 更新、或 CDN 缓存失效。盲目重试原 range 会陷入死循环。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 遇到
416,先发HEAD请求,取最新Content-Length和ETag - 对比本地文件大小:
if localSize != contentLength,说明必须全量重下;否则可能是临时错位,可指数退避重试 - 网络中断(如
net/http: request canceled、i/o timeout)需捕获err并检查resp == nil,此时直接重试当前 range,无需重新HEAD - 每次重试前建议调用
file.Sync(),防止内核缓存未刷盘导致下次os.Stat()读出错误 size
服务端怎么可靠接收分片并避免内存爆炸
Go 标准库默认在首次访问 r.MultipartForm 时调用 r.ParseMultipartForm(32 ,这会把整个请求体读进内存或临时文件——对大文件上传就是灾难。
立即学习“go语言免费学习笔记(深入)”;
- 在 handler 开头立即调用
r.ParseMultipartForm(0)(传0表示禁用自动解析),否则r.Body已被消费,后续读不到原始字节流 - 元信息(
uploadId、chunkIndex)从r.URL.Query().Get("uploadId")或r.FormValue("chunkIndex")获取,别依赖r.MultipartForm.Value - 分片数据直接从
r.Body读,用io.CopyN(dst, r.Body, chunkSize)控制单次写入量,路径按./uploads/{uploadId}/chunk_{index:04d}组织 - 并发写同一分片时,用
os.O_CREATE | os.O_WRONLY | os.O_EXCL打开文件,防止覆盖
Seek() 返回值没校验、Content-Range 没解析、或 ParseMultipartForm(0) 忘了加,都会导致静默损坏,且很难事后发现。

















