必须先调用r.parsemultipartform,否则r.formfile会panic或返回nil;因go对multipart表单采用懒解析机制,未显式调用则r.multipartform为nil,而r.formfile内部隐式解析仅用32kb默认缓存,不可控且不适用于生产环境。

r.FormFile 会 panic 或返回 nil?根本原因没调 ParseMultipartForm。 这不是 bug,是 Go HTTP 处理 multipart 的硬性前提——不解析,r.MultipartForm 就是 nil,r.FormFile 内部虽会尝试自动解析,但只在 r.MultipartForm == nil 时触发,且固定用 32KB 缓存,生产环境不可控。
为什么 r.FormFile 总返回 http.ErrNotMultipart
常见现象:前端用了 enctype="multipart/form-data",后端却拿不到文件。根本不在前端或网络层,而在服务端逻辑顺序。
- 没调
ParseMultipartForm,或调得太晚(比如在r.FormValue("xxx")之后)——后者会隐式触发一次解析,导致后续ParseMultipartForm报http: multipart handled - 误用
r.ParseForm()替代:它对 multipart 完全无效,还可能污染状态 - 调试时写了
io.ReadAll(r.Body):这会把底层 reader 耗尽,后续所有解析都失败
ParseMultipartForm 的 maxMemory 参数怎么设
这个值决定多少字节以内留在内存、超了写哪,直接影响 OOM 风险和 I/O 压力。
- 设为
0等价于禁用内存缓存,全部落盘,但r.FormFile可能返回临时文件句柄而非内存流,行为不一致 - 设太大(如
500 )——高并发下易 OOM;设太小(如 <code>1 )——频繁磁盘 I/O,拖慢吞吐 - 推荐分档:纯文本/小图用
10 (10MB),含 ZIP/MP4 用 <code>100 (100MB),专业媒体上传可到 <code>500 (500MB),同时确保 <code>os.TempDir()有空间和写权限
多个同名文件怎么取:别再只用 r.FormFile
r.FormFile("files") 只返回第一个,且第二次调用大概率失效——因为内部 reader 已被重置或 EOF。
- 必须先调
r.ParseMultipartForm(maxMemory) - 改用
r.MultipartForm.File["files"],它返回[]*multipart.FileHeader切片 - 每个
FileHeader有Filename、Size、Header,但注意:FileHeader不是数据本身,得调header.Open()拿io.ReadCloser,且必须defer f.Close() - 忘记
Close()会导致“too many open files”,尤其并发上传时极易触发
保存文件前必须做的三件事
客户端传来的 Filename 是完全不可信的输入,直接拼路径 = 开放任意写入。
- 丢弃原始路径:
filepath.Base(header.Filename)提取文件名,再用白名单校验扩展名(如只允许.jpg、.pdf) - 生成服务端唯一名:
uuid.New().String() + "." + ext,避免覆盖和猜测 - 确保目录存在:
os.MkdirAll(uploadDir, 0755),再用os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)创建文件,显式指定权限,不依赖 umask - 写入必须流式:
io.CopyN(dst, src, maxFileSize)防恶意超大文件,写完调dst.Sync()确保落盘,再dst.Close()
真正难的不是“怎么传”,而是“怎么信”:boundary、Content-Type、size、甚至整个请求体,客户端都能伪造。路径净化、MIME 重检(用 github.com/h2non/filetype)、磁盘配额、句柄释放——漏掉任一环,上线即高危。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











