
本文讲解如何使用 go 的 `multipart.reader` 正确解析包含文件(如 pdf)和 json 字符串的混合表单请求,避免因误读 `r.body` 导致 json 解析失败的问题。
在 Go Web 开发中,处理前端(如 AngularJS)通过 multipart/form-data 提交的混合表单数据(例如一个 PDF 文件 + 一段 JSON 元数据)是一个常见但易出错的场景。初学者常误以为调用 r.ParseMultipartForm() 后,r.Body 就“剩下”了纯 JSON 内容,从而直接用 json.NewDecoder(r.Body) 解析——这是错误的:r.Body 在 ParseMultipartForm() 后已耗尽或处于不可预测状态,且 multipart 请求体是二进制分段结构,不能当作普通 JSON 流读取。
正确做法是绕过 ParseMultipartForm(),改用 r.MultipartReader() 获取一个 mime/multipart.Reader,然后逐个遍历表单部件(parts),按字段名(part.FormName())区分处理:
- 当 part.FormName() == "file" 时,将其内容流式写入磁盘(如 PDF);
- 当 part.FormName() == "doc" 时,用 json.NewDecoder(part) 直接解码该 part 的字节流为结构体。
以下是推荐的完整实现:
func (s *Server) PostFileHandler(w http.ResponseWriter, r *http.Request) {
// 获取 multipart reader(无需预先 ParseMultipartForm)
mr, err := r.MultipartReader()
if err != nil {
http.Error(w, "无法初始化 multipart reader: "+err.Error(), http.StatusBadRequest)
return
}
doc := Doc{} // 假设 Doc 是你定义的结构体,含 Title, Cat, Date, Url, Id 等字段
for {
part, err := mr.NextPart()
// 所有 parts 已读完
if err == io.EOF {
break
}
if err != nil {
http.Error(w, "读取 multipart part 失败: "+err.Error(), http.StatusInternalServerError)
return
}
switch part.FormName() {
case "file":
filename := part.FileName()
if filename == "" {
http.Error(w, "文件名为空", http.StatusBadRequest)
return
}
doc.Url = filename
outfile, err := os.Create("./docs/" + filename)
if err != nil {
http.Error(w, "创建文件失败: "+err.Error(), http.StatusInternalServerError)
return
}
defer outfile.Close() // 注意:defer 在循环中需谨慎;此处安全,因每次循环新建 outfile
_, err = io.Copy(outfile, part)
if err != nil {
http.Error(w, "保存文件失败: "+err.Error(), http.StatusInternalServerError)
return
}
fmt.Printf("✅ 已保存文件: %s\n", filename)
case "doc":
// 直接解码当前 part(它是一个 io.Reader,内容即原始 JSON 字符串)
if err := json.NewDecoder(part).Decode(&doc); err != nil {
http.Error(w, "JSON 解析失败: "+err.Error(), http.StatusBadRequest)
return
}
fmt.Printf("✅ 已解析元数据: %+v\n", doc)
default:
// 可选:忽略未知字段,或记录警告
fmt.Printf("⚠️ 跳过未知字段: %s\n", part.FormName())
}
}
// 补充业务逻辑:生成 ID、存入数据库等
doc.Id = len(docs) + 1
if err := s.db.Insert(&doc); err != nil {
checkErr(err, "数据库插入失败")
http.Error(w, "保存文档元数据失败", http.StatusInternalServerError)
return
}
s.Ren.JSON(w, http.StatusOK, &doc) // 假设 Ren 是自定义响应封装器
}✅ 关键要点总结:
- ❌ 不要调用 r.ParseMultipartForm() 后再读 r.Body —— 它已无效;
- ✅ 使用 r.MultipartReader() + mr.NextPart() 按需提取每个字段;
- ✅ part 本身实现了 io.Reader,可直接传给 json.NewDecoder() 或 io.Copy();
- ✅ 注意 part.FileName() 仅对文件字段有效,doc 字段调用会返回空字符串;
- ⚠️ defer outfile.Close() 在循环中是安全的(每个 outfile 是独立变量),但若需更严格控制,可用显式 Close();
- ? 生产环境建议增加 MIME 类型校验(如 part.Header.Get("Content-Type"))、文件大小限制、JSON 字段白名单等安全措施。
通过这种方式,你能健壮、高效地处理任意数量的混合表单字段,为前后端协作提供清晰可靠的数据契约。










