必须用 multipart.Writer 构造请求体:创建 bytes.Buffer,用 multipart.NewWriter 写字段和文件,调用 w.Close(),设置 req.Header.Set("Content-Type", w.FormDataContentType())。

用 http.Post 上传文件会失败,必须用 multipart.Writer
直接调用 http.Post 并传入 bytes.NewReader 是行不通的——它只会把原始字节当纯文本发过去,服务器端收不到 multipart/form-data 的边界(boundary)和字段结构。正确做法是手动构造 multipart 请求体。
关键点在于:先创建 bytes.Buffer,再用 multipart.NewWriter 写入字段和文件数据,最后把 Buffer.Bytes() 作为请求体发送,并设置正确的 Content-Type 头(含 boundary)。
-
multipart.Writer会自动生成并管理 boundary,不能硬编码 - 写完所有字段后必须调用
w.Close(),否则 boundary 结尾缺失,服务端解析失败 -
req.Header.Set("Content-Type", w.FormDataContentType())是必须的,不能手写multipart/form-data; boundary=xxx
上传单个文件时如何设置文件名和字段名
服务端(比如 Gin 或标准 http.Request.ParseMultipartForm)依赖 multipart.Writer.WriteField 和 multipart.Writer.CreateFormFile 的参数来识别字段名和文件元信息。字段名(file)、文件名(report.pdf)都由你控制,不是自动推断的。
常见错误是把文件内容当成字符串传给 WriteField,结果服务端收到的是明文内容而非文件流。
立即学习“go语言免费学习笔记(深入)”;
- 普通表单字段用
w.WriteField("username", "alice") - 文件字段必须用
w.CreateFormFile("file", "report.pdf")获取io.Writer,再把文件内容写进去 - 字段名(如
"file")要和服务端r.FormFile("file")中的键完全一致
上传多个文件或混合字段的完整示例
package main
import (
"bytes"
"io"
"mime/multipart"
"net/http"
)
func uploadFiles() error {
buf := &bytes.Buffer{}
w := multipart.NewWriter(buf)
// 写普通字段
if err := w.WriteField("project_id", "123"); err != nil {
return err
}
// 写第一个文件
fw1, err := w.CreateFormFile("files", "a.txt")
if err != nil {
return err
}
io.WriteString(fw1, "content of a.txt")
// 写第二个文件
fw2, err := w.CreateFormFile("files", "b.json")
if err != nil {
return err
}
io.WriteString(fw2, `{"id": 42}`)
// 关键:关闭 writer,生成结尾 boundary
w.Close()
req, err := http.NewRequest("POST", "http://localhost:8080/upload", buf)
if err != nil {
return err
}
req.Header.Set("Content-Type", w.FormDataContentType())
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
return nil
}
注意:CreateFormFile 的第一个参数是字段名(可复用,如都用 "files" 实现多文件),第二个是客户端看到的文件名(影响 Header.Get("Filename"))。
服务端解析时容易忽略的边界条件
Go 标准库要求在读取 multipart 前调用 r.ParseMultipartForm,否则 r.MultipartForm 为 nil。而且这个方法会一次性把全部文件读进内存或临时磁盘,不设限可能被恶意大文件打爆内存。
- 务必设置合理上限:
r.ParseMultipartForm(32 (32MB) - 如果只处理小文件且想避免磁盘临时文件,可设
MaxMemory,超过才落盘 -
r.MultipartForm.File["files"]返回的是[]*multipart.FileHeader,不是单个值 - 读取文件内容前需用
header.Open()获得io.ReadCloser,别漏掉Close()
boundary 不匹配、没调 w.Close()、服务端没调 ParseMultipartForm——这三个问题占了调试失败的八成以上。










