如何在 Go 中正确读取 HTTP 请求的查询参数与请求体数据

本文详解 go 的 net/http 包中 `req.body` 为空的原因,并提供针对 get/post 请求分别获取参数的规范做法:get 应使用 `req.url.query()` 或 `req.parseform()` + `req.form`,而 post 的 json 数据才应通过 `json.decoder(req.body)` 解析。

在 Go 的 net/http 中,*http.Request.Body 仅承载 HTTP 请求体(request body)中的原始字节流,例如 POST/PUT 请求中以 Content-Type: application/json 发送的 JSON 负载。它完全不包含 URL 查询参数(query string)——这些参数位于请求行中(如 /step?steps=1&direction=1),由 req.URL.Query() 直接解析,与 Body 无关。

你遇到的 req.Body 为 nil 或解码失败,根本原因在于:
✅ 你的 AJAX 请求使用的是 method: "get",浏览器将所有 data 自动序列化为 URL 查询参数(见示例 URL),此时 HTTP 请求体为空,req.Body 自然不可读或已关闭;
❌ 调用 json.NewDecoder(req.Body).Decode(&v) 对空 Body 解码必然失败(io.EOF 或 invalid character),且 log.Println(v) 输出 是因解码失败后 v 未被赋值,仍为零值 interface{}(即 nil)。

✅ 正确处理方式(按请求方法区分)

1. 对于 GET 请求(推荐:直接解析 URL 查询参数)

func stepHandler(res http.ResponseWriter, req *http.Request) {
    // 方法一:直接获取 URL 查询参数(最轻量、无需 ParseForm)
    steps := req.URL.Query().Get("steps")           // "1"
    direction := req.URL.Query().Get("direction")   // "1"
    cellsJSON := req.URL.Query().Get("cells")       // "[{\"row\":11,...}]"

    // 方法二:ParseForm 后统一访问(对 GET/POST 均有效,但 GET 下等价于 Query)
    err := req.ParseForm()
    if err != nil {
        http.Error(res, "Invalid form data", http.StatusBadRequest)
        return
    }
    steps = req.FormValue("steps")      // 等价于 req.URL.Query().Get("steps")
    cellsJSON = req.FormValue("cells")

    // 解析 JSON 字符串字段(注意:cells 是 URL 编码后的字符串,需二次 JSON 解码)
    var cells []map[string]int
    if err := json.Unmarshal([]byte(cellsJSON), &cells); err != nil {
        http.Error(res, "Invalid cells JSON", http.StatusBadRequest)
        return
    }

    log.Printf("Steps: %s, Direction: %s, Cells: %+v", steps, direction, cells)
}

2. 对于 POST 请求(发送 JSON Body)

若需真正使用 req.Body,客户端应改为 POST 并设置 Content-Type: application/json:

// 客户端 AJAX(POST + JSON Body)
$.ajax({
  url: "/step",
  method: "POST",
  contentType: "application/json",
  data: JSON.stringify({
    steps: parseInt($("#step-size").val()),
    direction: $("#step-forward").prop("checked") ? 1 : -1,
    cells: painted // 直接传数组,无需 stringify
  }),
  success: function(data) { /* ... */ }
});

服务端则可安全读取 Body:

func stepHandler(res http.ResponseWriter, req *http.Request) {
    if req.Method != "POST" {
        http.Error(res, "Method not allowed", http.StatusMethodNotAllowed)
        return
    }

    var payload struct {
        Steps     int      `json:"steps"`
        Direction int      `json:"direction"`
        Cells     []struct { Row, Column int } `json:"cells"`
    }

    // Body 可被 json.Decoder 读取(自动处理 Content-Length 和关闭)
    if err := json.NewDecoder(req.Body).Decode(&payload); err != nil {
        http.Error(res, "Invalid JSON", http.StatusBadRequest)
        return
    }
    defer req.Body.Close() // 显式关闭(虽 Decoder 通常已关闭,但属最佳实践)

    log.Printf("Payload: %+v", payload)
}

⚠️ 关键注意事项

  • req.Body 只能读取一次:读取后会被耗尽,后续再调用 req.ParseForm() 或 json.Decoder 将失败(除非提前 ioutil.ReadAll 并重置 req.Body)。
  • req.ParseForm() 对 GET 是冗余的:它内部会检查 req.Method,若为 GET 则直接解析 req.URL.RawQuery,无需额外开销。
  • 永远校验 err:json.Decode 失败时 v 不会被修改,直接打印 v 会输出零值(如 nil, 0, ""),易造成误判。
  • 生产环境务必设置超时与限长:避免恶意大请求体阻塞服务,建议配合 http.MaxBytesReader 使用。

总结:理解 HTTP 协议分层是关键——URL 参数属于请求行,Body 属于消息体。Go 的 net/http 严格遵循此规范,选择正确的 API(URL.Query() vs Body)才能高效、健壮地处理各类请求。