前言
之前做了 Gin 关于路由源码的解析,这次我们来看看经常与之一起出现的另一个框架 Echo,Echo 也是一个功能强大且用途广泛的 Web 框架,这两者的定位非常相似,同样具有简洁,高性能,灵活等特性,在官方的宣称中 Echo 的性能甚至要比 gin 更高,所以这次来进行 Echo 框架关于路由部分的解析,以及与 Gin 的实现上会有什么区别,关于 Gin 的源码解析可查看这里:Gin 源码解析。
与 gin 的路由算法类似,ehco 的路由算法也采用了 radix tree 的算法,在这里还是在简述一下有关 tire tree 与 radix tree 的概念,简单来说,radix tree 是 tire tree 的改版,tire tree,也叫“前缀树”或者“字典树”,它是一个树形结构,专门用于处理字符串匹配,用来解决在一组字符串集合中快速查找某个字符串的问题,比如以下是一颗 tire tree:
这棵树中保存了8个键:“A”,“to”,“tea”,“ted”,“ten”,“i”,“in”,“inn”。
而 radix tree,基数树,是 tire tree 的改版,基数树会合并那些只有一个子节点的路径,从而减少树的高度和节点的数量。在实现上,radix 树更加复杂,但是减少了内存的使用,并提高了查询效率。
比如以下是一颗 radix tree:
这颗树中保存了7个键:“romane”,“romanus”,“romulus”,“rubens”,“ruber”,“rubicon”,“rubicundus”。
关于这两种树在这里只做简单描述,接下来进入正题。
路由注册
首先,在 Echo 中注册路由的伪代码如下图所示:
func routers(group *echo.Group) {
testGroup := group.Group("test")
testGroup.GET("/success", success)
testGroup.GET("/500", success)
testGroup.GET("/400", success)
testGroup.GET("/401", success)
testGroup.GET("/403", success)
testGroup.GET("/panic", success)
testGroup.GET("/query", success)
testGroup.GET("/path/:id", success)
testGroup.GET("/action/*", success)
}
假如我们注册以上路由,那么在 Echo 中就生成如下图所示的路由树(简易版本):
先从 GET 方法开始,GET 方法在 group.go 文件中,是路由注册的入口:
// 注册 GET 路由
func (g *Group) GET(path string, h HandlerFunc, m ...MiddlewareFunc) *Route {
return g.Add(http.MethodGet, path, h, m...)
}
// 为所有 method 注册路由
func (g *Group) Any(path string, handler HandlerFunc, middleware ...MiddlewareFunc) []*Route {
routes := make([]*Route, len(methods)) // 初始化 method 树切片
for i, m := range methods {
routes[i] = g.Add(m, path, handler, middleware...) // 所有 method 都会注册一次路由
}
return routes
}
methods 的列表如下:
var methods = [...]string{
http.MethodConnect,
http.MethodDelete,
http.MethodGet,
http.MethodHead,
http.MethodOptions,
http.MethodPatch,
http.MethodPost,
PROPFIND,
http.MethodPut,
http.MethodTrace,
REPORT,
}
接着进入 add 方法,add 方法合并 handler 与 group 的中间件,然后调用真正的路由添加方法,g.echo.add:
func (g *Group) Add(method, path string, handler HandlerFunc, middleware ...MiddlewareFunc) *Route {
// 合并路由组的中间件到欲添加的中间件列表中
m := make([]MiddlewareFunc, 0, len(g.middleware)+len(middleware))
m = append(m, g.middleware...)
m = append(m, middleware...)
// 执行路由添加操作
return g.echo.add(g.host, method, g.prefix+path, handler, m...)
}
进入 add 方法之前,先看看 Echo 关于树结构的定义,其中 router 属性代表无指定 host 的路由树,是默认的路由树,routers 是一个 map,存储的是指定了特定 host 的路由树,与 gin 不同的是,gin 会将不同的 method 的路由分别存储在一棵树中,而 ehco 的所有路由都在一棵树中,代码中 Router,Route,routeMethods 是路由树相关的结构,后面讲解:
echo.New()
// 创建 echo 实例
func New() (e *Echo) {
// 省略部分代码
e.router = NewRouter(e) // 初始化默认路由树
e.routers = map[string]*Router{}
return
}
type Echo struct {
// 省略部分代码
router *Router // 无 host 的路由树,默认的路由树
routers map[string]*Router // 特定 host 的路由树,这个是当使用 echo.Host() 创建指定 host 的 group 时,group 的路由树会创建在这里
}
func NewRouter(e *Echo) *Router {
return &Router{
tree: &node{
methods: new(routeMethods),
},
routes: map[string]*Route{},
echo: e,
}
}
进入 add 方法,首先查询特定 host 的路由树,否则使用路由树,封装 handler,然后调用 router.add 方法添加路由树节点:
func (e *Echo) add(host, method, path string, handler HandlerFunc, middlewares ...MiddlewareFunc) *Route {
router := e.findRouter(host) // 查询路由树,会先根据 host 查询特定 host 下的路由树,否则则返回默认路由树
name := handlerName(handler) // 反射获取 handler 方法的名称
// 添加到路由树中,添加到路由树的 handler 是一个 handler wrapper,它会先依次执行 handler 上的 middleware,再执行 handler
route := router.add(method, path, name, func(c Context) error {
h := applyMiddleware(handler, middlewares...) // 执行 middlewares
return h(c)
})
// 添加路由时的回调函数
if e.OnAddRouteHandler != nil {
e.OnAddRouteHandler(host, *route, handler, middlewares)
}
return route
}
进入 router.add 方法之前,先看看在 router.go 文件中路由树相关的结构定义,Router 为路由树的定义,包含根节点,树上已注册的路由信息和 Echo 实例,其中关于树节点的结构定义与 gin 有较大区别,在 ehco 的树节点定义中,methods 注册在该节点上的各种路由 method,其子节点也分为三个列表存储:静态子节点,: 通配符子节点,* 通配符子节点:
// 路由树
type Router struct {
tree *node // 树节点
routes map[string]*Route // 已注册的路由信息,包括 method,路由路径,名称
echo *Echo
}
// 路由树节点
type node struct {
methods *routeMethods // 注册在该节点上的各种路由方法
parent *node // 父节点
paramChild *node // :path 通配符子节点
anyChild *node // * 通配符子节点
notFoundHandler *routeMethod // 使用 echo.RouteNotFound 注册的 404 handler
prefix string // 当前节点的路由
originalPath string // 完整路径,只有叶子节点才会有值
staticChildren children // 静态路由子节点
paramsCount int // 通配符路由参数数量
label byte // 路由第一个字符的 ascii 码值
kind kind // 节点类型
isLeaf bool // 是否是叶子节点,叶子节点下没有子节点
isHandler bool // 是否被标记为路由注册节点
}
// method 路由信息
type routeMethod struct {
handler HandlerFunc
ppath string // 路由路径
pnames []string // 名称
}
// 同一个路由的不同 method
type routeMethods struct {
connect *routeMethod
delete *routeMethod
get *routeMethod
head *routeMethod
options *routeMethod
patch *routeMethod
post *routeMethod
propfind *routeMethod
put *routeMethod
trace *routeMethod
report *routeMethod
anyOther map[string]*routeMethod
allowHeader string
}
接下来进入 add 方法,add 先简单地格式化路由,然后调用 insert 方法插入节点,insert 首先判断插入节点的类型,即静态与两种通配符类型,然后调用 insertNode 方法执行节点插入操作:
func (r *Router) add(method, path, name string, h HandlerFunc) *Route {
path = normalizePathSlash(path) // 格式化路由,添加 '/'
r.insert(method, path, h) // 添加节点
route := &Route{
Method: method,
Path: path,
Name: name,
}
r.routes[method+path] = route // 将路由信息注册到 reoutes map 中
return route
}
func (r *Router) insert(method, path string, h HandlerFunc) {
path = normalizePathSlash(path) // 格式化路由
pnames := []string{} // Param names
ppath := path // Pristine path
if h == nil && r.echo.Logger != nil {
// FIXME: in future we should return error
r.echo.Logger.Errorf("Adding route without handler function: %v:%v", method, path)
}
// for 循环检测欲添加路由是否包含通配符
for i, lcpIndex := 0, len(path); i < lcpIndex; i++ {
if path[i] == ':' { // :通配符节点
if i > 0 && path[i-1] == '\\' { // 去掉通配符前 '\' 的字符
path = path[:i-1] + path[i:]
i--
lcpIndex--
continue
}
j := i + 1 // 记录通配符后加 1 的位置
r.insertNode(method, path[:i], staticKind, routeMethod{}) // 将通配符前部分添加为静态节点
for ; i < lcpIndex && path[i] != '/'; i++ { // 将 i 置为通配符后下一个不等于 '/' 的字符的位置,即找到 : 通配符下一个路由块之前的位置
}
pnames = append(pnames, path[j:i]) // 添加通配符参数
path = path[:j] + path[i:] // 欲添加的通配符路由块
i, lcpIndex = j, len(path)
if i == lcpIndex { // i,通配符后下一个路由块之前的位置,如果等于欲添加的路由块长度,说明通配符是最后一个路由块,case: /user/:id
// 添加为 paramKind 节点,routeMethod 赋值,说明添加的节点为叶子节点
r.insertNode(method, path[:i], paramKind, routeMethod{ppath: ppath, pnames: pnames, handler: h})
} else {
// 通配符路由块并非在最后,添加为 paramKind 节点,routeMethod 不赋值,说明添加的节点不为叶子节点
r.insertNode(method, path[:i], paramKind, routeMethod{})
}
} else if path[i] == '*' { // * 通配符
r.insertNode(method, path[:i], staticKind, routeMethod{}) // 将通配符前部分添加为静态节点,routeMethod 不赋值,说明添加的节点不为叶子节点
pnames = append(pnames, "*") // 添加通配符参数
r.insertNode(method, path[:i+1], anyKind, routeMethod{ppath: ppath, pnames: pnames, handler: h}) // 将通配符后部分添加为 anyKind 节点,routeMethod 赋值,说明添加的节点为叶子节点
}
}
// 不包含通配符,添加为静态节点,routeMethod 赋值,说明添加的节点为叶子节点
r.insertNode(method, path, staticKind, routeMethod{ppath: ppath, pnames: pnames, handler: h})
}
insertNode 方法是节点插入的方法,其中 search 为当前欲添加的路由,通过一个 for 循环不断重置 search 的值来完成节点插入和节点分裂的操作:
func (r *Router) insertNode(method, path string, t kind, rm routeMethod) {
// 重新赋值最大的通配符参数数
paramLen := len(rm.pnames)
if *r.echo.maxParam < paramLen {
*r.echo.maxParam = paramLen
}
currentNode := r.tree
if currentNode == nil {
panic("echo: invalid method")
}
search := path
for {
searchLen := len(search) // 欲添加路由的长度
prefixLen := len(currentNode.prefix) // 当前节点的路由长度
lcpLen := 0 // Longest Common Prefix,最长相同前缀
max := prefixLen
if searchLen < max {
max = searchLen
}
for ; lcpLen < max && search[lcpLen] == currentNode.prefix[lcpLen]; lcpLen++ { // 执行循环后,lcpLen 的位置为欲添加路由与当前节点路由的最长相同前缀的位置
}
if lcpLen == 0 { // lcpLen 等于 0 说明没有相同前缀
// 节点属性赋值
currentNode.label = search[0]
currentNode.prefix = search
if rm.handler != nil { // 如果 handler 不为空,说明该节点被标记为路由注册节点
currentNode.kind = t
currentNode.addMethod(method, &rm)
currentNode.paramsCount = len(rm.pnames)
currentNode.originalPath = rm.ppath
}
// 如果当前节点没有任何子节点,标记为叶子节点
currentNode.isLeaf = currentNode.staticChildren == nil && currentNode.paramChild == nil && currentNode.anyChild == nil
} else if lcpLen < prefixLen { // lcpLen 小于当前节点的路由,说明存在小于当前节点路由的相同前缀,需要分裂节点
// case:当前节点路由 /test,欲添加路由 /te/*
// 1. 当前节点置为 /te 节点
// 2. 分裂 /st 节点为子节点
// 3. 分裂 /* 节点为子节点
// 创建一个新节点作为子节点,即 /st 节点
n := newNode(
currentNode.kind,
currentNode.prefix[lcpLen:], // 取相同前缀后部分,即 /st
currentNode, // 父节点节点设为当前节点
currentNode.staticChildren,
currentNode.originalPath,
currentNode.methods,
currentNode.paramsCount,
currentNode.paramChild,
currentNode.anyChild,
currentNode.notFoundHandler,
)
// 更新当前节点子节点的父节点,即将已注册的 /test 下的子节点置为 /st 节点的子节点
for _, child := range currentNode.staticChildren {
child.parent = n
}
// 同理更新 paramChild 通配符子节点
if currentNode.paramChild != nil {
currentNode.paramChild.parent = n
}
// 同理更新 anyChild 通配符子节点
if currentNode.anyChild != nil {
currentNode.anyChild.parent = n
}
// 重置当前节点,即重置为 /te 节点
currentNode.kind = staticKind
currentNode.label = currentNode.prefix[0]
currentNode.prefix = currentNode.prefix[:lcpLen] // 取相同前缀部分
currentNode.staticChildren = nil
currentNode.originalPath = ""
currentNode.methods = new(routeMethods)
currentNode.paramsCount = 0
currentNode.paramChild = nil
currentNode.anyChild = nil
currentNode.isLeaf = false
currentNode.isHandler = false
currentNode.notFoundHandler = nil
// 添加创建的节点为静态子节点,即 /te 节点添加子节点 /st
// 这里只添加静态节点,如果路由包含通配符,也会分裂为一个静态节点和通配符节点,这里是添加静态节点
currentNode.addStaticChild(n)
// lcpLen 等于欲添加路由长度,则将分裂完成的当前节点标记为路由注册节点
// case:当前节点路由 /test,欲添加路由 /te,则当前节点 /te 标记为路由注册节点
if lcpLen == searchLen {
currentNode.kind = t
if rm.handler != nil { // 如果 handler 不为空,说明该节点被标记为路由注册节点
currentNode.addMethod(method, &rm) // 判断是哪个 method 的路由
currentNode.paramsCount = len(rm.pnames)
currentNode.originalPath = rm.ppath
}
} else { // lcpLen 不等于欲添加路由长度,创建新节点作为当前节点的子节点
// case:当前节点路由 /test,欲添加路由 /te/*,这步是创建 /* 节点,作为 /te 的子节点
n = newNode(t, search[lcpLen:], currentNode, nil, "", new(routeMethods), 0, nil, nil, nil)
if rm.handler != nil { // 如果 handler 不为空,说明该节点被标记为路由注册节点
n.addMethod(method, &rm)
n.paramsCount = len(rm.pnames)
n.originalPath = rm.ppath
}
// 添加子节点
// 这里只添加静态节点,如果路由包含通配符,也会分裂为一个静态节点和通配符节点,这里是添加静态节点,然后进入下一轮 for 循环
currentNode.addStaticChild(n)
}
currentNode.isLeaf = currentNode.staticChildren == nil && currentNode.paramChild == nil && currentNode.anyChild == nil
} else if lcpLen < searchLen { // 最大相同前缀不小于当前节点路由的相同前缀,且小于欲添加路由
// case:已注册/test,欲添加 /test/*
search = search[lcpLen:]
// 查询是否存在子节点
c := currentNode.findChildWithLabel(search[0])
if c != nil {
// Go deeper
currentNode = c // 如果存在,进行新一轮 for 循环
continue
}
// 不存在子节点,创建新节点
n := newNode(t, search, currentNode, nil, rm.ppath, new(routeMethods), 0, nil, nil, nil)
if rm.handler != nil {
n.addMethod(method, &rm)
n.paramsCount = len(rm.pnames)
}
// 根据节点类型添加节点
switch t {
case staticKind:
currentNode.addStaticChild(n)
case paramKind:
currentNode.paramChild = n
case anyKind:
currentNode.anyChild = n
}
currentNode.isLeaf = currentNode.staticChildren == nil && currentNode.paramChild == nil && currentNode.anyChild == nil
} else {
// 否则说明节点已经存在,标记为注册路由节点
if rm.handler != nil {
currentNode.addMethod(method, &rm)
currentNode.paramsCount = len(rm.pnames)
currentNode.originalPath = rm.ppath
}
}
return
}
}
路由查询
以上完成了 Echo 路由注册的解析,接下来开始路由查询,首先从请求开始,Echo 接收请求的入口从 go sdk 中,http.Server.Server 方法开始,调用 echo.Echo.ServerHTTP 方法进入,首先查找路由节点是否存在,如果存在,在执行目标 handler 之前,依次执行中间件,然后执行 handler 响应结果:
func (e *Echo) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Acquire context
c := e.pool.Get().(*context)
c.Reset(r, w)
var h HandlerFunc
// premiddleware 是 echo 设置的在进行路由查询之前需要执行的 middleware 列表
// 如果 premiddleware 不为空,那么先将 premiddleware 列表加入待执行 middleware 列表的头部,再依次执行 middleware 列表
if e.premiddleware == nil {
e.findRouter(r.Host).Find(r.Method, GetPath(r), c) // 路由查询
h = c.Handler()
h = applyMiddleware(h, e.middleware...) // 依次执行 middleware 列表
} else {
h = func(c Context) error {
e.findRouter(r.Host).Find(r.Method, GetPath(r), c) // 路由查询
h := c.Handler()
h = applyMiddleware(h, e.middleware...) // 依次执行 middleware 列表
return h(c)
}
h = applyMiddleware(h, e.premiddleware...) // premiddleware 加入 middleware 列表,并执行
}
// 执行 handler
if err := h(c); err != nil {
e.HTTPErrorHandler(err, c)
}
// Release context
e.pool.Put(c)
}
func applyMiddleware(h HandlerFunc, middleware ...MiddlewareFunc) HandlerFunc {
for i := len(middleware) - 1; i >= 0; i-- {
h = middleware[i](h)
}
return h
}
查找路由节点的关键方法是 findRouter 方法,findRouter 方法在 router.go 文件中,其中主要实现了路由节点的查找操作以及路由匹配失败时的处理,路由匹配按照静态,: 通配符,* 通配符顺序匹配,方法中也将代码从上到下分为这三个部分,并标记为代码块标签,此部分代码比较长且复杂,会一直跳到循环中的不同部分,需要耐心阅读:
func (r *Router) Find(method, path string, c Context) {
ctx := c.(*context)
currentNode := r.tree // Current node as root
var (
previousBestMatchNode *node // 可能会匹配的节点
matchedRouteMethod *routeMethod // 匹配到的 method
search = path // 欲查询路由
searchIndex = 0 // 欲查询路由的索引
paramIndex int // Param counter
paramValues = ctx.pvalues // Use the internal slice so the interface can keep the illusion of a dynamic slice
)
// 回溯节点,当路由树遍历到叶子节点时,需要回溯节点,回溯时,按照叶子节点的类型 static, param,any 类型的顺序循环遍历下一个节点
backtrackToNextNodeKind := func(fromKind kind) (nextNodeKind kind, valid bool) {
previous := currentNode // 将当前节点保留为上一个节点
currentNode = previous.parent // 当前节点回溯到父节点
valid = currentNode != nil // 回溯的父节点不能为空
// 如果当前节点类型是 * 通配符,则下一个是静态类型,如果是 : 通配符类型,则下一个是 * 通配符,如果是静态类型,则下一个是 : 通配符
if previous.kind == anyKind {
nextNodeKind = staticKind
} else {
nextNodeKind = previous.kind + 1
}
if fromKind == staticKind { // fromKind 是上一个尝试匹配的节点类型,如果它是静态类型,那么决定好 nextNodeKind 之后,就可以结束,因为静态节点是最优先匹配的,无需其他修改
return
}
if previous.kind == staticKind {
searchIndex -= len(previous.prefix) // 如果当前节点是静态节点,重置索引到当前节点的路由
} else {
paramIndex--
// 对于上一次匹配是通配符的匹配的情况下,重置欲查询的路由与索引,即重置到通配符参数前的位置(:param / * 的位置)
searchIndex -= len(paramValues[paramIndex])
paramValues[paramIndex] = ""
}
search = path[searchIndex:] // 重置欲查询路由
return
}
for {
prefixLen := 0 // 当前节点路由长度
lcpLen := 0 // 最大相同前缀长度
if currentNode.kind == staticKind { // 如果当前节点是静态节点,计算出最大相同前缀长度
searchLen := len(search)
prefixLen = len(currentNode.prefix)
max := prefixLen
if searchLen < max {
max = searchLen
}
for ; lcpLen < max && search[lcpLen] == currentNode.prefix[lcpLen]; lcpLen++ {
}
}
if lcpLen != prefixLen { // 由于分裂节点的存在,静态节点中,如果拥有相同前缀,那么 lcpLen 和 prefixLen 会是相等的,如果是通配符节点,lcpLen 和 prefixLen 都是 0,也相等,所以如果 lcpLen 和 prefixLen 不相等,说明路由匹配不会涉及通配符匹配且静态路由不匹配
// case:注册 /test,查询 /tes
nk, ok := backtrackToNextNodeKind(staticKind) // backtrackToNextNodeKind 方法用于决定是否还有下一个回溯节点以及下一个匹配的节点类型 static > param > any
if !ok {
return // 已经回溯到根节点,路由匹配失败,返回
} else if nk == paramKind { // 下一个匹配类型是 : 通配符,跳到相应模块
goto Param
// NOTE: this case (backtracking from static node to previous any node) can not happen by current any matching logic. Any node is end of search currently
//} else if nk == anyKind {
// goto Any
} else {
// Not found (this should never be possible for static node we are looking currently)
break
}
}
// 到这里说明欲查询路由相同前缀与当前节点路由一致,重置欲查询路由与其索引
search = search[lcpLen:]
searchIndex = searchIndex + lcpLen
if search == "" { // 说明欲查询路由已经刚好等于当前节点路由,路由匹配(但是 method 不一定匹配)
if currentNode.isHandler { // 如果该节点被标记为已注册路由
if previousBestMatchNode == nil { // 赋值当前节点为可能匹配的节点
previousBestMatchNode = currentNode
}
if h := currentNode.findMethod(method); h != nil { // 查询该节点是否注册了对应的 method 路由,如果没找到,说明 method 无法匹配
matchedRouteMethod = h // 查询到 method,说明路由已找到,退出
break
}
} else if currentNode.notFoundHandler != nil {
// 该节点没有标记为已注册路由且 notFoundHandler 非空(echo.RouteNotFound 可以为指定路由注册 notFoundHandler),那么将 notFoundHandler 赋值节点匹配 method,即 404 的处理 handler,并退出
matchedRouteMethod = currentNode.notFoundHandler
break
}
}
// 如果欲查询路由除了相同前缀后还有一段,查询是否存在静态子节点
// 静态节点中,search 不为空的情况是为了匹配分裂节点,比如注册了 /test 和 /tes,查询 /test 时匹配 /tes 节点,直到 search 为空
if search != "" {
if child := currentNode.findStaticChild(search[0]); child != nil {
currentNode = child // 如果存在静态子节点,设置为当前节点,进入下一轮路由匹配,找不到静态子节点说明它可能有通配符子节点,接着往下
continue
}
}
// 会到这里的情况是,匹配了路由,但是没找到匹配的 method,静态路由匹配失败,接下来进入 : 通配符的匹配
// 此时的欲查询路由可能是空也可能不为空,不为空的情况是没找到匹配的 method
Param:
if child := currentNode.paramChild; search != "" && child != nil { // 在当前节点拥有 param 子节点与欲查询路由不为空(有参数要匹配)的情况下才会执行
currentNode = child
i := 0
l := len(search)
if currentNode.isLeaf {
// 如果通配符的路由块在最后,那么它与 * 通配符一致,即会匹配之后所有路由路径
// case:/test/:param
i = l // i = l 会使 search 被裁剪为空,search 为空说明路由匹配
} else {
// 不是叶子节点说明还有子节点,需要裁剪出剩余部分继续匹配
// case:/test/:param/test
for ; i < l && search[i] != '/'; i++ {
}
}
paramValues[paramIndex] = search[:i]
paramIndex++
search = search[i:]
searchIndex = searchIndex + i
continue
}
Any:
if child := currentNode.anyChild; child != nil { // 在当前节点拥有 any 子节点的情况下才会执行
currentNode = child
paramValues[currentNode.paramsCount-1] = search // 将剩余部分作为入参
// update indexes/search in case we need to backtrack when no handler match is found
paramIndex++
searchIndex += +len(search)
search = "" // 设置 search 为空,search 为空说明路由匹配
if h := currentNode.findMethod(method); h != nil { // 是否能找到匹配 method
matchedRouteMethod = h
break // 找到则退出
}
// 找不到匹配的 method,那么将当前节点做为可能匹配的节点
if previousBestMatchNode == nil {
previousBestMatchNode = currentNode
}
if currentNode.notFoundHandler != nil {
matchedRouteMethod = currentNode.notFoundHandler
break
}
}
// 当前节点经过通配符的匹配仍匹配不到(或者不满足条件匹配),尝试回溯父节点进行对应模块的匹配
nk, ok := backtrackToNextNodeKind(anyKind)
if !ok {
break // 无回溯节点,匹配失败
} else if nk == paramKind {
goto Param
} else if nk == anyKind {
goto Any
} else {
// Not found
break
}
}
// 所有回溯完毕后匹配不到,则匹配失败
if currentNode == nil && previousBestMatchNode == nil {
return // nothing matched at all
}
var rPath string
var rPNames []string
if matchedRouteMethod != nil { // matchedRouteMethod 不为空第一种情况是在之前的 for 循环中匹配到 method,第二种情况是为特定的路由指定了 notFoundHandler,此 handler 优先级较高,以此 handler 返回(如果注册了 /* 的 notFoundHandler 那么该节点也会被回溯匹配到),否则就会采用通用的 notFoundHandler
rPath = matchedRouteMethod.ppath
rPNames = matchedRouteMethod.pnames
ctx.handler = matchedRouteMethod.handler
} else { // 这里是匹配了路由但是没匹配到方法的情况
currentNode = previousBestMatchNode
rPath = currentNode.originalPath
rPNames = nil // no params here
ctx.handler = NotFoundHandler // 使用通用的 NotFoundHandler
if currentNode.notFoundHandler != nil {
rPath = currentNode.notFoundHandler.ppath
rPNames = currentNode.notFoundHandler.pnames
ctx.handler = currentNode.notFoundHandler.handler // 如果在上面的匹配中匹配到了 notFoundHandler,那么优先使用
} else if currentNode.isHandler {
ctx.Set(ContextKeyHeaderAllow, currentNode.methods.allowHeader)
ctx.handler = MethodNotAllowedHandler
if method == http.MethodOptions {
ctx.handler = optionsMethodHandler(currentNode.methods.allowHeader)
}
}
}
ctx.path = rPath
ctx.pnames = rPNames
}
最后
以上就是 Echo 中关于路由部分源码的解析,其路由算法的查找时间复杂度为 O(n),n 为欲查找路由的长度,这点与 Gin 类似;在 Gin 与 Echo 源码的解析对比中,个人觉得 Gin 的代码更加易读一些,当然它们的实现都很巧妙。 以上,希望本文对各位有所帮助。