正确获取中文文件名需跳过默认解析,手动读取原始content-disposition头并按rfc 5987解码;或前端用encodeuricomponent编码、后端url.pathunescape解码;禁用c.formfile()是关键。

为什么上传中文文件名会乱码
HTTP 协议本身不规定 multipart/form-data 中 filename 的编码方式,各浏览器自行实现:Chrome 用 UTF-8 原样传(但可能被 Go 的 multipart.Reader 解析为 Latin-1)、Firefox 会做 RFC 5987 编码、Safari 则可能截断或丢弃非 ASCII 字符。Gin 的 c.FormFile() 底层调用 http.Request.MultipartReader(),它默认把 filename 字段当作 ISO-8859-1 解码——遇到中文就变成乱码字节,比如 "æµè¯.txt"。
这不是 Gin 的 bug,而是 HTTP 表单规范的历史包袱。你拿到的 *multipart.FileHeader.Filename 已经是错的,后续再转码也晚了。
上传时怎么拿到正确的中文文件名
关键不是“解码”,而是**跳过默认解析,手动读取原始 header 字段**。Gin 不提供直接访问原始 multipart boundary 数据的接口,所以得退回到 c.Request 层:
- 禁用 Gin 自动 multipart 解析:启动时设
r.MaxMultipartMemory = 0,避免 c.FormFile() 提前触发错误解码
- 用
c.Request.MultipartReader() 手动遍历 part,检查 part.Header.Get("Content-Disposition")
- 从该 header 字符串里正则提取
filename\*=(.+?)''(.+)(RFC 5987 格式)或 fallback 的 filename="(.+)"
- 对提取出的值用
url.PathEscape() 反向解码(注意:不是 url.QueryEscape(),它会把 + 当空格处理)
r.MaxMultipartMemory = 0,避免 c.FormFile() 提前触发错误解码c.Request.MultipartReader() 手动遍历 part,检查 part.Header.Get("Content-Disposition")
filename\*=(.+?)''(.+)(RFC 5987 格式)或 fallback 的 filename="(.+)"
url.PathEscape() 反向解码(注意:不是 url.QueryEscape(),它会把 + 当空格处理)示例片段:
reader, _ := c.Request.MultipartReader()
for {
part, err := reader.NextPart()
if err == io.EOF {
break
}
if part.FormName() == "file" {
cd := part.Header.Get("Content-Disposition")
// 匹配 filename*="utf-8''%E6%B5%8B%E8%AF%95.txt"
re := regexp.MustCompile(`filename\*=(?:[^'']*')?([^'']*)''([^"]+)`)
if m := re.FindStringSubmatchIndex([]byte(cd)); m != nil {
enc := string(cd[m[0][1]:m[0][2]])
name, _ := url.PathUnescape(enc) // ← 正确反解
log.Printf("real filename: %s", name) // "测试.txt"
}
}
}
更稳妥的做法:前端不传原始中文名
与其在后端硬解各种浏览器差异,不如让前端主动适配:
- 前端用
encodeURIComponent() 编码文件名,再塞进 FormData:fd.append("filename", encodeURIComponent(file.name))
- 后端收到后用
url.PathUnescape() 解一次即可,稳定且无歧义
- 服务端生成唯一 ID 存文件(如
uuid.New().String()),把原始名存数据库字段,下载时再用 RFC 5987 编码返回——上传和下载解码逻辑分离
encodeURIComponent() 编码文件名,再塞进 FormData:fd.append("filename", encodeURIComponent(file.name))
url.PathUnescape() 解一次即可,稳定且无歧义uuid.New().String()),把原始名存数据库字段,下载时再用 RFC 5987 编码返回——上传和下载解码逻辑分离这样既绕开浏览器 multipart 实现差异,又避免在 header 里反复 encode/decode 出错。生产环境推荐这条路径。
别踩的坑:c.FormFile() + strings.ToValidUTF8() 没用
有人试过对 fileHeader.Filename 做 strings.ToValidUTF8() 或强制 utf8.DecodeRuneInString(),这解决不了根本问题——因为 Go 的 multipart 包在读取时已经按错误字符集解释了字节流,原始信息已损毁。就像把 JPEG 用 ASCII 打开再保存,再怎么“修复”也回不到原图。
真正要动手的地方,是绕过 c.FormFile(),控制 multipart 解析的源头。否则所有后续转码都是徒劳。











