
本文讲解如何使用 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("✅ 已保存文件: %sn", 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("✅ 已解析元数据: %+vn", doc) default: // 可选:忽略未知字段,或记录警告 fmt.Printf("⚠️ 跳过未知字段: %sn", 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 字段白名单等安全措施。
通过这种方式,你能健壮、高效地处理任意数量的混合表单字段,为前后端协作提供清晰可靠的数据契约。