应跳过 ParseMultipartForm,直接用 multipart.NewReader 流式处理:提取 boundary 后迭代 part,忽略非文件字段,校验 filename 后流式写入;限流须前置,用 http.MaxBytesReader 包裹 r.Body 再解析。

别调用 ParseMultipartForm,直接用 multipart.NewReader
这是最常踩的坑:只要调了 ParseMultipartForm,Go 就会尝试把整个 multipart 请求体(包括大文件)先加载进内存或临时磁盘缓冲区。它默认行为是“尽可能全解析”,哪怕你只关心一个 file 字段,也会为所有文本字段分配缓冲、触发多次内存拷贝。
正确做法是跳过这个封装,手动控制流:
- 从
r.Header.Get("Content-Type")提取 boundary,构造multipart.NewReader(r.Body, boundary) - 用
reader.NextPart()迭代每个 part,遇到非文件字段(比如name="username")就io.Copy(io.Discard, part)忽略 - 对目标文件 part,检查
part.Header.Get("Content-Disposition")是否含filename=,避免误处理纯文本 - 拿到有效 part 后,用
part.Open()得到io.Reader,直接流式写入磁盘或转发
http.MaxBytesReader 必须放在最外层
很多人在 handler 里做校验,但恶意请求可能在解析 multipart 前就耗尽内存——比如发超长 boundary、伪造头部,multipart.NewReader 内部仍会尝试解析并分配缓冲。
所以限流必须早于任何 multipart 操作:
立即学习“go语言免费学习笔记(深入)”;
r.Body = http.MaxBytesReader(w, r.Body, 1 // 例如限制 1GB 总请求体大小- 这个赋值必须在提取 boundary、创建
multipart.NewReader之前执行 - 错误时立即返回
http.StatusRequestEntityTooLarge,不要继续后续逻辑
上传目标选磁盘还是内存?看用途,但别混用
纯中转(如上传到 S3 或代理给后端服务)和需处理(哈希校验、抽帧、转码)是两种完全不同的路径,混用会导致双倍 IO 和内存暂存。
- 中转场景:用
io.Copy(dstWriter, partBody)直接流式转发,dst 是minio.PutObject的io.Reader或http.Post的 body - 需处理场景:用
os.CreateTemp("", "upload-*.tmp")创建临时文件,写入后立刻file.Close(),后续操作用新os.Open句柄打开 - 绝对别用
bytes.Buffer接整个文件——它会在堆上不断扩容,GC 不会立刻回收,OOM 风险极高
需要校验时用 io.TeeReader 边读边算
如果要计算 SHA256 或做其他流式校验,千万别先保存再读一遍——这等于多一次完整 IO 和内存占用。
- 把限流后的
r.Body包一层io.TeeReader(limitReader, hash),hash 是sha256.New() - 然后
io.Copy(dstFile, tee),数据流经 tee 时自动写入 hash - 结束后用
hash.Sum(nil)获取结果,全程零额外内存拷贝 - 注意:若同时需要多个校验(如 SHA256 + MD5),得用
io.MultiWriter组合多个 hash 实例
http.MaxBytesReader 放错行、ParseMultipartForm 残留调用、bytes.Buffer 无意识引入,这些地方一漏,1GB 文件就能让 RSS 瞬间飙到 2GB 以上。


















