单/多文件上传与大小限制实现
结论先行
单文件:c.FormFile("file") 拿文件头,c.SaveUploadedFile 存盘;多文件:c.MultipartForm() 拿到 form.File["files"] 后遍历。注意 engine.MaxMultipartMemory(默认 32MB)只是“内存转临时文件的阈值”,不是总大小上限;真正的硬限制要用 http.MaxBytesReader 包住请求体。
关键点
| 主题 | 说明 |
|---|---|
| 取单文件 | c.FormFile("fieldName") → *multipart.FileHeader |
| 存文件 | c.SaveUploadedFile(fh, dst),内部 open+copy |
| 取多文件 | c.MultipartForm().File["files"] → []*FileHeader |
| 内存阈值 | engine.MaxMultipartMemory 默认 32MB,超过的部分落临时文件 |
| 总大小限制 | c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, max),超限时报错(Go 1.19+ 可断言 *http.MaxBytesError) |
| 文件名安全 | 不要信任 file.Filename 直接拼路径:目录穿越/覆盖风险,用服务端生成文件名 + 白名单后缀 |
代码示例
func uploadSingle(c *gin.Context) {
// 硬限制:整个 body ≤ 8MB
c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, 8<<20)
fh, err := c.FormFile("file")
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// 服务端重命名,绝不直接拼接 fh.Filename
dst := filepath.Join("uploads", uuid.NewString()+filepath.Ext(fh.Filename))
if err := c.SaveUploadedFile(fh, dst); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "save failed"})
return
}
c.JSON(http.StatusOK, gin.H{"path": dst})
}
func uploadMany(c *gin.Context) {
c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, 32<<20)
form, err := c.MultipartForm() // 需要先 ParseMultipartForm
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
for _, fh := range form.File["files"] { // 字段名 files,多选
_ = c.SaveUploadedFile(fh, filepath.Join("uploads", fh.Filename))
}
c.JSON(http.StatusOK, gin.H{"count": len(form.File["files"])})
}
进阶注意
- MaxBytesReader 限制的是整个 body:多个文件、多个字段合计超限都会失败,正好防总量攻击。
- 超大文件(GB 级)别走 multipart 全量缓存:直接流式读 c.Request.Body 分段落盘。
- 生产建议把上传目录放独立磁盘/对象存储,并做扩展名 + 内容嗅探白名单校验。
常见追问 / 记忆点
- 追问:MaxMultipartMemory 调大是不是就能传大文件?答:不是——它只决定多少数据驻留内存、多少落临时文件,改它只是调“内存/磁盘”分界,不构成大小限制。
- 追问:为什么不能直接用 fh.Filename 存盘?答:恶意文件名可含 ../ 造成目录穿越,或覆盖已有文件。
- 记忆点:FormFile 单文件 / MultipartForm 多文件 / MaxBytesReader 限总量 / 文件名服务端生成。