gin 默认不支持超大文件分片上传,因c.formfile和c.multipartform会提前读空r.body导致后续分片读取返回eof;必须用application/octet-stream类型、禁用multipart解析、首行调用parsemultipartform(0)、从url获取元信息并精确限流读取分片。

直接说结论:Gin 默认不支持超大文件分片上传,c.FormFile 和 c.MultipartForm 会提前读空 r.Body,导致后续读取分片数据时返回 EOF —— 这不是配置问题,是框架默认行为设计使然。
为什么 ParseMultipartForm(0) 必须放在 handler 最开头
框架内部(包括 Gin 的中间件、第三方 multipart 中间件如 gin-contrib/multiform)一旦触发 ParseMultipartForm,就会调用 r.ParseMultipartForm,进而调用 r.multipartReader,最终把整个 r.Body 读完并丢弃。哪怕你只传 0,也必须在任何可能触发解析的地方之前执行。
-
c.Request.ParseMultipartForm(0)必须写在 handler 第一行,不能晚于任何c.PostForm、c.Query以外的请求体访问操作 - 若用了
gin.Logger()或自定义中间件,确认它们没隐式调用ParseMultipartForm;否则需重写或禁用 - 不要依赖
router.MaxMultipartMemory来“控制”大文件——它只影响 multipart 解析阶段的内存上限,对 raw body 分片无效
Content-Type 必须设为 application/octet-stream
分片上传不是传统表单提交,multipart/form-data 会强制触发框架 multipart 解析流程,绕不开 Body 被消费的问题。客户端必须显式设置请求头:
Content-Type: application/octet-stream
- 服务端不能再用
c.FormFile或c.MultipartForm,否则立刻报错或读不到数据 - 元信息(如
chunk_index、file_md5、upload_id)只能从 URL 查询参数或r.URL.Query().Get("...")获取 - 若前端用
fetch发送 Blob,注意不要用FormData包裹分片二进制流,否则浏览器自动加multipart头
如何安全读取并保存单个分片
分片不是“随便读”,必须精确控制字节数,防止客户端伪造 Content-Length 或恶意拖长连接。
- 先做限流:
c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, 5(限制单片 ≤5MB) - 再按预期长度读取:
io.CopyN(dstFile, c.Request.Body, int64(expectedSize)),比io.Copy更可靠 - 保存时用
os.O_CREATE | os.O_WRONLY | os.O_EXCL打开文件,避免并发重复写同一分片覆盖 - 临时分片路径建议按
upload_id/chunk_index组织,别用用户传的原始文件名作路径,防目录穿越
断点续传状态怎么存才不踩坑
状态存储不是“记个 map 就完事”,真实场景下要考虑并发、重启、清理和一致性。
- 不要用内存 map 存已传分片列表——进程重启就丢,且多实例部署时不同节点状态不共享
- 推荐用轻量级持久化方案:SQLite(单机)、Redis(分布式)、或直接写 JSON 到磁盘(配合
flock防并发写) - 每个分片上传成功后,立即落库/落文件,再返回 200;不要等所有分片收齐再统一记录,否则中断后无法判断哪些已成功
- 合并前务必校验所有分片是否存在 + 文件大小是否匹配预期,防止缺片或损坏片被忽略
最易被忽略的一点:分片合并时,要用 os.OpenFile 以 os.O_CREATE|os.O_WRONLY|os.O_APPEND 模式逐个追加,而不是用 os.Rename 或 mv 拼接 —— 否则顺序错乱或部分写入失败会导致最终文件损坏。











