c.multipartform比c.formfile更适合多文件上传,因后者仅返回同名字段首个文件,而前者通过form.file["key"]获取完整文件切片,支持同名多文件提取,且需校验空值、净化文件名、限制类型大小、预检路径,避免直接使用不安全的c.saveuploadedfile。

c.MultipartForm 为什么比 c.FormFile 更适合多文件上传
因为 c.FormFile 只能取单个同名字段的首个文件,而浏览器用 <input type="file" name="files" multiple> 或多个同名 <input type="file" name="photo"> 提交时,实际会生成多个 *multipart.FileHeader,必须靠 c.MultipartForm() 才能拿到完整列表。
常见错误是试图循环调用 c.FormFile("files") —— 第二次起就返回 nil, error,因为 Gin 的 multipart body 只解析一次,且 c.FormFile 不支持同名多值提取。
-
c.MultipartForm()返回*multipart.Form,其中form.File是map[string][]*multipart.FileHeader,天然支持同名多文件 - 它会一次性把所有文件 header 和表单文本字段都加载进内存,适合中小并发、文件数量可控的场景
- 如果只传一个文件却误用
c.MultipartForm,开销略大;但多文件下这是唯一可靠方式
如何安全提取并遍历 files 字段中的全部文件
前端字段名决定后端 key:若 HTML 是 <input type="file" name="attachments" multiple>,后端必须用 form.File["attachments"],不能写死为 "files" 或 "upload[]"(后者是旧式 PHP 风格,Gin 不识别)。
空数组或字段缺失时,form.File["attachments"] 返回 nil 切片,直接 range 会 panic。必须先判空:
form, err := c.MultipartForm()
if err != nil {
c.JSON(400, gin.H{"error": "parse multipart form failed"})
return
}
files := form.File["attachments"]
if len(files) == 0 {
c.JSON(400, gin.H{"error": "no files uploaded"})
return
}
for _, file := range files {
// 处理每个 file
}
保存前必须做的三件校验事
不校验就调 c.SaveUploadedFile 或手动 io.Copy,轻则目录被遍历覆盖,重则执行恶意 payload(比如上传 shell.php 到静态资源路径)。
-
文件名净化:丢弃路径分量,只保留 basename,用
filepath.Base(file.Filename),防止../../etc/passwd类攻击 -
类型与大小限制:检查
file.Size是否超限(如 >50MB),再读前几个字节验证 MIME 类型(file.Header不可靠,需file.Open()后用http.DetectContentType) -
目标路径预检:确保
os.Stat(dstDir)存在且可写,否则os.Create或c.SaveUploadedFile会静默失败或 panic
为什么 c.SaveUploadedFile 不该直接用于生产环境
c.SaveUploadedFile 内部只是封装了 file.Open() + os.Create() + io.Copy,但它跳过了所有前置校验环节,且目标路径完全由你拼接 —— 这意味着你手动写的 "./uploads/" + file.Filename 一旦没过滤,就等于把文件系统权限交给用户。
更关键的是:它无法在保存前做内容扫描(如防病毒、图片尺寸校验)、无法加唯一 hash 命名、无法流式处理(比如边上传边转码)。真实业务中,你应该自己控制 file.Open() 后的流,而不是依赖这个“便利但裸奔”的方法。
真正安全的做法是显式打开、校验、重命名、再保存:
src, err := file.Open()
if err != nil {
c.JSON(500, gin.H{"error": "failed to open file"})
return
}
defer src.Close()
dstName := fmt.Sprintf("%s_%d%s",
strings.TrimSuffix(filepath.Base(file.Filename), filepath.Ext(file.Filename)),
time.Now().UnixNano(),
filepath.Ext(file.Filename),
)
dstPath := filepath.Join("./uploads", dstName)
out, err := os.Create(dstPath)
if err != nil {
c.JSON(500, gin.H{"error": "failed to create file"})
return
}
defer out.Close()
if _, err := io.Copy(out, src); err != nil {
c.JSON(500, gin.H{"error": "save failed"})
return
}
最易被忽略的一点:c.MultipartForm() 解析后,整个 multipart body 已被读完,不能再调 c.PostForm 或重复解析 —— 所有表单字段必须从 form.Value 里取,否则拿不到值。











