c.FormFile() 返回 nil 是因 multipart 请求体构造错误,需用 bytes.Buffer + multipart.Writer 正确构建:字段名一致、调用 writer.Close()、设置 writer.FormDataContentType();若中间件提前读取 Request.Body 会导致 MultipartForm 为 nil。

c.FormFile() 在测试中返回 nil,不是 Gin 有 bug,而是 multipart 请求体没构造对。 大多数失败都卡在这一步,后续逻辑再正确也白搭。
怎么用 bytes.Buffer + multipart.Writer 构造合法的文件上传请求
必须让 Go 标准库的 multipart.Reader 能完整解析出字段,否则 c.FormFile() 永远拿不到文件头。
- 用
bytes.Buffer承载整个 body,不能用字符串拼接或strings.NewReader - 调用
writer.CreateFormFile("file", "test.png")时,第一个参数必须和 handler 里c.FormFile("file")的字段名完全一致 - 写完内容后必须调用
writer.Close(),否则结尾 boundary 缺失,ParseMultipartForm直接失败 -
req.Header.Set("Content-Type", writer.FormDataContentType())这行不能少,且值必须来自writer实例,不能手写或硬编码
为什么 handler 里 c.MultipartForm() 是 nil
Gin 只在首次调用 c.FormFile()、c.PostForm() 或 c.MultipartForm() 时才触发解析。如果中间件提前读了 c.Request.Body(比如日志中间件、鉴权中间件),body 流就关了,后续解析直接跳过。
- 检查 handler 开头加
fmt.Printf("form: %+v\n", c.MultipartForm()),输出&{nil nil nil}就说明没解析 - 确保测试用的
req.Method是POST或PUT,其他方法 Gin 不走 multipart 解析路径 - 避免在测试中调用
c.GetRawData()或ioutil.ReadAll(c.Request.Body)—— 这会消耗掉 body 流
如何覆盖空文件、超限、非法扩展名等边界 case
Gin 本身不校验文件大小或后缀,这些逻辑都在 handler 里手动写。测试的关键是让 c.FormFile() 成功返回一个非 nil 的 *multipart.FileHeader,然后业务代码才能走到判断分支。
- 空文件:用
writer.CreateFormFile("file", "empty.txt")后不写任何字节,fh.Size就是 0 - 超限:构造一个大 buffer(比如 11MB),但 handler 中设了
c.Request.ParseMultipartForm(10 ,就会触发 <code>http.ErrMissingBoundary或自定义错误 - 非法扩展名:传
"virus.exe"作为 filename,靠filepath.Ext(fh.Filename)判断即可
真正容易被忽略的是:测试中所有对 c.Request.Body 的读取操作都是不可逆的,哪怕只调一次 io.ReadAll,后续 c.FormFile() 就必然失败 —— 这个流没法“重放”,只能靠 httptest.NewRecorder() 和原始 bytes.Buffer 从头再来。


















