关键在于服务端可靠接收分片、客户端精准定位续传位置及双方状态一致;须绕过 r.ParseMultipartForm 默认调用,避免 os.O_APPEND 和默认 multipart 解析导致的 90% 失败。

Go 实现文件断点续传,关键不在“能不能发请求”,而在于服务端能否可靠接收分片、客户端能否精准定位续传位置、双方状态是否一致。硬套 os.O_APPEND 或依赖 ParseMultipartForm 默认行为,90% 会出错。
服务端接收分片时必须绕过 ParseMultipartForm
标准库默认调用 r.ParseMultipartForm(32 会把整个请求体读进内存或临时文件,破坏流式写入能力,且无法控制分片落盘路径和时机。
- 在 handler 开头立即调用
r.ParseMultipartForm(0)(传 0 表示禁用自动解析),否则r.Body已被消费,后续读不到原始字节流 - 元信息(如
file_id、chunk_index、total_chunks)应从 URL 查询参数或表单字段获取,别依赖r.MultipartForm.Value—— 它可能为空或延迟可用 - 分片数据直接从
r.Body读取,用io.CopyN(dst, r.Body, chunkSize)写入磁盘,路径建议为./uploads/{file_id}/chunk_{index:04d} - 写入前检查目标目录是否存在,用
os.MkdirAll创建;避免并发写同一文件名,加os.O_EXCL防止覆盖
客户端续传前必须校验服务端真实支持 Range
只看响应头 Accept-Ranges: bytes 不够——Nginx 或 CDN 可能伪造该 header,但实际返回 200 OK 或直接 500。
- 首次下载前,先发一个试探请求:
HEAD /file.zip或GET /file.zip带Range: bytes=0-1023 - 仅当响应码为
206 Partial Content且Content-Range匹配(如bytes 0-1023/12345678)才启用续传逻辑 - 若返回
200,说明服务端不支持,应清空本地文件重下;若返回416,需用HEAD获取真实长度再重试 - 每次续传请求的
Range头必须严格为bytes={offset}-,不能多空格、不能写成bytes={offset}-后带多余横杠
写文件必须用 Seek 而非 O_APPEND
os.O_APPEND 是断点续传最大陷阱:它会让 Write() 忽略之前所有 Seek(),强制写到末尾,导致数据错位或空白填充。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
立即学习“go语言免费学习笔记(深入)”;
- 正确打开方式:
os.OpenFile(filename, os.O_WRONLY|os.O_CREATE, 0644)(不加O_TRUNC,不加O_APPEND) - 打开后立刻
f.Seek(offset, io.SeekStart),并检查返回值是否等于offset;不等说明文件已被外部修改 - 写入前可选调用
f.Truncate(expectedSize)确保文件长度足够,防止稀疏文件问题 - 并发写同一文件时,
Seek+Write非原子,必须用sync.Mutex包裹,或改用单 goroutine 顺序写
断点状态必须落地且轻量,不能只存内存
重启或部署更新后,内存状态全丢,客户端将无法判断“该从哪继续”。状态文件比数据库更可靠、更快、更易维护。
- 状态文件命名建议为
{filename}.download.state,内容只有一行纯数字(已下载字节数) - 读状态前先
os.Stat检查文件是否存在;不存在则视为首次下载,offset = 0 - 写状态必须用
os.WriteFile(原子替换),禁止os.Create + Write,否则中断时留下半截状态 - 每次写入前,用
os.Stat校验原文件大小是否 ≥ 当前 offset;不匹配说明文件被手动截断或篡改,应清空状态重来
最常被忽略的一点:服务端合并分片时,不能简单按文件名排序后 cat 拼接。必须校验每个分片的 X-Chunk-Hash(如 SHA256),且合并前确认 len(chunkFiles) == total_chunks —— 缺一片,最终文件就不可用。

















