Skip to content

第08章:路由进阶 —— 匹配规则与路由树 ​

本章目标 ​

学完本章后,你将能够:

  1. 理解 Gin 路由的底层实现:Radix Tree(基数树)
  2. 掌握路由匹配优先级规则
  3. 使用 NoRoute() 和 NoMethod() 处理异常情况
  4. 使用 Routes() 查看所有已注册路由
  5. 理解 Gin 为什么这么快

前置知识 ​

  • 需要先学习:第03-05章(路由基础、路径参数)

8.1 路由匹配优先级 ​

当一个请求到来时,Gin 按以下优先级匹配路由:

1. 静态路由(精确匹配)     /users/new      ← 优先级最高
2. 参数路由(:param)       /users/:id
3. 通配路由(*param)       /users/*action   ← 优先级最低

验证示例 ​

go
r := gin.Default()

// 静态路由
r.GET("/users/new", func(c *gin.Context) {
	c.String(200, "创建用户页面")
})

// 参数路由
r.GET("/users/:id", func(c *gin.Context) {
	c.String(200, "用户ID:%s", c.Param("id"))
})

// 通配路由
r.GET("/users/*action", func(c *gin.Context) {
	c.String(200, "用户操作:%s", c.Param("action"))
})

测试:

bash
curl http://localhost:8080/users/new
# "创建用户页面"  ← 静态路由精确匹配

curl http://localhost:8080/users/123
# "用户ID:123"  ← 参数路由匹配

curl http://localhost:8080/users/export/csv
# "用户操作:/export/csv"  ← 通配路由匹配(最后才匹配)

8.2 路由冲突与解决 ​

同名参数路由冲突 ​

go
// ❌ 冲突!Gin 会 panic
r.GET("/users/:id", handler1)
r.GET("/users/:name", handler2)  // panic: ':name' in new path ...

// ✅ 不同路径前缀,不冲突
r.GET("/users/:id", handler1)
r.GET("/admins/:name", handler2)

参数路由和通配路由的区分 ​

go
// :id 只匹配一个路径段(user-id、123、abc...)
r.GET("/data/:id", handler)

// *path 匹配剩余所有路径段
r.GET("/data/*path", handler)

8.3 NoRoute():处理 404 ​

go
r := gin.Default()

r.GET("/users", listUsers)
r.GET("/users/:id", getUser)

// 上面都没匹配到 → 到这里
r.NoRoute(func(c *gin.Context) {
	c.JSON(http.StatusNotFound, gin.H{
		"code":    404,
		"message": fmt.Sprintf("路径 %s 不存在", c.Request.URL.Path),
	})
})
bash
curl http://localhost:8080/notexist
# {"code":404,"message":"路径 /notexist 不存在"}

8.4 NoMethod():处理 405 ​

go
r.POST("/users", createUser)

// 路径对了但方法不对 → 到这里
r.NoMethod(func(c *gin.Context) {
	c.JSON(http.StatusMethodNotAllowed, gin.H{
		"code":    405,
		"message": fmt.Sprintf("%s 方法不被允许", c.Request.Method),
	})
})
bash
curl -X DELETE http://localhost:8080/users
# {"code":405,"message":"DELETE 方法不被允许"}
# 路径 /users 存在,但只支持 POST,不支持 DELETE

8.5 Routes():查看所有路由 ​

go
r := gin.Default()

r.GET("/users", handler)
r.GET("/users/:id", handler)
r.POST("/users", handler)

// 打印所有已注册路由
routes := r.Routes()
for _, route := range routes {
	fmt.Printf("%-6s %s → %s\n", route.Method, route.Path, route.Handler)
}
// 输出:
// GET    /users        → main.handler
// GET    /users/:id    → main.handler
// POST   /users        → main.handler

8.6 Radix Tree:Gin 为什么快 ​

Gin 的路由基于 httprouter 的 Radix Tree(基数树) 算法:

普通路由匹配(遍历) ​

请求:GET /users/123
遍历所有路由:
  /users/new    → 不匹配
  /users/:id    → 匹配!

有 1000 个路由,最坏情况要比对 1000 次

Radix Tree 匹配(树形查找) ​

请求:GET /users/123
        ┌─ /
        ├─ users/
        │   ├─ new      → 静态节点
        │   ├─ :id      → 参数节点
        │   └─ *action  → 通配节点
        └─ products/

前缀压缩 + 树形查找 = O(路径长度) 而不是 O(路由数量)

无论注册多少个路由,Gin 的匹配速度几乎不受影响。这就是 Gin "高性能" 的来源。


常见错误 ​

错误1:参数路由和静态路由的顺序理解错误 ​

go
// 新手担心:"/users/:id" 会不会把 "/users/new" 也匹配了?
// 不用担心!Gin 内部做了优先级处理:
// 静态路由永远优先于参数路由

r.GET("/users/:id", handler1)
r.GET("/users/new", handler2)
// 访问 /users/new → 命中 handler2(静态优先)

错误2:路径末尾多加斜杠 ​

go
r.GET("/users", handler)
// /users  ✅ 匹配
// /users/  ❌ 不匹配!

// 如果需要同时匹配 /users 和 /users/
r.GET("/users", handler)
r.GET("/users/", handler) // 单独注册

错误3:*param 得到的值包含前导斜杠 ​

go
r.GET("/files/*filepath", func(c *gin.Context) {
	path := c.Param("filepath")  // "/css/style.css"(注意开头的 /)
	// 如果需要去掉前导 /
	path = strings.TrimPrefix(path, "/")
})

本章小结 ​

  • 路由优先级:静态 > 参数(:id)> 通配(*path)
  • NoRoute() 处理 404,NoMethod() 处理 405
  • Routes() 查看所有已注册路由
  • Radix Tree 保证了路由匹配的高性能
  • Gin 路由匹配时间和路由数量无关,只和路径长度有关

练习题 ​

  1. 创建三个路由:/articles/latest(静态)、/articles/:id(参数)、/articles/*other(通配),分别测试。
  2. 使用 NoRoute() 和 NoMethod() 自定义 404 和 405 响应。
  3. 使用 r.Routes() 打印所有路由,观察不同路由类型的 Handler 信息。
  4. (思考题)如果 GET /users/:id 和 GET /users/:name 不能同时存在,那怎么实现"既能按 ID 查询又能按用户名查询"?

把零散知识,整理成可以反复走的路。