核心是防止 r.body 被提前读取:go http 在首次访问 r.multipartform 或调用 r.formvalue 时会隐式执行 parsemultipartform,导致 r.body 为空;必须禁用该自动解析,否则分片上传和断点续传将失败。

Go 微服务里做分片上传和断点续传,核心不是“能不能实现”,而是“别让 r.Body 被提前吃掉”——一旦标准库自动解析 multipart,后续读取就全是空数据,分片直接写失败。
必须禁用 ParseMultipartForm,否则 r.Body 读不到字节
Go HTTP 默认在首次访问 r.MultipartForm 或调用 r.FormValue 时,会隐式执行 r.ParseMultipartForm(32,把整个请求体塞进内存或临时文件。这对分片上传是致命的:body 被消费完,<code>io.Copy 只能读到 EOF。
- 第一行就写
r.ParseMultipartForm(0),传 0 表示禁用自动解析,保留原始流 - 元信息(如
uploadId、chunkIndex)统一从r.URL.Query().Get()或r.FormValue()拿,别碰r.MultipartForm - 如果非要用表单字段传 JSON 元数据,改用
r.MultipartReader()手动流式解析,避免全量加载
分片写入必须用 WriteAt,O_APPEND 是静默损坏陷阱
很多人用 os.O_APPEND 想“追加写”,结果分片错位:本该写在 10MB 偏移的数据,被强制塞到文件末尾,中间全是零填充,文件看似存在但打不开。
- 打开文件用
os.OpenFile(path, os.O_CREATE|os.O_WRONLY, 0644),去掉O_APPEND - 写入前调
f.Seek(offset, io.SeekStart)定位,再用f.WriteAt(data, offset) -
Seek()返回值必须校验是否等于期望offset,否则说明文件可能被截断或移动过 - 并发写同一分片时,用
os.O_EXCL配合os.O_CREATE,靠系统级原子性防覆盖
客户端分片不能依赖 FileReader.readAsArrayBuffer
浏览器里把几 GB 文件全读进内存再切片,不是慢的问题,是直接触发 GC 压力、UI 卡死甚至 OOM。Chrome 在 2GB+ 场景下会主动 kill 页面。
- 用
Blob.slice(start, end)直接截二进制段,返回新Blob,零拷贝、低内存 - 每个分片发独立 POST 请求,URL 带
uploadId和index,例如/upload/chunk?uploadId=abc123&index=5 - 请求 body 直接传
Blob,headers必须设"Content-Type": "application/octet-stream";设成multipart/form-data却没 boundary,服务端直接 400 - 每片附带
X-Chunk-Hash头(如 SHA256 前 8 字节),服务端收到立刻用crypto/sha256.New()+io.Copy实时校验,不落地再算
合并前必须验证三元组唯一性,仅靠 chunkIndex 不够幂等
网络重传会让相同序号多次到达,只按序号判断“是否已上传”,会导致重复写、覆盖或静默跳过真实缺失块。
- 客户端预计算整个文件 SHA256 作为
fileId,每片请求携带fileId、offset、size - 服务端用这三元组生成 Redis key:
upload:<fileid>:<offset>:<size></size></offset></fileid>,而非fileId:chunkIndex - 合并前检查
len(chunkFiles) == totalChunks且所有offset+size连续无重叠,缺一不可 - 未完成的分片超 24 小时自动清理,避免磁盘被占满;状态建议用“追加日志 + 定期快照”,别只存内存 map
真正容易被忽略的是:服务端校验 Seek() 返回值、客户端用 Blob.slice 替代全量读、以及三元组去重逻辑——它们不显眼,但任何一个出问题,都会导致最终文件损坏或上传静默失败。











