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

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,因为:

  1. 流式解析:不需要把整个请求体加载到内存
  2. 自动处理: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))
}

相关推荐
luj_17681 小时前
塔防牌:策略与卡牌的智慧碰撞
服务器·c语言·开发语言·经验分享·算法
wuyk5551 小时前
3.链表:用指针串联的动态数据结构
c语言·开发语言·数据结构·链表
weixin_383196472 小时前
java基础面试题@Autowired和@Resource区别
java·开发语言
言乐63 小时前
Python游戏水平测试辅助系统2
开发语言·windows·python·游戏·django
我不是疯子是傻子3 小时前
Qt 实时曲线卡顿优化:从QPainter到OpenGL的3级加速实战
开发语言·qt
zlinear数据采集卡3 小时前
D223 ADC数据处理流水线:从原始采样值到工程单位的完整转换链
开发语言·arm开发·嵌入式硬件·fpga开发·开源·c#
nnerddboy4 小时前
Rust教程04:引用、借用与切片
开发语言·后端·rust
jerryinwuhan4 小时前
《Python数据预处理》第四章
开发语言·python
长江&后浪4 小时前
防火墙的描述
开发语言·网络·php
观无4 小时前
若依EasyExcel实现单元格合并
开发语言·前端·javascript