c.formfile只拿第一个文件,因其底层仅返回form.file["field_name"]切片首元素;需用c.multipartform()获取完整map[string][]*fileheader切片来处理多文件。

为什么 c.FormFile 只拿到第一个文件?
因为 c.FormFile 是 Gin 对 multipart.Form.File 的单值封装,底层调用的是 form.File["field_name"] —— 它只返回切片第一个元素。即使前端传了多个同名字段(如 <input type="file" name="files" multiple>),Gin 默认也只取一个。
真正要拿全部,得绕过 c.FormFile,直接操作原始表单:
- 调用
c.MultipartForm()获取完整*multipart.Form - 从
form.File["field_name"]拿到[]*multipart.FileHeader切片 - 逐个用
c.SaveUploadedFile(header, dst)保存
如何处理多个不同字段的文件(如 avatar + cover + docs)?
不同字段名对应不同键,form.File 是 map[string][]*multipart.FileHeader,所以可分别提取:
form, err := c.MultipartForm()
if err != nil {
c.JSON(400, gin.H{"error": "parse form failed: " + err.Error()})
return
}
// 分别取不同字段的文件列表
avatarFiles := form.File["avatar"]
coverFiles := form.File["cover"]
docsFiles := form.File["docs"]
// 注意:每个都是 []*multipart.FileHeader,可能为空
for _, f := range avatarFiles {
c.SaveUploadedFile(f, "./uploads/avatar_"+f.Filename)
}
关键点:
- 字段名必须和前端
name属性完全一致(区分大小写) - 即使某字段只传一个文件,它仍是长度为 1 的切片,不能用
c.FormFile替代 - 如果字段未上传,对应切片为
nil,遍历前建议判空
前端 <input multiple> 和后端接收的对应关系
前端写 <input type="file" name="photos" multiple>,浏览器会把所有选中的文件打包进同一个字段,后端收到的是 form.File["photos"] 切片 —— 不是多个独立字段。
常见误解是以为 multiple 会自动变成多个字段名,其实不会。要上传多个文件到同一逻辑分组,就用一个字段名 + multiple;要按语义分组(头像/封面/附件),就用多个不同字段名,各自加或不加 multiple。
示例 HTML:
后端就能分别处理 avatar(单文件)、photos(多图)、docs(多文档)。
保存时容易忽略的路径与覆盖风险
c.SaveUploadedFile 的第二个参数是**完整目标路径**,不是目录。如果写成 ./uploads/,会报错“is a directory”;如果文件名没做去重或哈希,重复上传同名文件会直接覆盖。
- 务必确保目标目录存在:
os.MkdirAll("./uploads", 0755) - 不要直接拼接用户传的
f.Filename,需过滤路径遍历(如去掉../) - 生产环境建议用唯一 ID 或时间戳重命名:
uuid.New().String() + path.Ext(f.Filename) - 注意磁盘空间和单次请求总大小限制,默认
gin.DefaultWriter无限制,但应通过router.MaxMultipartMemory = 8 设上限
最常被跳过的其实是权限和路径校验 —— 本地跑通不代表线上安全,尤其当文件名来自用户输入时。











