吃透 tree.go:数据结构、建树算法 addseg、匹配算法 match、参数解析 splitSegment。
1. 数据结构(tree.go:32-41)
type Tree struct {
prefix string // 当前节点的静态段,如 "user"
fixrouters []*Tree // 静态子节点(按序优先匹配)
wildcard *Tree // 参数子节点(:id 这类,整棵树只有一个 wildcard 通道)
leaves []*leafInfo // 本节点终结的路由
}
type leafInfo struct { // tree.go:387-395
wildcards []string // 参数名列表,如 [":id"]
regexps *regexp.Regexp // 整条路由编译出的完整正则(可空)
runObject interface{} // *ControllerInfo(路由对象)
}
设计要点:
- 三层匹配顺序:fixrouters(精确)→ wildcard(参数)→ leaves(吞剩余段)
- 每个
ControllerRegister.routers[method]是一棵*Tree;HTTP method 各自独立
- leaves 是头插:
addseg里leaves = append([]*leafInfo{leaf}, leaves...)(tree.go:210-213)→ 同前缀后注册的优先匹配
2. splitSegment:单段解析的状态机(tree.go:482-588)
输入一段(两个 / 之间),输出 `(isParam, wildcards, regex)":
| 输入段 | wildcards | regex |
|---|---|---|
admin | - | - |
:id | [:id] | - |
?:id | [:id](可空) | - |
:id:int | [:id] | [0-9]+ |
:name:string | [:name] | [\w]+ |
:id(\d{4}) | [:id] | \d{4} |
* | [:splat] | - |
*.* | [:path :ext] | - |
实现是逐字符状态机,处理 :、(、)、?、`` 转义。源码注释即完整规则表,阅读时对照 tree_test.go 的 TestSplitSegment。
3. 建树:addseg(tree.go:207-281)
注册 /api/user/:id/comments 的递归过程:
addseg(segments=["api","user",":id","comments"], wildcard=[], reg="")
1. 段 "api" 是静态:
在 fixrouters 找 prefix=="api" 的子树,没有则建
→ 递归 addseg(["user",":id","comments"])
2. "user" 静态 → 同上递归
3. ":id" 是参数:
wildcard 子树不存在则建(w.prefix=":id")
wildcards 累加 [":id"],reg 累加 "[^/]+"?
→ 递归 addseg(["comments"], [":id"], …)
4. segments 空:构造 leaf{wildcards,regexps,runObject},头插进当前节点 leaves
关键代码形态:
if len(segments) == 0 { // 终止:挂叶子
leaf := &leafInfo{wildcards, regexp.MustCompile(reg), runObject}
p.leaves = append([]*leafInfo{leaf}, p.leaves...) // 头插!
}
参数段的处理(tree.go:238-262 附近):?:id 可空时会同时尝试"跳过该段"的路径(构建双路径)。
AddTree / addtree(合并,tree.go:50-159)
Namespace 合并用:把子树所有 leaves 带前缀重新 addseg 进父树;filterTreeWithPrefix(161-198)把前缀上的 wildcards/regexps 合并进每个 leaf——beego 没有真正的"子树挂接",合并即重建。
4. 匹配:Match → match(tree.go:284-385)
func (t *Tree) Match(url string) (runObject, params) {
url = path.Clean(url) // 规整 //、. 等
return t.match(url, nil, nil, nil)
}
递归 match 的每一层,对当前段:
① 遍历 fixrouters:prefix 精确等于段 → 进入子树递归
② 失败且段带 .json/.xml/.html 后缀:
剥后缀再试 fixrouters(343-358),命中则 params[":ext"]=后缀
③ 失败 → 若 wildcard 非空:
段值入 wildcardValues,进 wildcard 子树(359-361)
④ 子树都失败 → 尝试本层 leaves:
剩余所有段并入 wildcardValues(363-383)
对每个 leaf 调 leafInfo.match 验证
leafInfo.match(tree.go:397-457)
分两类:
无 regexps(纯 :id/:splat):按 wildcards 顺序把值 ctx.Input.SetParam(name, val) 写回。:splat 特殊:匹配多段时自动展开 :splat0,:splat1…(router.go:1124-1128 处理)。
有 regexps(如 :id:int):regexps.MatchString(path.Join(wildcardValues...));命中后 FindStringSubmatch 取捕获组回填。不命中则该 leaf 失败,回到上层试下一个 leaf / 回溯。
5. 匹配示例走读
注册:
web.Router("/api/:id:int", &A{}) // leaf1: regex ^[0-9]+$
web.Router("/api/:name", &B{}) // leaf2
web.Router("/api/list", &C{})
树结构:
root ── "api"(fixrouter)
├─ "list"(fixrouter)→ leaf: C
├─ wildcard(":id")
│ └─ leaves: [leaf2(:name 后注册在前?), leaf1(:id:int)]
│ (实际按注册顺序头插:若先注册 leaf1 后注册 leaf2,leaves=[leaf2, leaf1])
请求 /api/123:
- root 层段 "api" → fixrouters 命中,进子树
- 子树段 "123":"list" 不等;无后缀;进 wildcard,wildcardValues=["123"]
- wildcard 子树无 fixrouters,剩余段空,试 leaves:
-
- leaf2(无 regex):
SetParam(":name","123")→ 返回 B ✓(头插优先)
- leaf2(无 regex):
-
- 若 leaf2 不存在,leaf1 的
^[0-9]+$命中 → A
- 若 leaf2 不存在,leaf1 的
请求 /api/list:
- "api" → 子树
- "list" 精确命中 → C(静态永远优先于参数)
请求 /api/list.json:
- "list" 不等(整段是 list.json)
- 剥
.json后缀 → "list" 命中,:ext= "json" → C
6. 性能与局限
- 静态路由:逐层 map 无,但 fixrouters 是 slice 线性扫——子路由多时 O(段数 × 平均子节点数) ;一般应用 (<100 路由) 足够
- 参数路由:无前缀树压缩(radix),与 httprouter 等相比是朴素实现
- 回溯成本:fixrouters 失败后剥后缀、wildcard、leaves 层层兜底,最坏复杂度偏高,但日常路由深度浅
- 优化技巧(源码可学):RouterCaseSensitive=false 时注册即 lowercase(addToRouter:320-331),匹配路径也 lowercase,大小写不敏感零开销
7. 与 tree_test.go 对读
推荐阅读的用例:
TestSplitTrailingColon、TestTreeReg::id([0-9]+)捕获组
TestAddPrefix:AddTree 前缀合并
TestPreFixRoute:静态优先级
TestWildcard:*splat展开行为
测试即规范,改动 tree.go 必须全绿这些用例。