欢迎光临
我们一直在努力

Golang HTTP 路由设计与请求处理

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 实现子路由:

// 方式一:嵌套 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。标准做法:

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 提供静态文件托管能力:

    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:自定义路径参数解析器

    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 全参数解析

    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 版本化路由

    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))
    }


    赞(0)
    未经允许不得转载:171主机测评 » Golang HTTP 路由设计与请求处理
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址