Echo 源码解析 - 路由注册与路由查询

342 阅读17分钟

前言

之前做了 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 的代码更加易读一些,当然它们的实现都很巧妙。 以上,希望本文对各位有所帮助。

Gin 源码解析