Gin 默认不自动解析 multipart 表单,需显式调用 c.FormFile() 或 c.MultipartForm();常见错误是误用 c.PostForm() 或直接读 c.Request.Body;须确保前端 enctype="multipart/form-data" 且 input name 与 c.FormFile("xxx") 一致。

上传文件时 multipart/form-data 解析失败,返回 400 或空文件
Gin 默认不自动解析 multipart 表单,必须显式调用 c.FormFile() 或 c.MultipartForm()。常见错误是直接读 c.Request.Body 或误用 c.PostForm()(它只读文本字段)。
- 确保前端表单
enctype="multipart/form-data"已设置,且<input type="file">的name属性与后端代码中c.FormFile("xxx")的字符串一致 - Gin 中获取文件应使用
c.FormFile("file"),返回*multipart.FileHeader;若字段名不匹配,会返回nil, err,err 为"http: no such file" - 上传大文件前建议先调用
c.Request.ParseMultipartForm(32 (如 32MB),否则默认仅解析前 32MB,超限后 <code>FormFile返回空 - 注意:Gin 的
MaxMultipartMemory是内存缓冲上限(默认 32MB),超出部分会写入临时磁盘,但不影响FormFile可用性
保存图片到本地时路径拼接出错或权限拒绝
Go 的 os.OpenFile 或 ioutil.WriteFile 在 Windows/Linux/macOS 下对路径分隔符、相对路径、目录是否存在都敏感,尤其部署在 Docker 或非开发环境时容易失败。
- 不要硬编码路径如
"uploads/xxx.jpg",改用filepath.Join("uploads", filename),自动适配系统分隔符 - 保存前必须确保目录存在:
os.MkdirAll("uploads", 0755),否则open uploads/xxx.jpg: no such file or directory - 避免使用
os.Getwd()拼路径——容器中可能返回 /,应使用绝对路径或基于二进制所在位置:filepath.Dir(os.Args[0]) - 文件名需清洗:从
header.Filename提取后,用正则过滤掉/、..、控制字符,防止路径遍历(如../../../etc/passwd)
返回图片 URL 时域名和端口没动态适配,外网无法访问
本地调试时用 http://localhost:8080/uploads/xxx.jpg 没问题,但部署到 Nginx 或云服务器后,客户端拿到的仍是 localhost 地址,导致加载失败。
- 不要在代码里写死域名。改用
c.Request.Host+c.Request.URL.Scheme构建基础 URL,例如:scheme := "http"; if c.Request.TLS != nil { scheme = "https" } - 更稳妥的做法是配置项驱动:通过环境变量
BASE_URL设置(如https://img.example.com),启动时读取并注入到 handler 闭包中 - 如果前端走 CDN,后端只返回相对路径
/uploads/xxx.jpg,由反向代理(Nginx)或前端统一补前缀,解耦更干净 - 注意:Gin 的
c.Request.Host可能被代理篡改,若用 Nginx,需确保配置了proxy_set_header Host $host;
并发上传时文件重名覆盖或 os.Rename 失败
直接用原始文件名保存(如 avatar.png)必然冲突;而用 os.Rename 移动临时文件时,在 Windows 下常报 invalid cross-device link。
立即学习“go语言免费学习笔记(深入)”;
- 生成唯一文件名:推荐组合时间戳 + 随机字符串 + 原扩展名,例如
fmt.Sprintf("%d_%s%s", time.Now().UnixNano(), randStr(6), ext) - 不要用
os.Rename跨分区移动——它本质是rename()系统调用,不支持跨设备。改用io.Copy+os.Create安全写入 - 若需原子性,可先写入临时文件(
xxx.tmp),再os.Rename到目标路径(同分区下安全),最后检查目标是否存在以避免竞态 - 考虑加简单锁(如
sync.Mutex)保护共享资源(如计数器、配置缓存),但文件写入本身无需全局锁——每个请求操作独立路径即可


















