3. HTTP请求解析:使用net/http解析GET/POST请求参数、路径参数、查询参数、请求体
在构建REST API时,请求参数的解析是参数校验的第一道关卡。Go标准库net/http提供了原生且高效的请求解析能力,理解其底层机制是后续校验逻辑正确落地的基石。本章将系统梳理四种核心参数类型的解析方式,并揭示常见陷阱。
3.1 参数类型与解析概览
REST API中,参数通常通过以下四种载体传递,每种载体对应不同的解析策略:
| 参数类型 | 传递位置 | 典型示例 | net/http 核心方法 |
|---|---|---|---|
| 路径参数 | URL路径段 | /users/42 |
r.URL.Path + 手动解析 |
| 查询参数 | URL问号后 | /users?page=2&limit=10 |
r.URL.Query() |
| 表单参数 | 请求体(POST/PUT) | username=alice&age=30 |
r.ParseForm() + r.Form |
| JSON/XML请求体 | 请求体(POST/PUT) | {"name":"alice"} |
json.NewDecoder(r.Body) |
核心原则:
net/http 不会自动解析请求体。你必须显式调用 ParseForm()、ParseMultipartForm() 或手动读取 r.Body,否则 r.Form、r.PostForm 等字段将为空。
3.2 路径参数解析:手动拆分与标准库局限
Go标准库net/http的默认ServeMux不支持路径参数模式匹配(如/users/:id)。你需要手动从r.URL.Path中提取参数值。
func userHandler(w http.ResponseWriter, r *http.Request) {
// 假设路由为 /users/{id}
path := r.URL.Path
// 去掉前缀 /users/
idStr := strings.TrimPrefix(path, "/users/")
if idStr == "" || idStr == path {
http.Error(w, "invalid path", http.StatusBadRequest)
return
}
// 此时 idStr 即为路径参数值,例如 "42"
fmt.Fprintf(w, "User ID: %s", idStr)
}
生产环境建议: 手动解析路径参数容易出错且难以维护。在真实项目中,推荐使用
gorilla/mux、chi 或 gin 等第三方路由器,它们原生支持路径参数提取,例如 chi.URLParam(r, "id")。
3.3 查询参数解析:Query() 与 Form() 的差异
查询参数位于URL的?之后,使用r.URL.Query()可以快速获取。但r.Form也包含查询参数,两者存在细微差别。
func searchHandler(w http.ResponseWriter, r *http.Request) {
// 方法一:仅获取查询参数
queryParams := r.URL.Query()
page := queryParams.Get("page") // "2"
limit := queryParams.Get("limit") // "10"
// 方法二:通过 r.Form 获取(需先调用 ParseForm)
r.ParseForm()
page2 := r.Form.Get("page") // 同样返回 "2"
// 关键区别:r.Form 包含查询参数 + 表单参数
// r.URL.Query() 只包含查询参数
}
| 方法 | 数据来源 | 是否需要显式解析 | 典型使用场景 |
|---|---|---|---|
r.URL.Query() |
仅URL查询字符串 | 否(直接解析) | GET请求,只关心查询参数 |
r.Form |
查询参数 + 表单参数 | 是(需先调用ParseForm()) |
需要同时处理查询和表单参数 |
r.PostForm |
仅表单参数(POST/PUT) | 是(需先调用ParseForm()) |
只关心POST表单数据,忽略查询参数 |
3.4 请求体解析:表单与JSON的两种路径
请求体解析是参数校验中最容易出错的环节。根据Content-Type的不同,解析方式截然不同。
3.4.1 表单请求体(application/x-www-form-urlencoded)
func formHandler(w http.ResponseWriter, r *http.Request) {
// 1. 检查 Content-Type
contentType := r.Header.Get("Content-Type")
if !strings.HasPrefix(contentType, "application/x-www-form-urlencoded") {
http.Error(w, "unsupported content type", http.StatusUnsupportedMediaType)
return
}
// 2. 解析表单
err := r.ParseForm()
if err != nil {
http.Error(w, "failed to parse form", http.StatusBadRequest)
return
}
// 3. 获取参数
username := r.FormValue("username") // 便捷方法,内部调用 ParseForm
age := r.PostFormValue("age") // 仅从表单获取,忽略查询参数
}
3.4.2 JSON请求体(application/json)
type CreateUserRequest struct {
Username string `json:"username"`
Age int `json:"age"`
Email string `json:"email"`
}
func jsonHandler(w http.ResponseWriter, r *http.Request) {
// 1. 检查 Content-Type
if r.Header.Get("Content-Type") != "application/json" {
http.Error(w, "Content-Type must be application/json", http.StatusUnsupportedMediaType)
return
}
// 2. 限制请求体大小(防止内存攻击)
r.Body = http.MaxBytesReader(w, r.Body, 1<<20) // 1MB
// 3. 解码 JSON
var req CreateUserRequest
decoder := json.NewDecoder(r.Body)
// 禁止未知字段(严格模式)
decoder.DisallowUnknownFields()
err := decoder.Decode(&req)
if err != nil {
http.Error(w, "invalid JSON: "+err.Error(), http.StatusBadRequest)
return
}
// 4. 检查是否有多余数据
if decoder.More() {
http.Error(w, "request body contains multiple JSON objects", http.StatusBadRequest)
return
}
// 此时 req 结构体已填充,可进行后续校验
fmt.Fprintf(w, "User: %+v", req)
}
关键实践:
- 始终使用
http.MaxBytesReader限制请求体大小,防止大请求耗尽服务器内存。 - 使用
decoder.DisallowUnknownFields()拒绝未知字段,避免客户端传错字段名导致静默忽略。 - 调用
decoder.Decode()后检查decoder.More(),确保请求体只包含一个JSON对象。 - 请求体读取后必须关闭
r.Body,但net/http在请求处理结束后会自动关闭,无需手动操作。
3.5 请求解析完整流程:从原始字节到结构化数据
下面的SVG图展示了从HTTP请求到达Go服务器,到参数被解析为结构化数据的完整流程:
3.6 常见陷阱与最佳实践
陷阱1:多次调用 ParseForm 是安全的
r.ParseForm() 可以多次调用,第二次调用不会重复解析,也不会报错。但 r.Body 只能读取一次,JSON解码后无法再次读取。
陷阱2:r.Form 包含查询参数和表单参数
如果URL中同时包含查询参数和表单参数,
如果URL中同时包含查询参数和表单参数,
r.Form.Get("key") 会返回查询参数的值(查询参数优先级更高)。使用 r.PostFormValue() 可以只获取表单参数。
最佳实践清单:
- 始终检查 Content-Type:在解析请求体前验证
Content-Type头,返回415 Unsupported Media Type拒绝不支持的格式。 - 限制请求体大小:使用
http.MaxBytesReader防止大请求攻击。 - JSON解码使用严格模式:启用
DisallowUnknownFields()拒绝未知字段。 - 路径参数使用第三方路由器:避免手动解析
r.URL.Path,使用chi、gorilla/mux等库。 - 查询参数使用
r.URL.Query():语义清晰,不依赖ParseForm()调用。 - 表单参数使用
r.PostFormValue():避免意外读取查询参数。
3.7 参数解析后的数据流向
下面的SVG图展示了参数解析完成后,数据如何从原始请求流向业务逻辑层:
3.8 本章小结
HTTP请求解析是参数校验的起点。通过本章的学习,你应该掌握:
- 四种参数类型(路径、查询、表单、JSON)的解析方法
r.URL.Query()、r.Form、r.PostForm的差异与选择- JSON请求体解析的严格模式与安全限制
- 解析层与校验层的职责分离原则