Revel 框架需手动调用 c.Request.ParseMultipartForm(32

Revel 框架本身不提供类似 c.FormFile() 那样的多文件快捷方法,c.Params.Get("file") 或 c.Request.FormValue("file") 也只会返回第一个文件——这不是 bug,而是 HTTP 表单解析的默认行为。要真正拿到全部文件,必须绕过高层封装,直接操作原始 multipart.Form 结构。
必须先调用 ParseMultipartForm 并检查 File 字段
Revel 的 c.Request 是标准 *http.Request,但它的 MultipartForm 字段默认为 nil,不自动解析。如果你跳过这步,c.Request.MultipartForm.File 就是空 map,遍历不出任何文件。
- 在控制器方法开头显式调用
c.Request.ParseMultipartForm(32 (例如 32MB 上限) - 检查
c.Request.MultipartForm.File["files"]是否为非空切片(注意 key 名要和前端name="files"一致) - 该字段类型是
map[string][]*multipart.FileHeader,每个值都是[]*multipart.FileHeader - 不要用
c.Params.Get("files")—— 它底层走的是FormValue,只取首项
逐个打开并流式保存,避免内存爆炸
别用 io.ReadAll() 把整个文件读进内存,也别依赖 Revel 未暴露的内部保存逻辑。并发上传时,全量加载多个大文件极易触发 OOM。
- 对每个
*multipart.FileHeader,调用header.Open()得到multipart.Part流 - 用
os.Create("upload/" + header.Filename)创建目标文件句柄 - 用
io.Copy(dst, src)流式写入(底层 buffer 默认 32KB,足够高效) - 务必
defer src.Close()和defer dst.Close(),否则句柄泄漏 - 文件名需做安全处理:过滤路径遍历(如
filepath.Base(header.Filename))
前端 name 必须匹配,且 enctype 不可省略
Revel 不做表单字段重写或智能展开,它完全依赖浏览器提交的原始结构。如果前端写成 name="file" 单数形式,即使选了 10 个文件,后端看到的仍是同一个 key 下的一个切片 —— 这没问题;但如果写成 name="file[]",部分浏览器会扁平化为多个同名键,而 Revel 的 Form 解析并不保证按顺序聚合,反而容易出错。
- 推荐统一用
name="files"(复数),配合multiple属性 -
enctype="multipart/form-data"必须显式声明,否则文件根本不会发送 - 若需带额外字段(如
user_id),它们会出现在c.Request.Form中,和文件分开处理 - 不要试图在 Revel 中复用
c.Params.Get()获取文件列表——它根本不处理 multipart 文件头
BodyLimit 中间件会静默拦截大文件
Revel 默认启用 BodyLimit 中间件(4MB),超过即返回 413,请求甚至不会到达你的路由函数——你连日志都看不到。这对批量上传视频、CAD 图纸等场景是硬伤。
- 启动时显式覆盖:
e.Use(revel.InterceptorFunc(middleware.BodyLimit("100MB"))) - 该中间件必须注册在
e.POST("/upload", ...)之前,顺序错就无效 - 若用自定义
http.Server,还需同步设置ReadTimeout和MaxHeaderBytes - 注意:
BodyLimit是字节上限,不是文件个数限制;单个 99MB 文件 + 一个 2MB 文件仍会触发 413
最容易被忽略的是 ParseMultipartForm 的调用时机和 BodyLimit 的注册顺序——这两处出错,现象都是“收不到文件”,但原因完全不同:前者是代码没跑完就结束了,后者是请求压根没进来。调试时先确认日志里有没有进到控制器第一行,再决定查前端还是查中间件。











