分片上传需绕过 Gin 默认 multipart 解析,改用 c.Request.MultipartReader() 流式读取;校验 identifier/chunkIndex/totalChunks;流式落盘至 .part 文件;用文件锁防合并竞态;同步调优 Go/Nginx/ Gin 三层超时与内存限制。

分片上传必须手动解析 multipart,不能用 c.FormFile
Gin 默认的 c.FormFile("file") 会触发完整 multipart 解析,把整个分块加载进内存,单块 50MB、10 并发就可能 OOM。这不是 bug,而是 net/http.Request.ParseMultipartForm 的默认行为。你得绕过它,直接用原生流式读取。
关键动作是:调用 c.Request.MultipartReader() 获取 multipart.Reader,再循环 NextPart() 区分字段和文件流。不要依赖 c.PostForm 或 c.FormValue——它们在大分片场景下不可靠,且无法处理未完全解析的 body。
- 前端必须传
identifier(如文件名+大小的 hash)、chunkIndex(从 0 开始)、totalChunks,后端拒绝缺失任一字段的请求(返回 400) -
chunkIndex和totalChunks必须用strconv.Atoi转整型,别用ParseInt(..., 10, 64)后强转,易 panic - 临时目录(如
/tmp/uploads/{identifier}/)需提前os.MkdirAll(..., 0755),否则并发写时可能创建失败
分片落盘必须用 io.Copy 直接写磁盘,禁用 c.SaveUploadedFile
c.SaveUploadedFile 内部仍调 c.FormFile,等于又走了一遍内存加载路径。它还默认不创建父目录,拼接路径时若含 ../ 会引发路径遍历漏洞。
正确做法是:对每个 part 判断 part.FormName() == "file" 后,用 os.Create(fmt.Sprintf("/tmp/uploads/%s/%d.part", identifier, chunkIndex)) 打开文件,再 io.Copy(dst, part) 流式落盘。注意加 .part 后缀,避免合并逻辑误读未写完的文件。
- 不要用
io.CopyN带限长拷贝——分片大小本就不固定,前端可能最后一块更小 - 写入后立即
dst.Close(),否则文件句柄泄漏,Linux 下最多打开 1024 个文件就卡住 - 若前端带
Content-Range头(如bytes 1000-1999/100000),则改用os.OpenFile(..., os.O_WRONLY|os.O_CREATE)+file.Seek()定位写入,而非覆盖创建
合并逻辑必须用文件锁防竞态,不能靠 if-check + merge
多个分片请求几乎同时到达,都判断“已收齐全部 0..N-1”,然后都执行合并,结果文件被多次 io.Copy 拼接,内容错乱或 panic(比如两个 goroutine 同时 os.Open 同一个分片文件)。
解决方法是「原子标记 + 单点合并」:收到最后一个分片(chunkIndex == totalChunks - 1)时,尝试 os.OpenFile("/tmp/uploads/{identifier}.merged.lock", os.O_CREATE|os.O_EXCL)。成功则执行合并;失败说明别人已在合并,直接 return。
- 合并时用
os.Create(最终路径)+ 循环os.Open("{identifier}_{i}.part")+io.Copy(最终文件, 分片文件) - 合并完成后,删所有
{identifier}_*.part和{identifier}.merged.lock,别留残留 - 不要用全局
sync.Mutex锁整个合并函数——它无法跨进程生效,部署多实例时照样冲突
超时和内存限制必须三层同步调,只改 Gin 不顶用
上传卡在 90% 不是代码问题,是连接被中间层切断了。Gin 的 r.MaxMultipartMemory 只控内存,不控时间;Go http.Server 默认无读取超时;Nginx 更默认 60 秒就断连。
必须同步调整三处:
- Go 层:
http.Server{ReadTimeout: 15 * time.Minute},启动时显式传入 - Gin 层:
r.MaxMultipartMemory = 256 (256MB),设在 <code>gin.Default()之后、路由注册之前 - Nginx 层:
client_max_body_size 1g;+proxy_read_timeout 900;,否则请求根本到不了 Gin
真正容易被忽略的是:一旦开始解析 multipart body,连接就持续占用,此时若超时触发,c.Request.Body 会被关闭,后续任何 Read 都返回 http: request body closed —— 这不是你代码错了,是配置没跟上。

















