ctx.formfile() 只能获取第一个文件,因其底层调用 go 标准库 formfile(),仅返回同名文件中索引为 0 的项;正确方式是用 ctx.multipartform() 获取全部文件,再通过 form.file["files"] 遍历,每个文件需调用 fileheader.open() 读取流并 io.copy 写入,同时校验大小、过滤路径、设置 maxmemory 防 oom。

为什么 ctx.FormFile() 只能拿到第一个文件
因为 ctx.FormFile("files") 是 Echo 的单文件快捷方法,它底层调用的是 req.FormFile(),而 Go 标准库的 FormFile 仅返回第一个匹配 name 的文件 —— 即使 HTML 表单里写了 name="files[]" 或 name="files" 并带 multiple,它也只取索引 0。这不是 Echo 的 bug,是设计如此。
正确获取全部文件要用 ctx.MultipartForm()
必须改用 ctx.MultipartForm() 获取完整的 *multipart.Form,再从 .File 字段中提取所有同名文件。注意:HTML 中 name 不需要写 [](Go 不依赖 PHP 风格的数组命名),直接 name="files" 即可。
常见错误是仍用 ctx.FormFile("files") 循环调用,这毫无意义——它每次返回的都是同一个文件。
Echo框架 5.1.0 版本源码包下载,适合关注 RealIP 行为变化、StartConfig.Listener、NewDefaultFS 和观测性中间件入口的开发团队。
- 前端表单必须设
enctype="multipart/form-data" -
<input type="file" name="files" multiple>—— name 保持普通字符串,别加[] - 后端用
form, err := ctx.MultipartForm(),检查err是否为http.ErrNotMultipart - 取文件列表:
form.File["files"]是[]*multipart.FileHeader类型
逐个保存文件时必须用 fileHeader.Open() + io.Copy()
不能直接对 *multipart.FileHeader 调用 os.Create() 写入 —— 它只是头信息,不包含内容。真正读取文件流要靠 fileHeader.Open() 返回的 io.ReadCloser。
典型坑点:忘记 defer f.Close() 导致句柄泄漏;或用 ioutil.ReadAll() 全部加载进内存,上传大文件时 OOM。
- 每个文件都应独立打开:
f, err := fileHeader.Open() - 立即
defer f.Close()(在循环内,不是循环外) - 用
io.Copy(dst, f)流式写入,避免内存膨胀 - 生成唯一文件名建议用
uuid.NewString()或time.Now().UnixNano(),别依赖fileHeader.Filename
容易被忽略的边界:空文件、重复 name、超大文件限制
用户可能选了多个文件但其中某些为空(size == 0),或者浏览器在 multipart 边界中重复发送同名字段 —— 这些都会让 form.File["files"] 包含无效项。另外,Go 默认对单个请求体大小无限制,但生产环境必须设 echo.MaxMemory(32 防止 DoS。
- 遍历前先检查
len(form.File["files"]) > 0 - 每个
fileHeader都要验证fileHeader.Size > 0 - 用
filepath.Base(fileHeader.Filename)防路径穿越,再过滤非法字符 - 全局设置上传内存上限:
e := echo.New(); e.MaxMemory = 32










