直接调用 r.parsemultipartform 会崩溃,因为它默认将整个 multipart body 加载进内存或写入临时磁盘;若设 maxmemory=32mb 而上传 200mb 文件,前 32mb 留内存、其余写磁盘,磁盘失败则返回 io.errunexpectedeof。

为什么直接用 r.ParseMultipartForm 会崩
它默认把整个 multipart body 拷贝进内存或临时磁盘,哪怕只传一个 200MB 文件,r.ParseMultipartForm(32 也会先尝试塞进 32MB 内存缓冲区;超了就写磁盘临时文件——但你根本不想等它自动切,更不希望服务被几十个并发上传瞬间拖垮。真正的问题不是“能不能传”,而是“传的时候有没有流控和资源隔离”。
用 r.MultipartReader() + channel 信号量做两级流控
必须跳过 ParseMultipartForm,手动解析 multipart 流,同时用带缓冲 channel 控制并发数:
- 调用
r.MultipartReader()获取multipart.Reader,逐 part 调用NextPart(),对每个part.Header.Get("Content-Disposition")匹配文件字段 - 用
io.CopyBuffer(dst, part.Body, make([]byte, 1 边读边存,避免大 buffer 占用堆 - 务必在
defer中调用part.Close()和mr.Close(),否则 fd 泄露 - 全局声明
var uploadSem = make(chan struct{}, 5),每个 handler 开头uploadSem ,结尾 <code>defer func() {
http.MaxBytesReader 必须套在 r.Body 上,不能只靠前端校验
前端分片再规范,也挡不住恶意构造的超大请求体。绕过所有中间件,在 handler 最开头就做硬限制:
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
maxSize := int64(10r.Body = http.MaxBytesReader(w, r.Body, maxSize)- 之后再调用
r.MultipartReader(),否则MaxBytesReader不生效 - 注意:Nginx 前置时,
client_max_body_size和proxy_read_timeout必须同步调大,否则请求在反代层就被截断
分片合并阶段最容易丢状态和锁死
合并不是“读完所有 chunk 再 cat”,而是异步触发、状态可查、失败可重试:
- 每个分片落地后,用
atomic.AddInt64(&chunkCount, 1)计数,存 Redis 或内存 map:uploadId → {total: 47, uploaded: 12, status: "merging"} - 合并逻辑用单独 goroutine 执行,不阻塞 HTTP 响应;启动前检查
uploaded == total,避免竞态 - 合并时加分布式锁(如 Redis SETNX),防止同一
uploadId被多个请求同时触发 merge - 合并失败要记录 error 并设
status = "failed",不能静默吞掉 —— 否则前端轮询永远卡在 “merged 0/47”
真正的难点不在“怎么传”,而在“传一半断了怎么办”和“两个请求同时 merge 同一个文件怎么防”。流控只是起点,状态持久化和边界保护才是压测时暴雷最多的环节。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










