注册流程
我们先来关注注册url的流程,我们以下面的代码为例:
mux := http.NewServeMux()
mux.Handle("GET abc.com/hello/{name}", loggingMiddleware(http.HandlerFunc(helloHandler)))
Handle这个方法主要是区分了一下版本,没有其他处理逻辑,register 是 Handle/HandleFunc 的统一内部入口,当路由发生冲突(歧义)的时候会返回panic。这里ai给的解释是路由冲突属于编程错误,应该在启动阶段暴露,而不是静默忽略。主要的处理在registerErr里面
func (mux *ServeMux) Handle(pattern string, handler Handler) {
if use121 {
mux.mux121.handle(pattern, handler) // 老版本处理逻辑
} else {
mux.register(pattern, handler) // 新的处理逻辑
}
}
func (mux *ServeMux) register(pattern string, handler Handler) {
if err := mux.registerErr(pattern, handler); err != nil {
panic(err)
}
}
registerErr 首先会将url解析为 pattern 结构。
结构如下:
// pattern 表示可以与 HTTP 请求进行匹配的模式。
type pattern struct {
str string // 原始字符串
method string
host string
// 路径的内部表示与表面语法不同,这样可以简化大多数算法。
//
// 以 '/' 结尾的路径用一个匿名的 "..." 通配符表示。
// 例如,路径 "a/" 被表示为字面量段 "a" 后跟一个 multi==true 的段。
//
// 以 "{$}" 结尾的路径用字面量段 "/" 表示。
// 例如,路径 "a/{$}" 被表示为字面量段 "a" 后跟字面量段 "/"。
segments []segment
loc string // 注册调用的源码位置,用于生成有帮助的错误信息
}
type segment struct {
s string // 字面量或通配符名称,或 "/" 表示 "/{$}"
wild bool
multi bool // "..." 通配符
}
以"GET abc.com/hello/{name}"为例,首先会查找第一个空格或者制表符进行拆分,前面的就是method, 然后对剩下的字符串搜索第一个'/',以第一个'/'为间隔,前面的就是host,后面的就是path,
然后把后面 path 路径字符串按 / 切成一段一段,然后判断每段是"固定文字"还是"通配符占位符",分别记录下来,最终组成一个可用于路由匹配的结构体。
具体处理了以下几种情况:
-
路径以
/结尾(如/static/)— 视为"贪婪多段通配符",能匹配这个路径及其所有子路径。 -
普通字面量段(如
/users/list)— 直接做 URL 解码后原样保存,路由匹配时进行精确字符串比较。 -
通配符段(如
{id})— 必须严格用{和}包裹,中间是通配符名。这里还细分了三种情况:{$}:精确匹配到路径末尾,后面不能再有任何路径段(相当于"必须完全到这里结束")。{name...}:贪婪多段通配符,只能放在路径最后,能匹配剩余所有路径段。{name}:普通单段通配符,只匹配一个路径段。
-
重复通配符名检测 — 同一个 pattern 里不能出现两个同名的通配符(如两个
{id}),否则报错。
segment结构的取值有以下几种类型:
wild | multi | 含义 | 示例 |
|---|---|---|---|
false | false | 字面量段,精确匹配 | /users、/list |
false | false | {$} 特殊情况,s 存 "/" 表示路径终止 | /foo/{$} |
true | false | 单段通配符,匹配一个路径段 | {id}、{name} |
true | true | 多段贪婪通配符,匹配剩余所有路径段 | {rest...} 或尾部 / |
补充说明:
{$}比较特殊,wild=false, multi=false,但s被设为"/",用来和普通字面量区分,表示"路径在此精确结束"。- 尾部裸斜杠(如
/static/)和{name...}都会生成wild=true, multi=true的 segment,区别是前者s为空字符串,后者s为通配符名。
冲突检测
接下来我们看一个关键结构routingIndex,用来通过对 pattern 建立索引加快冲突检测
type routingIndex struct {
// segments 是一个从"(段位置, 字面量值)"到"所有在该位置上含有该字面量的 pattern 列表"的映射。
// 例如,key {pos:1, s:"b"} 会索引 "/a/b" 和 "/a/b/c",
// 但不会索引 "/a"、"/b/a"、"/a/c" 或 "/a/{x}"(后者是通配符,不是字面量)。
segments map[routingIndexKey][]*pattern
// multis 存储所有以多段通配符("...")或尾部斜杠结尾的 pattern。
// 对这类 pattern 不做精细索引,因为实际注册的数量通常极少,暴力遍历即可。
multis []*pattern
}
// routingIndexKey 是 segments 索引的键,唯一标识"某个段位置上的某个字面量"。
type routingIndexKey struct {
pos int // 段的位置,从 0 开始计数
s string // 字面量值;若为空字符串,则表示该位置是通配符
}
首先我们介绍三种 pattern 术语:
- dollar pattern: 以
"{$}"结尾,如/a/b/{$},只匹配精确路径末尾的斜杠 - multi pattern: 以尾斜杠或
"{x...}"结尾,可匹配任意多段路径 - ordinary pattern: 普通固定长度 pattern,两者皆非
这个结构的核心逻辑可以这样理解:
- routingIndex 是一个"剪枝索引",注册新 pattern 时不需要跟所有已注册的 pattern 逐一比对,而是先通过索引快速过滤掉"必然不冲突"的那些,只对剩余的"可能冲突"候选者做精确判断。
- segments 字段是剪枝的关键:如果新 pattern 在第 N 段是字面量
"foo",那么任何在第 N 段是字面量但值不是"foo"的 pattern,必然与它互斥,可以直接跳过。 - multis 字段单独处理以
{name...}或尾部/结尾的 pattern,因为这类 pattern 能跨越任意段数,无法用段位置索引来剪枝,但数量通常很少,直接遍历即可。 - routingIndexKey 中 s 为空字符串时表示通配符,这类 pattern 也不参与字面量索引(通配符能匹配任意值,不能用来剪枝)。
结合 possiblyConflictingPatterns 的完整实现,整个冲突检测流程是一个两阶段过滤的设计:
首先进行剪纸,去掉不必要的匹配项目,然后调用传入下面的函数进行真正的匹配,我们将这个函数命名为 f
func(pat2 *pattern) error {
if pat.conflictsWith(pat2) {
d := describeConflict(pat, pat2)
return fmt.Errorf("pattern %q (registered at %s) conflicts with pattern %q (registered at %s):\n%s",
pat, pat.loc, pat2, pat2.loc, d)
}
return nil
}
第一阶段:索引剪枝
目标是快速缩小"候选冲突集合",避免对所有已注册 pattern 做全量扫描。
新 pattern pat
│
├─① 先对所有 multi pattern(以 {x...} 或尾斜杠结尾的)全量调用 f 进行冲突检测
│ └─ 因为 multi 能匹配任意长度路径,无法通过段位置索引剪掉
│
├─② 若新 pat 是 dollar pattern(以 {$} 结尾)
│ └─ 只查索引中"同位置上有 '/' 字面量"的 pattern,其余全部跳过,结束检测
│ (dollar pattern 只与同位置的 dollar/multi 冲突,ordinary 不影响它)
│
└─③ 若 pat 是 ordinary pattern(普通固定长度)\
├─ 遍历 pat 中的每个字面量段,查索引得到两组候选:
│ · lmin = 同位置、同字面量的 pattern 列表
│ · wmin = 同位置、是通配符的 pattern 列表
├─ 取候选总数最少的那个字面量段对应的两组,作为最终候选集
│ (候选越少,后续精确判断的次数越少)
└─ 若 pat 全是通配符(无字面量可剪枝),则对所有已注册 pattern 全量检查
第二阶段:精确判断(conflictsWith)
精准判断就是调用上面的函数f, 对上一阶段筛出的每个候选 pat2 调用:
关键点 conflictsWith 内部的逻辑:
- 若两者 host 不同 → 直接返回
false(不冲突) - 分别比较 method 关系(
compareMethods)和 path 关系(comparePaths) - 用
combineRelationships合并两个维度的关系 - 结果为
equivalent(完全等价)或overlaps(交叉)→ 冲突,否则不冲突
决策树
决策树的示意图如上所示,ai生成的,可能略有瑕疵,但整体上是没问题的
决策树的核心结构是routingNode, 就代表图中的一个一个的节点。
// routingNode 是决策树中的一个节点,叶节点和内部节点共用同一结构体。
//
// - 叶节点(leaf node):pattern 和 handler 均非 nil,代表一条已注册的路由规则。
// - 内部节点(interior node):pattern 和 handler 为 nil,仅作为分支跳转使用。
type routingNode struct {
// A leaf node holds a single pattern and the Handler it was registered
// with.
// 叶节点存储对应的路由 pattern 及其 Handler。
pattern *pattern
handler Handler
// An interior node maps parts of the incoming request to child nodes.
// special children keys:
// "/" trailing slash (resulting from {$})
// "" single wildcard
//
// 内部节点将请求的某一部分映射到子节点。
// children 的特殊 key:
// "/" —— 尾部斜线(来自 pattern 中的 {$})
// "" —— 单段通配符(来自 {name} 形式的通配符)
children mapping[string, *routingNode]
multiChild *routingNode // child with multi wildcard; 多段通配符子节点(来自 {name...})
emptyChild *routingNode // optimization: child with key ""; 单段通配符子节点的快速访问缓存(key 为 "")
}
主要的流程大概就是一层一层添加节点,如果这一层是空或者{id}这种,就添加到emptyChild下面,如果是多端匹配({id ...})就添加到multiChild,其余需要完全匹配的就添加到children这个map里,方便快速查找。
这里需要特殊说明的是,尾部 {$},会按照'/'添加到children里面
举例
以 GET /users/{id} 为例,完整走一遍插入过程。可以从源码registerErr方法的mux.tree.addPattern(pat, handler)这里开始追踪,主要是将 pattern 和对应的 Handler 插入前缀路由树
第一步:入口 addPattern
调用 root.addPattern(p, h),其中 p.host = "",p.method = "GET",p.segments = ["users", "{id}"]。主要会用到以下方法,
// addChild 在节点 n 下查找或创建 key 对应的子节点并返回。
//
// - key == "" 时走 emptyChild 快速路径(单段通配符专用优化)。
// - 否则先在 children 中查找,不存在则新建并追加。
func (n *routingNode) addChild(key string) *routingNode {
if key == "" {
// 空 key 对应单段通配符,使用专用字段以避免 map 查找开销
if n.emptyChild == nil {
n.emptyChild = &routingNode{}
}
return n.emptyChild
}
if c := n.findChild(key); c != nil {
return c
}
c := &routingNode{}
n.children.add(key, c)
return c
}
第二步:建立 host 层(第 1 层)
root.addChild("")
host 是空字符串,走 emptyChild 快速路径。检查 root.emptyChild 是否存在,不存在则新建一个空节点,返回它。现在树是:
root
└── emptyChild ← 当前位置
第三步:建立 method 层(第 2 层)
hostNode.addChild("GET")
key 是 "GET",非空字符串,去 children 这个 mapping 里查找。没有则新建节点,追加到 children 切片(此时元素数 ≤ 8,用切片存)。返回该节点。树变为:
root
└── emptyChild
└── children["GET"] ← 当前位置
第四步:递归处理 path 段,调用 addSegments(["users", "{id}"], p, h)
处理第 1 段 "users"
seg.multi = false,seg.wild = false,是字面量。- 调用
methodNode.addChild("users"),在 children 中新建"users"子节点,返回它。 - 递归调用
usersNode.addSegments(["{id}"], p, h)。
树变为:
root
└── emptyChild
└── children["GET"]
└── children["users"] ← 当前位置
处理第 2 段 "{id}"
seg.multi = false,seg.wild = true,是单段通配符。- 调用
usersNode.addChild("")(空字符串 key),走 emptyChild 快速路径,新建节点,同时把它赋给usersNode.emptyChild,返回它。 - 递归调用
wildcardNode.addSegments([], p, h)。
树变为:
root
└── emptyChild
└── children["GET"]
└── children["users"]
└── emptyChild ← 当前位置(单段通配符 {id})
第五步:addSegments 收到空切片,调用 set(p, h)
segs 为空,说明所有段都已处理完毕,当前节点就是叶节点。调用 wildcardNode.set(p, h):
- 检查
pattern和handler是否已有值(防止重复注册)。 - 写入
wildcardNode.pattern = p,wildcardNode.handler = h。
最终树形状态
root
└── emptyChild (host = "" 通用路由)
└── children["GET"] (method = GET)
└── children["users"] (字面量段 "users")
└── emptyChild (单段通配符 {id})
├── pattern = "GET /users/{id}" ← 叶节点标志
└── handler = handleGetUser