beego路由树Tree

42 阅读3分钟

吃透 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)":

输入段wildcardsregex
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:

  1. root 层段 "api" → fixrouters 命中,进子树
  1. 子树段 "123":"list" 不等;无后缀;进 wildcard,wildcardValues=["123"]
  1. wildcard 子树无 fixrouters,剩余段空,试 leaves:
    • leaf2(无 regex):SetParam(":name","123") → 返回 B ✓(头插优先)
    • 若 leaf2 不存在,leaf1 的 ^[0-9]+$ 命中 → A

请求 /api/list:

  1. "api" → 子树
  1. "list" 精确命中 → C(静态永远优先于参数)

请求 /api/list.json:

  1. "list" 不等(整段是 list.json)
  1. 剥 .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 必须全绿这些用例。