
当使用 AJAX 发送 POST 请求时,Go 后端调用 json.NewDecoder(r.Body).Decode() 报 EOF panic,而 Postman 测试正常——根本原因通常是前端以 application/x-www-form-urlencoded(表单格式)发送数据,而非 application/json,导致 r.Body 已被隐式读取或为空。
当使用 ajax 发送 post 请求时,go 后端调用 `json.newdecoder(r.body).decode()` 报 `eof` panic,而 postman 测试正常——根本原因通常是前端以 `application/x-www-form-urlencoded`(表单格式)发送数据,而非 `application/json`,导致 `r.body` 已被隐式读取或为空。
在 Go 的 net/http 中,r.Body 是一个一次性可读的 io.ReadCloser。若请求头为 Content-Type: application/x-www-form-urlencoded(AJAX 默认行为,除非显式设置),Go 会在首次调用 r.FormValue、r.PostFormValue 或 r.ParseForm() 时自动读取并解析 r.Body,随后再次尝试 json.NewDecoder(r.Body).Decode() 就会遇到空流,触发 EOF 错误。
✅ 正确处理方式:按 Content-Type 分支解析
func handlePost(w http.ResponseWriter, r *http.Request) {
// 确保只处理 POST 请求
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
contentType := r.Header.Get("Content-Type")
var data map[string]interface{}
switch {
case strings.HasPrefix(contentType, "application/json"):
// JSON 格式:直接解码 Body
decoder := json.NewDecoder(r.Body)
if err := decoder.Decode(&data); err != nil {
http.Error(w, "Invalid JSON: "+err.Error(), http.StatusBadRequest)
return
}
case strings.HasPrefix(contentType, "application/x-www-form-urlencoded") ||
strings.HasPrefix(contentType, "multipart/form-data"):
// 表单格式:必须先 ParseForm(它会读取并解析 Body)
if err := r.ParseForm(); err != nil {
http.Error(w, "Failed to parse form: "+err.Error(), http.StatusBadRequest)
return
}
defer r.Body.Close() // 显式关闭,避免资源泄漏(虽 ParseForm 后 Body 已耗尽)
// 将表单值转为 map[string]interface{}
data = make(map[string]interface{})
for key, values := range r.PostForm {
if len(values) > 0 {
data[key] = values[0] // 取第一个值(兼容单值场景)
}
}
default:
http.Error(w, "Unsupported Content-Type: "+contentType, http.StatusUnsupportedMediaType)
return
}
// ✅ 此时 data 已安全填充,可继续业务逻辑
log.Printf("Parsed data: %+v", data)
json.NewEncoder(w).Encode(map[string]string{"status": "success", "received": fmt.Sprintf("%v", data)})
}⚠️ 关键注意事项
-
不要混用
r.ParseForm()和json.NewDecoder(r.Body):二者均消费r.Body,顺序错误或重复调用必致EOF。 -
AJAX 默认是表单提交:若想发 JSON,请在前端显式设置:
fetch("/api", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name: "Alice", age: 30 }) }); -
r.ParseForm()必须在读取r.Body前调用(如需表单数据),且仅需调用一次。 -
始终校验
Content-Type:避免假设请求格式,提升健壮性与安全性。 -
错误处理不可省略:
panic(err)在生产环境应替换为http.Error或结构化日志,防止服务崩溃。
✅ 总结
EOF 并非网络中断,而是对已关闭/已读尽的 r.Body 的二次读取。核心原则是:根据 Content-Type 选择唯一且正确的解析路径,并确保 r.Body 仅被消费一次。通过分支判断 + 显式错误处理,即可稳定支持 JSON 与表单两类常见 POST 场景。

















