文件断点续传核心在于三闭环:服务端流式接收分片(绕过ParseMultipartForm)、客户端精准Seek写入(非os.O_APPEND)、双方严格校验Range/Content-Range;90%失败因硬套默认multipart或误用追加模式。

Go 实现文件断点续传,核心不在“发不发得出请求”,而在于三件事是否闭环:服务端能否流式接收分片且不依赖 ParseMultipartForm、客户端能否精准定位已写偏移并用 Seek 而非 os.O_APPEND 写入、双方对 Range 和 Content-Range 的校验是否严格。90% 的失败源于硬套默认 multipart 解析或误用追加模式。
服务端接收分片必须绕过 ParseMultipartForm
标准库调用 r.ParseMultipartForm(32 会把整个请求体读进内存或临时文件,破坏流式写入能力,且无法控制落盘路径和时机。一旦调用,<code>r.Body 就被消费完,后续读不到原始字节流。
- 在 handler 开头立即调用
r.ParseMultipartForm(0)(传 0 表示禁用自动解析) - 元信息如
file_id、chunk_index必须从 URL 查询参数(如/upload?file_id=abc&chunk_index=3)或表单字段(r.FormValue("chunk_index"))获取,别依赖r.MultipartForm.Value—— 它可能为空或延迟可用 - 分片数据直接从
r.Body读取,用io.CopyN(dst, r.Body, chunkSize)写入磁盘,路径建议为./uploads/{file_id}/chunk_{index:04d} - 写入前检查目录是否存在:
os.MkdirAll("./uploads/"+fileID, 0755);并发写同一分片时加os.O_EXCL防覆盖
客户端下载续传必须用 Seek + 校验 206 响应
只看响应头有 Accept-Ranges: bytes 不够——Nginx 或 CDN 可能伪造该 header,但实际返回 200 OK 或 500。必须发试探请求验证真实支持。
- 首次下载前,先发 HEAD 或 GET 带
Range: bytes=0-1023的请求 - 仅当响应码为
206 Partial Content且Content-Range匹配(如bytes 0-1023/12345678)才启用续传逻辑 - 若返回
200,说明服务端不支持,应清空本地文件重下;若返回416,需用 HEAD 获取真实长度再重试 -
Range头必须严格为bytes={offset}-,不能多空格、不能写成bytes={offset}-后带多余横杠
写文件必须用 os.O_WRONLY | os.O_CREATE + Seek,禁用 os.O_APPEND
os.O_APPEND 是断点续传最大陷阱:它会让 Write() 忽略之前所有 Seek(),强制写到末尾,导致数据错位或空白填充。POSIX 和 Go 运行时共同行为,无法绕过。
立即学习“go语言免费学习笔记(深入)”;
- 正确打开方式:
f, _ := os.OpenFile(filepath, os.O_WRONLY|os.O_CREATE, 0644) - 立即
f.Seek(offset, io.SeekStart),并检查返回值是否等于offset(不等说明文件被其他进程修改) - 写入前建议
f.Truncate(expectedTotalSize)确保文件长度足够,防止稀疏文件问题 - 别用
bufio.Writer包裹,缓冲会导致实际写入位置和Seek位置不一致
断点状态必须持久化且防篡改
断点信息不能只存在内存里。最轻量可靠的方式是把当前已下载字节数写进同名的 .download.state 文件(如 foo.zip.download.state),内容就一行纯数字。
- 每次启动先
os.Stat检查状态文件是否存在,不存在则视为首次下载,offset = 0 - 读出 offset 后,必须用
os.Stat校验原文件大小是否匹配该数值——不匹配说明文件被手动修改过,应清空状态重下 - 写状态文件必须用
os.WriteFile(原子替换),别用os.Create + Write,否则中断时留下半截状态 - 不要把状态存进数据库或复杂配置,简单即可靠
最容易被忽略的是:服务端校验必须包含内容哈希(比如每个分片传 X-Chunk-Hash),否则网络丢包或重复上传会导致静默损坏;客户端重试必须用 req.Clone(ctx) + backoff.WithContext,裸 for 循环会丢 context、重放失败、引发重试风暴。


















