直接用 multipart.newreader 流式读取可避免内存暴涨,因 parsemultipartform 默认将整个请求体(含大文件)解析进内存或临时磁盘,即使只读取部分字段也会触发多次拷贝与缓冲分配,导致 rss 飙升甚至 oom。

别调用 ParseMultipartForm,直接用 multipart.NewReader 流式读取 —— 这是内存暴涨最常踩的坑。
为什么 ParseMultipartForm 会让 RSS 暴涨甚至 OOM
它默认把整个请求体(含 1GB 文件)先解析进内存或临时磁盘缓冲区:MaxMemory 只控制“内存部分上限”,超出才落盘,但文本字段、边界字符串、头部解析仍会触发多次拷贝和缓冲分配。哪怕你只关心一个 file 字段,其他所有 text 字段也照单全收。
常见错误现象:top 看 RSS 快速飙到 2GB+,pprof 显示 multipart.ReadForm 占用大量 inuse_space,runtime.ReadMemStats().HeapAlloc 在上传后不回落。
- 根本不是框架问题,是默认行为太“贪”
- 即使设了
r.ParseMultipartForm(32 (32MB),恶意请求发个超长 <code>boundary或伪造头,照样在解析前就卡死或崩溃 - 不要在 handler 里做校验 —— 解析 multipart 前就要限流
必须在最外层套 http.MaxBytesReader
这个限制必须出现在任何 multipart 解析逻辑之前,否则无效。它防的是“还没走到 multipart.NewReader 就被撑爆”的情况。
正确写法:
func uploadHandler(w http.ResponseWriter, r *http.Request) {
// 第一步:立刻限流,1GB 是硬顶
r.Body = http.MaxBytesReader(w, r.Body, 1// 第二步:手动提取 boundary(不能靠 r.MultipartReader(),它内部会调 ParseMultipartForm)
contentType := r.Header.Get("Content-Type")
boundary, _ := mime.ParseMediaType(contentType)
mr := multipart.NewReader(r.Body, boundary["boundary"])
// 后续迭代 part...
}
- 错误姿势:
r.ParseMultipartForm(...)或r.MultipartReader()放在MaxBytesReader前面 - 错误姿势:用
io.LimitReader替代 —— 它不识别 multipart 边界,可能切在中间导致解析失败 - 出错时直接
http.Error(w, "too large", http.StatusRequestEntityTooLarge),别继续执行
用 multipart.NewReader 手动迭代并丢弃非文件字段
拿到 multipart.Reader 后,逐个 NextPart(),对非目标字段立即丢弃,绝不留内存引用。
关键检查点:part.Header.Get("Content-Disposition") 必须含 filename=,否则就是纯文本字段,直接 io.Copy(io.Discard, part)。
for {
part, err := mr.NextPart()
if err == io.EOF {
break
}
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
<pre class="brush:php;toolbar:false;">filename := part.FileName() // 内部已解析 Content-Disposition
if filename == "" {
io.Copy(io.Discard, part) // 彻底忽略文本字段
continue
}
// 只对真实文件字段操作
dst, _ := os.CreateTemp("", "upload-*.tmp")
defer os.Remove(dst.Name()) // 确保清理
io.Copy(dst, part) // 流式写入,不走内存缓冲
dst.Close()}
- 别用
part.FormName()判断字段名 —— 它不校验是否为文件,容易误判 - 别用
bytes.Buffer接收内容 —— 它会在堆上不断扩容,GC 不会立刻回收 - 如果只是中转(如直传 S3),用
io.Copy(s3Uploader, part),跳过本地落盘
并发上传时 goroutine 和句柄数必须受控
单个大文件上传不需要开 goroutine;批量上传时,并发失控会导致三重耗尽:文件句柄、内存、TCP 连接。
实操建议:
- 用带缓冲的 channel 控制最大并发数,比如
sem := make(chan struct{}, 10) - 每个上传 goroutine 开始前
sem ,结束后 <code> - 临时文件用
os.CreateTemp("", "upload-*.tmp"),写完立刻Close(),后续处理用新os.Open()句柄 - 避免全局复用
bufio.Reader或bytes.Buffer—— 它们不是线程安全的,且易逃逸到堆
最容易被忽略的一点:流式处理不是“开了 goroutine 就叫流式”。真正的流式 = 全链路无全量加载 + 边界可控 + 上下文感知。从 http.MaxBytesReader 到 io.Copy,每一步都得亲手把关,不能依赖框架自动兜底。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











