mime/multipart 仅构造请求体,需手动设置匹配的 content-type 头、调用 w.close() 并传 io.reader 到 http.newrequest,否则导致 400 或解析失败。

为什么 mime/multipart 不能直接发 HTTP 请求
很多人卡在第一步:写完 multipart.Writer,发现没地方塞进 http.NewRequest。这是因为 mime/multipart 只负责构造符合 RFC 7578 的请求体(body),不处理 HTTP 头、连接或发送逻辑。你必须手动设置 Content-Type 头为 multipart/form-data; boundary=...,且这个 boundary 必须和 multipart.Writer 实际用的一致。
常见错误现象:400 Bad Request 或后端解析出空字段 —— 很大概率是头里写的 boundary 和写入时用的不一致,或者忘了设头。
- 永远用
w.Boundary()获取真实 boundary,不要手写或硬编码 -
http.NewRequest的 body 参数必须传*bytes.Reader或其他io.Reader,不能传*multipart.Writer - 写完所有 part 后必须调用
w.Close(),否则结尾 boundary 缺失,服务端无法识别结束
如何正确构造带文件和表单字段的请求体
核心是用 multipart.Writer 写入不同类型的 part:普通字段用 w.WriteField(),文件用 w.CreateFormFile() 或更灵活的 w.CreatePart() 配合自定义头。
示例场景:上传一个 avatar.png 文件,同时提交 user_id=123 和 note=hello:
buf := &bytes.Buffer{}
w := multipart.NewWriter(buf)
// 自动设置 Content-Disposition: form-data; name="user_id"
w.WriteField("user_id", "123")
w.WriteField("note", "hello")
// 创建文件 part,自动加 filename 和 Content-Type
f, _ := os.Open("avatar.png")
defer f.Close()
fw, _ := w.CreateFormFile("avatar", "avatar.png")
io.Copy(fw, f)
w.Close() // 关键:生成结尾 boundary
req, _ := http.NewRequest("POST", "https://api.example.com/upload", buf)
req.Header.Set("Content-Type", "multipart/form-data; boundary="+w.Boundary())
-
CreateFormFile会自动写Content-Disposition: form-data; name="..."; filename="...",适合标准文件上传 - 如果需要自定义
Content-Type(比如传 JSON 文件但不想被识别为application/json),改用w.CreatePart()+ 手动写头 - 避免把大文件整个读进内存:用
io.Pipe配合 goroutine 边写边发,但要注意错误传播和超时控制
边界符(boundary)冲突和编码问题
默认 boundary 是随机生成的 ASCII 字符串,但如果你手动指定(比如用 multipart.NewWriterWithBoundary(buf, "my-boundary")),要确保它不包含 \r\n--、不以 - 开头或结尾,且不在原始数据中出现 —— 否则服务端解析会错位。
更隐蔽的问题是中文或 emoji 字段值:虽然 WriteField 内部会做 url.PathEscape,但实际 RFC 要求非 ASCII 字段名/值应使用 Content-Transfer-Encoding: quoted-printable 或 UTF-8 + name*=UTF-8''... 形式。标准库不支持后者,所以含中文的字段名(如 用户ID)大概率被服务端忽略。
- 字段名强制用 ASCII(如
user_id),值可 utf-8 编码,但需确认后端是否接受 raw utf-8 值(多数现代框架支持) - 避免手动指定 boundary;用默认随机值最安全
- 如果必须传非 ASCII 字段名,考虑换用
application/json或服务端约定的其他格式
调试 multipart 请求体的实际内容
线上出问题时,光看代码很难判断 body 是否合法。最有效的方法是打印出原始字节流,人工对照 RFC 检查结构。
注意:打印前确保 w.Close() 已调用,否则结尾 boundary 缺失,输出不完整。
fmt.Printf("Request body:\n%s\n", buf.String())
// 或更安全地查看不可见字符
fmt.Printf("Raw hex: %x\n", buf.Bytes())
- 正常 multipart body 应以
--<boundary>\r\n</boundary>开头,每个 part 以--<boundary>\r\n</boundary>分隔,结尾是--<boundary>--\r\n</boundary> - 如果看到
--<boundary>--\r\n--<boundary>\r\n</boundary></boundary>连续两次,说明Close()被调了两次 - 用
curl -v对比自己程序发出的请求和 curl 的请求体,能快速定位差异点
边界符生成、关闭时机、头与 body 的一致性——这三个点只要错一个,后端就收不到完整数据。别依赖“看起来差不多”,逐字节比对才是真调试。











