HTTP 路由设计与请求处理
一、知识点总结
1.1 ServeMux 的匹配规则深度解析
ServeMux 的路由匹配遵循最长前缀匹配原则,理解它的行为对排查 404 问题非常关键:
| 注册路径 | 请求路径 | 是否匹配 | 说明 |
|---|---|---|---|
/api/ |
/api/users |
✅ | 前缀匹配,/api/ 是最长匹配前缀 |
/api/users |
/api/users |
✅ | 精确匹配,优先于 /api/ |
/ |
/anything |
✅ | 根路径 / 是兜底匹配 |
/foo |
/foo/ |
✅ | 自动重定向到 /foo(301) |
/foo/ |
/foo |
✅ | 自动重定向到 /foo/(301) |
/api |
/api/ |
✅ | 同上,自动处理尾部斜杠 |
关键陷阱 :ServeMux 不支持 路由参数(如 /users/:id),也不支持通配符。这是标准库 router 与 Gin/Echo 等框架 router 的最大差距。
1.2 多路复用器的嵌套与分区
大型项目通常按业务模块拆分路由,可以用 StripPrefix 或嵌套 ServeMux 实现子路由:
go
// 方式一:嵌套 ServeMux
apiMux := http.NewServeMux()
apiMux.HandleFunc("/users", handleUsers)
apiMux.HandleFunc("/orders", handleOrders)
rootMux := http.NewServeMux()
rootMux.Handle("/api/", http.StripPrefix("/api", apiMux))
http.StripPrefix(prefix, handler) 是一个适配器函数 ,它先从请求路径中剥掉指定前缀,再把修改后的请求交给子 Handler 处理。注意前缀末尾是否需要 / 是个常见踩坑点。
1.3 请求参数解析全景
HTTP 请求传递数据有四种常见渠道:
| 数据来源 | 获取方式 | 示例 URL |
|---|---|---|
| URL Query String | r.URL.Query().Get("key") |
/search?q=go |
| POST Form | r.PostFormValue("key") |
Body: name=sky&age=25 |
| Path(需手动解析) | strings.Split(r.URL.Path, "/") |
/users/123 |
| Header | r.Header.Get("X-Token") |
--- |
Form 解析陷阱 :r.FormValue() 和 r.PostFormValue() 会隐式调用 ParseForm() ,但如果在 Handler 中同时读取 Body(io.ReadAll(r.Body)),会导致 Form 解析失败------因为 Body 只能读一次。解决办法是:要么只用 Form 系列方法,要么先读 Body 再手动解析。
1.4 请求体读取与 JSON 解析
REST API 中最常见的数据交换格式是 JSON。标准做法:
go
func handleCreateUser(w http.ResponseWriter, r *http.Request) {
var req CreateUserRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
defer r.Body.Close()
// ... 处理逻辑
}
使用 json.Decoder 优于 io.ReadAll + json.Unmarshal,因为:
- 流式解析:不需要把整个请求体加载到内存
- 自动处理:Decoder 内部会处理大 Body 的流式读取
1.5 静态文件服务
http.FileServer 提供静态文件托管能力:
go
fs := http.FileServer(http.Dir("./static"))
mux.Handle("/static/", http.StripPrefix("/static/", fs))
安全警示 :直接用 http.Dir(".") 作为根目录可能暴露源码文件。生产环境应限制只开放特定目录,并禁用目录列表(Go 1.22+ 已默认禁用,但旧版本需注意)。
1.6 重定向与错误处理
http.Redirect(w, r, "/new-path", http.StatusFound)------ 302 临时重定向http.Error(w, "msg", code)------ 便捷返回错误响应http.NotFound(w, r)------ 404 响应http.ServeFile(w, r, "path")------ 直接返回文件内容
二、练习代码
示例 1:自定义路径参数解析器
go
package main
import (
"fmt"
"log"
"net/http"
"strconv"
"strings"
)
// UserStore 模拟用户数据存储
type UserStore struct {
users map[int]string
}
func NewUserStore() *UserStore {
return &UserStore{
users: map[int]string{
1: "Alice",
2: "Bob",
3: "Charlie",
},
}
}
func main() {
store := NewUserStore()
mux := http.NewServeMux()
// 路由:/api/users -> 列出所有用户
// /api/users/{id} -> 获取指定用户
// 使用最长前缀匹配策略:先注册精确路径,再注册前缀路径
mux.HandleFunc("/api/users/", func(w http.ResponseWriter, r *http.Request) {
// 从 /api/users/{id} 中提取 id
// r.URL.Path 可能是 /api/users/ 或 /api/users/123
tail := strings.TrimPrefix(r.URL.Path, "/api/users/")
tail = strings.Trim(tail, "/")
if tail == "" {
// /api/users/ 尾部斜杠情况,列出所有
listUsers(w, store)
return
}
id, err := strconv.Atoi(tail)
if err != nil {
http.Error(w, "invalid user id", http.StatusBadRequest)
return
}
getUser(w, store, id)
})
mux.HandleFunc("/api/users", func(w http.ResponseWriter, r *http.Request) {
// /api/users 无尾部斜杠
if r.Method != http.MethodGet {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
listUsers(w, store)
})
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "API Server. Try /api/users or /api/users/1")
})
log.Println("Server on :8080")
log.Fatal(http.ListenAndServe(":8080", mux))
}
func listUsers(w http.ResponseWriter, store *UserStore) {
w.Header().Set("Content-Type", "text/plain")
fmt.Fprintln(w, "User List:")
for id, name := range store.users {
fmt.Fprintf(w, " ID=%d, Name=%s\n", id, name)
}
}
func getUser(w http.ResponseWriter, store *UserStore, id int) {
name, ok := store.users[id]
if !ok {
http.NotFound(w, nil)
return
}
fmt.Fprintf(w, "User: ID=%d, Name=%s\n", id, name)
}
示例 2:Query / Form / Header 全参数解析
go
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"strconv"
)
func main() {
mux := http.NewServeMux()
// GET /search?q=go&page=2&size=10
mux.HandleFunc("/search", func(w http.ResponseWriter, r *http.Request) {
// 解析 Query String
query := r.URL.Query()
q := query.Get("q")
page, _ := strconv.Atoi(query.Get("page"))
if page < 1 {
page = 1
}
size, _ := strconv.Atoi(query.Get("size"))
if size < 1 || size > 100 {
size = 10
}
fmt.Fprintf(w, "Search: q=%s, page=%d, size=%d\n", q, page, size)
})
// POST /login - 表单提交
mux.HandleFunc("/login", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "POST only", http.StatusMethodNotAllowed)
return
}
// ParseForm 自动解析 application/x-www-form-urlencoded
if err := r.ParseForm(); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
username := r.PostFormValue("username")
password := r.PostFormValue("password")
// 安全提示:实际项目绝不要明文打印密码!此处仅演示
fmt.Fprintf(w, "Login: username=%s, password=***\n", username)
})
// POST /api/users - JSON Body
mux.HandleFunc("/api/users", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
listAllUsers(w, r)
return
}
var req struct {
Name string `json:"name"`
Email string `json:"email"`
Age int `json:"age"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
defer r.Body.Close()
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(map[string]interface{}{
"id": 42,
"name": req.Name,
"email": req.Email,
"age": req.Age,
})
})
// /headers - 展示请求头读取
mux.HandleFunc("/headers", func(w http.ResponseWriter, r *http.Request) {
auth := r.Header.Get("Authorization")
contentType := r.Header.Get("Content-Type")
custom := r.Header.Get("X-Request-ID")
fmt.Fprintf(w, "Authorization: %s\n", auth)
fmt.Fprintf(w, "Content-Type: %s\n", contentType)
fmt.Fprintf(w, "X-Request-ID: %s\n", custom)
})
log.Println("Server on :8080")
log.Fatal(http.ListenAndServe(":8080", mux))
}
func listAllUsers(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "GET /api/users - user list")
}
示例 3:嵌套 ServeMux 实现 API 版本化路由
go
package main
import (
"fmt"
"log"
"net/http"
)
func main() {
// v1 API
v1 := http.NewServeMux()
v1.HandleFunc("/users", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "v1 users list")
})
v1.HandleFunc("/users/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "v1 user detail")
})
// v2 API(结构可能完全不同)
v2 := http.NewServeMux()
v2.HandleFunc("/users", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, `{"version":"v2","users":[]}`)
})
// 根路由
root := http.NewServeMux()
// 注意:StripPrefix 的第二个参数 handler 收到的 r.URL.Path 已被修改
// /api/v1/users -> StripPrefix("/api/v1") -> /users
root.Handle("/api/v1/", http.StripPrefix("/api/v1", v1))
root.Handle("/api/v2/", http.StripPrefix("/api/v2", v2))
root.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "API Server")
fmt.Fprintln(w, " /api/v1/users")
fmt.Fprintln(w, " /api/v2/users")
})
log.Println("Server on :8080")
log.Fatal(http.ListenAndServe(":8080", root))
}