c.FormFile 拿不到文件的根本原因是手动读取了 c.Request.Body,导致 multipart body 被提前消耗,后续无法解析;必须使用 POST + enctype="multipart/form-data",并预先设置 r.MaxMultipartMemory。

为什么 c.FormFile 拿不到文件?常见报错 http: no such file
根本原因不是前端没传,而是 Gin 的 multipart 解析被跳过了。只要你在 c.FormFile("file") 之前手动读过 c.Request.Body(比如调了 c.PostForm、io.ReadAll(c.Request.Body),甚至只是 log 了整个 body),Gin 就再也拿不到文件句柄——因为 multipart body 只能被读一次。
-
c.FormFile内部会自动触发ParseMultipartForm;但手动读 body 会绕过这步,导致后续解析失败 - 必须用
POST+enctype="multipart/form-data";发 JSON 或普通表单绝对不行 - 服务端要提前设好内存上限:
r.MaxMultipartMemory = 32 (32MB),否则超限直接 400,连错误细节都不给 - 字段名大小写敏感:
<input name="avatar">就得用c.FormFile("avatar"),拼错一个字母就返回 nil
怎么安全保存上传的文件?别直接用 header.Filename
原始文件名是攻击入口:它可能带 ../../etc/passwd、空字节、Unicode 零宽字符,或伪装成 shell.php.jpg。Gin 不校验,你必须自己拦住。
- 用
filepath.Base(header.Filename)提取纯文件名,再用strings.ToLower(filepath.Ext(name))拿扩展名 - 只允许白名单后缀:
[]string{".jpg", ".pdf", ".xlsx"},别信header.Header.Get("Content-Type") - 大小限制不能只靠
MaxMultipartMemory:它只管内存缓存,大文件仍会落地临时目录。必须显式检查header.Size,比如if header.Size > 20 - 生成唯一文件名:
uuid.NewString() + ext,路径用filepath.Join(uploadDir, ...),彻底避开遍历和覆盖风险
下载时浏览器不弹窗/乱码/打不开?关键在 Content-Disposition
c.File() 只设了 Content-Type 和 Content-Length,但下载行为由 Content-Disposition 控制。缺它,Chrome 当作网页渲染,Safari 直接拒载中文名。
- 强制下载必须加头:
c.Header("Content-Disposition", "attachment; filename=\""+url.PathEscape(filename)+"\"") - 中文名不用 base64,
url.PathEscape就够用;不编码的话,Chrome 会静默失败 - 路径必须校验:
if !strings.HasPrefix(absPath, safeRoot) { c.AbortWithStatus(403) },防../config.yaml穿透 - 大文件别用
c.File():它全量读入内存。改用c.DataFromReader流式传输,配好Content-Type和Content-Length
用 c.SaveUploadedFile 还是手写 io.Copy?
c.SaveUploadedFile 是封装好的快捷方式,底层就是 Open() + Copy + Close,但它不处理错误回滚——如果磁盘满或权限不足,文件可能写一半就卡住,残留脏数据。
立即学习“go语言免费学习笔记(深入)”;
- 生产环境建议手写:
tmp, err := os.CreateTemp("", "upload-*")→io.Copy→os.Rename,原子性有保障 -
c.SaveUploadedFile适合开发调试或内部小工具,省事但不可靠 - 无论哪种方式,
defer src.Close()必须在Open()后立刻写,且不能放在校验逻辑之后——校验失败也要确保资源释放


















