Gin 依赖 Go 标准库处理文件上传,必须使用 enctype="multipart/form-data" 且字段名匹配;c.FormFile() 是安全入口,返回 *multipart.FileHeader 需 Open() 后 Copy() 写入,文件名须校验防路径遍历;多文件或混合表单用 c.MultipartForm(),注意默认 32MB 限制。

Gin 本身不提供文件存储或持久化能力,所有文件处理必须基于 Go 标准库 os、io 和 mime/multipart 手动实现——直接调用 c.FormFile() 或 c.MultipartForm() 是唯一安全入口,跳过它会导致解析失败或 panic。
如何正确接收上传的单个文件(c.FormFile())
这是最常用也最容易出错的场景。Gin 封装了 multipart.Form 的获取逻辑,但要求前端必须使用 enctype="multipart/form-data",且字段名要严格匹配。
- 错误现象:
c.FormFile("file")返回nil, err,err是http.ErrMissingFile→ 前端没传、字段名写错、或 Content-Type 不是multipart/form-data - 必须先调用
c.Request.ParseMultipartForm()(Gin 在c.FormFile()内部已自动调用,无需手动) -
c.FormFile()返回的是*multipart.FileHeader,不是文件内容;需用header.Open()得到io.ReadCloser,再用io.Copy()写入目标位置 - 示例关键步骤:
file, err := c.FormFile("upload") if err != nil { c.JSON(400, gin.H{"error": err.Error()}) return } src, _ := file.Open() defer src.Close() dst, _ := os.Create("./uploads/" + file.Filename) defer dst.Close() io.Copy(dst, src)注意:
file.Filename不可信,应做白名单校验或重命名,避免路径遍历(如../../../etc/passwd)如何处理多个同名文件或混合表单字段(
c.MultipartForm())当一个表单里有多个
type="file"字段、或文件与文本字段混传时,c.FormFile()不够用,必须用c.MultipartForm()获取完整结构。-
c.MultipartForm()返回*multipart.Form,其File字段是map[string][]*multipart.FileHeader,键为 HTMLname属性值 - 文本字段值在
form.Value中,例如form.Value["title"][0] - 必须确保请求体大小未超限:默认 Gin 限制为 32MB,可通过
gin.SetMode(gin.ReleaseMode)后调用router.MaxMultipartMemory = 8 调整 - 常见坑:多次调用
c.MultipartForm()会触发重复解析,导致http: invalid Read on closed Body错误
如何安全保存上传文件(避免路径遍历和覆盖)
直接拼接
file.Filename到路径是高危操作,Gin 不做任何过滤。- 永远不要信任
file.Filename:它可被客户端任意篡改,含../、空字节、控制字符等 - 推荐做法:用
filepath.Base(file.Filename)截取纯文件名,再结合 UUID 或时间戳重命名,扩展名从file.Header的ContentType推断(如image/jpeg→.jpg),而非依赖后缀 - 保存前检查目标目录是否存在,用
os.MkdirAll()创建;写入用os.O_CREATE | os.O_WRONLY | os.O_EXCL防止覆盖 - 大文件建议流式写入(
io.CopyN()分块)并设超时,避免阻塞 HTTP 连接
如何返回文件供下载(
c.Header()+c.DataFromReader())Gin 没有类似
c.File()的便捷方法支持流式响应,必须手动设置 Header 并调用c.DataFromReader()。- 关键 Header 必须设全:
Content-Disposition: attachment; filename="xxx"(注意加attachment;才触发下载)、Content-Type(可用mime.TypeByExtension()推断)、Content-Length(若已知) -
c.DataFromReader()第二个参数是size int64,传-1表示未知长度,此时 Gin 会自动用 chunked encoding,但某些代理或浏览器可能表现异常 - 示例:
f, _ := os.Open("./uploads/report.pdf") defer f.Close() stat, _ := f.Stat() c.Header("Content-Type", "application/pdf") c.Header("Content-Disposition", `attachment; filename="report.pdf"`) c.DataFromReader(200, stat.Size(), "application/pdf", f, nil)注意:
c.DataFromReader()不会自动关闭 reader,需自行管理生命周期文件处理真正的复杂点不在 Gin,而在于边界控制:上传大小限制、临时文件清理时机、并发写入冲突、磁盘满错误处理、以及 ContentType 与实际内容不一致时的校验。这些 Gin 都不介入,得自己兜底。
-


















