r.URL.Query() 是解析 query 参数最稳妥、最语义清晰的方式,它自动解码、拆分并支持多值,而 r.FormValue 和 r.URL.RawQuery 存在行为不可控、漏解码、丢多值等问题。

r.URL.Query() 是解析 query 参数最稳妥、最语义清晰的方式,别用 r.FormValue 或手撕 r.URL.RawQuery —— 它们要么行为不可控,要么漏解码、丢多值、出乱码。
query 参数必须用 r.URL.Query(),不是 r.FormValue
很多人一上来就写 r.FormValue("id") 想取 /api?name=张三&tag=a&tag=b 里的值,结果在 POST 请求里拿不到,或中文变成 %E5%BC%A0%E4%B8%89。原因有二:
-
r.FormValue("key")是“混合读取”:它先尝试解析 body(仅限application/x-www-form-urlencoded),再 fallback 到 URL query —— 但前提是没调过r.ParseForm();一旦调过,r.Form就被填充,r.FormValue就只查r.Form,不再碰 URL -
r.URL.RawQuery是原始未解码字符串,?q=Go%2BDev直接strings.Split会得到"Go%2BDev",不是"Go+Dev" -
r.URL.Query()自动做三件事:URL 解码、按&和=拆分、把同名 key 收进[]string切片 —— 安全、无副作用、无需提前调ParseForm
正确姿势:
query := r.URL.Query()
name := query.Get("name") // 返回第一个值,空时为 ""
tags := query["tag"] // 返回 []string,可 len(tags) > 0 判断是否存在
if query.Has("debug") { ... } // 显式检查参数是否存在
POST 表单必须先调 r.ParseForm(),否则 r.PostForm 为空
对 Content-Type: application/x-www-form-urlencoded 的 POST 请求(比如 HTML 表单默认提交),r.PostForm 字段初始为空。不调 r.ParseForm() 就直接读 r.PostFormValue("user"),永远是空字符串。
立即学习“go语言免费学习笔记(深入)”;
-
r.ParseForm()是“开关”:调了它,r.Form和r.PostForm才有数据;多次调没副作用 -
r.PostFormValue("key")等价于r.PostForm.Get("key"),适合单值场景;要取多值(如复选框)必须用r.PostForm["key"] - 如果表单含文件(
enctype="multipart/form-data"),r.ParseForm()不起作用 —— 必须改用r.ParseMultipartForm(32 (设内存上限,单位字节),否则r.MultipartForm为 nil
常见错误:http.ErrNotMultipart 表示你对普通表单误用了 ParseMultipartForm;http.ErrMissingFile 表示字段名拼错或前端没传。
JSON 请求体不能靠 ParseForm(),必须用 json.NewDecoder(r.Body)
当请求头是 Content-Type: application/json,比如 {"id":123,"tags":["a","b"]},r.ParseForm() 完全无效 —— 它只认 key=value&key2=value2 格式,不认识 JSON 结构。
-
r.Body只能读一次。如果之前调过r.ParseForm()、r.FormValue()或任何读 body 的逻辑,json.NewDecoder(r.Body).Decode(&v)会立刻返回EOF或空结构体 - 别写
json.Unmarshal(io.ReadAll(r.Body), &v):大 payload 可能 OOM;应始终用流式解码:json.NewDecoder(r.Body).Decode(&v) - 若需复用
r.Body(比如日志中间件 + 后续解码),得重置:r.Body = io.NopCloser(bytes.NewReader(data)),其中data是之前io.ReadAll(r.Body)的结果(Go 1.16+ 推荐)
结构体定义建议加校验 tag,比如用 github.com/go-playground/validator/v10:
type CreateUserReq struct {
Name string `json:"name" validate:"required,min=2"`
Email string `json:"email" validate:"required,email"`
}
路径参数(如 /user/{id})标准库不支持,必须用 chi/gorilla/mux
Go 原生 net/http 路由器只做前缀匹配,http.HandleFunc("/user/", ...) 无法提取 id。想写 /user/123 并拿到 "123",必须引入第三方路由库。
-
chi最轻量:用chi.URLParam(r, "id")直接取值,字符串类型,需自行转int -
gorilla/mux类似:vars := mux.Vars(r); id := vars["id"] - 别试图用正则从
r.URL.Path里手动截取 —— 路径可能含编码字符(如/user/%E5%BC%A0%E4%B8%89),URLParam已自动解码
最容易被忽略的一点:query、form、JSON、path 这四类参数来源完全不同,解析方式、触发条件、生命周期都隔离。混用 Get 和 Has、误判 r.Body 是否已被消费、在 multipart 场景下还用 ParseForm —— 这些才是线上 500 和空响应的真正源头。










