必须提前调用 ParseMultipartForm,否则 r.MultipartForm 为空、r.FormFile 返回 http.ErrNotMultipart;未设足够内存限制会导致 400 或 “request body too large” 错误。

Go HTTP 上传文件时 ParseMultipartForm 必须提前调用
不调就拿不到 r.MultipartForm,r.FormFile 也会返回 http.ErrNotMultipart —— 这不是 bug,是 Go 的设计约束:必须显式声明要解析 multipart 数据,且只解析一次。
常见错误现象:r.FormFile("file") 返回 nil, http.ErrNotMultipart;或上传大文件时直接 400;或小文件能传、大文件报 http: request body too large。
r.ParseMultipartForm(32 必须在读取任何表单字段或文件前调用,参数是内存缓冲上限(字节),超过此值会写临时文件- 设太小(如
1 )会导致大文件被强制落盘,但没配 <code>MaxMemory以外的临时路径,可能因权限或磁盘满失败 - 设太大(如
100 )可能耗尽服务内存,尤其并发高时;Go 默认 <code>MaxMemory = 32 ,不调用 <code>ParseMultipartForm就沿用这个值 - 如果只上传小图/文本,设
10 足够;含视频或 ZIP 的接口,建议 <code>100 并确保 <code>os.TempDir()可写
用 r.FormFile 拿文件但实际需要 r.MultipartForm.File
r.FormFile 是快捷封装,但它内部调用 r.ParseMultipartForm(用默认 32MB),一旦你之前已手动调过且参数不同,就会 panic 或行为异常。真要灵活控制,得绕过它,直取 r.MultipartForm.File。
使用场景:同一个请求里既要读多个同名文件(如 files 字段传 3 个),又要区分原始文件名、头信息,或者想跳过 FormFile 的隐式解析逻辑。
立即学习“go语言免费学习笔记(深入)”;
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 先确保
r.ParseMultipartForm已调用,再访问r.MultipartForm.File["file"],它返回[]*multipart.FileHeader -
FileHeader有Filename、Size、Header,比FormFile返回的单个multipart.File和*multipart.FileHeader更可控 - 别对同一字段反复调
r.FormFile—— 它内部会重置 reader,第二次调用可能返回空或 error - 若需流式处理(比如边读边校验 hash),用
fileHeader.Open()得到io.ReadCloser,记得defer f.Close()
保存文件时 os.Create 路径没做安全校验,导致目录穿越
用户传 ../../../etc/passwd 当文件名?os.Create 默认不拦截,直接写进系统目录。这不是 Go 的错,是你没过滤。
常见错误现象:上传后文件出现在意料之外的位置;日志显示创建了 /tmp/uploads/../../.ssh/id_rsa;上线后被扫描出任意文件写入漏洞。
- 永远不要直接拼接用户输入的
fileHeader.Filename到路径里 - 用
filepath.Base(fileHeader.Filename)截取纯文件名,丢弃所有路径成分 - 额外校验:
strings.Contains(filename, "..") || strings.HasPrefix(filename, "."),拒绝非法命名 - 保存路径用
filepath.Join(uploadDir, safeName),且确保uploadDir是绝对路径(如/var/www/uploads),避免相对路径歧义
HTTP handler 里没设超时,大文件上传卡死连接
默认 HTTP server 没上传超时,一个 2GB 文件慢速上传,可能占住 goroutine 几十分钟,拖垮整个服务。
性能影响:连接不释放 → net/http 的 idle connection 数暴涨 → 新请求排队甚至被拒绝;goroutine 堆积触发调度压力。
- 不要依赖全局
http.Server.ReadTimeout—— 它从 TLS 握手或第一次读开始计时,对长传不精准 - 更可靠的是在 handler 内部加 context 超时:
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Minute),然后用ctx控制file.Open()后的读取循环 - 配合
http.MaxBytesReader限流:body := http.MaxBytesReader(w, r.Body, 500,防止恶意发超大 body 耗尽内存 - 前端也得配
timeout(如 axios 的timeout: 300000),否则用户点上传后没反馈,以为卡了
文件上传看着简单,真正上线时最常崩在边界:超大文件、畸形文件名、慢速上传、并发压测下的临时目录争抢。这些地方没兜底,光靠 FormFile + Create 早晚出事。

















