携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第5天,点击查看活动详情
前言
前情提要:
终于,来到路由树构建的最后的一篇了(确信)。本篇文章会把 insertChild 函数讲完,再顺便提一下节点优先级是怎么定的,这样就把整个树的构建讲解完了。让我们继续从示例开始今天的旅程。
insertChild
示例
package main
import (
"fmt"
"log"
"net/http"
"github.com/julienschmidt/httprouter"
)
func Index(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
fmt.Fprint(w, "Welcome!\n")
}
func main() {
router := httprouter.New()
router.GET("/hello/", Index)
router.GET("/hello/:name", Index)
router.GET("/hel/:name/config", Index)
router.GET("/hel/:name/city/*config", Index)
log.Fatal(http.ListenAndServe(":8080", router))
}
概览
源码在这里 ↓
func (n *node) insertChild(numParams uint8, path, fullPath string, handle Handle)
fullPath这个参数仅用于报错。
insertChild 这个函数会在树的叶子节点上继续插入新的路径节点,它不关注也不需要关注前面的路径信息,因为 addRoute 函数已经帮它都处理好了。我们先来看看整个函数大体上都干了什么:
- 函数在开头定义了
offset变量来标记已经处理过的路径位置; - 接下来,它循环遍历整条插入路径并对不同的节点进行处理;
- 由于参数节点比较特殊,所以在遍历路径的过程中,优先查找通配符
:或*的起始以及结束的位置;- 如果没有参数节点,循环结束;
- 如果是命名参数捕获,把前面的路径插入,插入参数节点,继续遍历;
- 如果是任意参数捕获,把前面的路径插入,插入参数节点,直接结束;
- 最后,把剩下的路径插入,handle挂在叶子节点上。
详细分析
router.GET("/hello/", Index)
由于是第一个路径,这里将直接插入。
var offset int
for i, max := 0, len(path); numParams > 0; i++ {
c := path[i]
if c != ':' && c != '*' {
continue // 如果没有匹配到参数节点,这里会一直continue到循环结束
}
// ...
}
// 直接插入 /hello/
n.path = path[offset:]
n.handle = handle
router.GET("/hello/:name", Index)
这里要处理命名捕获参数,它从 : 开始到下一个 / 之前(不包括 / )或者路径结尾结束。
// 这一部分处理是两种参数共用的
c := path[i]
if c != ':' && c != '*' {
continue
}
end := i + 1 // i为0
for end < max && path[end] != '/' {
switch path[end] {
case ':', '*': // 在同一个参数片段中不能包含多个通配符,如::na:me/ 、:na*me
panic("only one wildcard per path segment is allowed, has: '" +
path[i:] + "' in path '" + fullPath + "'")
default:
end++
}
} // end为5
// 能执行到这里,就说明插入路径中有参数,而参数作为子节点时,它的父节点不能有其他子节点
if len(n.children) > 0 {
panic("wildcard route '" + path[i:end] +
"' conflicts with existing children in path '" + fullPath + "'")
}
// 参数必须要有名字
if end-i < 2 {
panic("wildcards must be named with a non-empty name in path '" + fullPath + "'")
}
if c == ':' {
// 判断一下 ':' 前面有没有路径,有就插入
if i > 0 {
n.path = path[offset:i]
offset = i
}
// 创建参数节点并把参数节点挂在父节点下
child := &node{
nType: param,
maxParams: numParams,
}
n.children = []*node{child}
n.wildChild = true
n = child
n.priority++
numParams--
// 如果后面还有路径,这里就需要把参数节点的路径完成,并创建新节点,然后继续遍历
if end < max {
n.path = path[offset:end]
offset = end
child := &node{
maxParams: numParams,
priority: 1,
}
n.children = []*node{child}
n = child
}
}
router.GET("/hel/:name/config", Index)
这个是给 if end < max 代码块写的示例,它的流程就是:
-
要插入的路径是
/:name/config; -
先匹配到
:的位置和它后面第一个/的位置; -
把
:前面的/插入; -
发现
:name后面还有路径(有/就是有路径,只有一个/也是路径),把参数节点补充完整并创建新节点; -
后面没有参数了,遍历到路径结尾结束循环,执行最后两行代码直接插入
/config。 -
n.path = path[offset:] n.handle = handle
此时,完整的路由树是这样的:
- "/hel" indices: "l/"
- "lo/"
- ":name"
- "/"
- ":name"
- "/config"
router.GET("/hel/:name/city/*config", Index)
这里要插入的路径是 ity/*config 。
else {
// 任意捕获必须为路径的最后一个片段,它后面不能再有路径了
if end != max || numParams > 1 {
panic("catch-all routes are only allowed at the end of the path in path '" + fullPath + "'")
}
// 如果
if len(n.path) > 0 && n.path[len(n.path)-1] == '/' {
panic("catch-all conflicts with existing handle for the path segment root in path '" + fullPath + "'")
}
// 任意捕获节点前需要有 '/'
i--
if path[i] != '/' {
panic("no / before catch-all in path '" + fullPath + "'")
}
// 插入'/'之前的路径
n.path = path[offset:i]
// 插入一个路径为空的节点,指示下面有任意捕获节点
child := &node{
wildChild: true,
nType: catchAll,
maxParams: 1,
}
if n.maxParams < 1 {
n.maxParams = 1
}
n.children = []*node{child}
n.indices = string(path[i])
n = child
n.priority++
// 这个节点是真正存储参数变量的节点
child = &node{
path: path[i:],
nType: catchAll,
maxParams: 1,
handle: handle,
priority: 1,
}
n.children = []*node{child}
return // 后面不可能有路径了,直接返回
}
这部分路径关系应该是这样的:
- "ity" indices: "/"
- ""
- "/*config"
小结
insertChild 函数终于分析完了,路由构建的部分也即将迎来尾声,我们先把这一部分小结一下。
整体上来说,insertChild 代码写得非常干净,逻辑顺畅,所以理解起来也比较容易。整个函数只负责一件事,就是在同一条节点链上依次去插入每个节点。代码在编写时,考虑到节点参数的特殊性,用参数片段将路径切分,并顺序插入到路由树中。此外,函数对于各种可能发生的异常情况也都提供了完整清晰的报错信息。
incrementChildPrio
在进入路由服务之前,让我们把剩下的节点优先级排序讲完。
树的每个层上的子节点会按照优先级排序,节点的优先级仅仅是该节点下面的所有子节点(儿子、孙子等等)的 handle 总数量。这在以下两个方面有所帮助:
-
优先匹配属于大多数路由路径的节点。这有助于使尽可能多的路线能够尽快到达。
-
这是某种成本补偿。总是可以优先匹配最长的可到达路径(最高的成本)。
对于优先级的设置,有一部分掺杂在前面所讲的两个函数的代码中。还有一部分是在插入过程中,遇到有多个子节点时,在增加其中一个节点优先级后,要对它们的优先级进行重排序的函数。
我们来看看代码:
func (n *node) incrementChildPrio(pos int) int {
n.children[pos].priority++ // 需要增加优先级的子节点
prio := n.children[pos].priority
// 调整子节点间的位置顺序(优先级高的排在前面)
newPos := pos // 拷贝当前子节点的位置
for newPos > 0 && n.children[newPos-1].priority < prio {
// 如果当前节点的优先级比前一个节点高,则交换两者位置
n.children[newPos-1], n.children[newPos] = n.children[newPos], n.children[newPos-1]
newPos-- // 交换后,当前节点位置减一
}
// 更改索引顺序
if newPos != pos {
n.indices = n.indices[:newPos] + // 没有变化的部分,可能为空
n.indices[pos:pos+1] + // 正在移动的子节点的索引
n.indices[newPos:pos] + // 被往后挪的部分
n.indices[pos+1:] // 剩余的部分
}
return newPos // 返回新的位置索引
}
总结
至此,所有路由树构建的部分就讲完了,撒花 ✿✿ヽ(°▽°)ノ✿ !下一篇中,我们将开启路由服务部分的旅途,分析 HttpRouter 是如何匹配请求路径并从中获取到参数信息的。
如果本篇文章对你有帮助的话,(或者觉得作者打这么多字挺辛苦的话),不要忘记点个赞哦,感谢支持~