gin 路由源码完全解析

741 阅读4分钟

源码版本b4c8bf1,与你们看的可能有出入,需要注意!

参数解析

主要的结构体如下

type node struct {
   path      string
   indices   string
   children  []*node
   handlers  HandlersChain
   priority  uint32
   nType     nodeType
   maxParams uint8
   wildChild bool
   fullPath  string
}

path表示的是当前节点在完整的uri中的子串。

indices和子节点相关,是每个子节点的path的第一个字符组成的串,字符顺序是由每个子节点的priority决定的。

children是子节点,每个元素都是*node类型,顺序也是由每个子节点的priority决定。

nType是当前node的类型,有下面四种类型:

  1. static是普通的字符串,也是默认的类型
  2. root就和名字一样了,是根节点。每个HTTP请求方法都会有一个根节点。
  3. param是参数类型,例如/user/:name中的name
  4. catchAll也是参数类型,例如/user/*namename可以是任何字符串。不过这种类型只能出现在uri中的最后位置
const (
	static nodeType = iota // default
	root
	param
	catchAll
)

wildChild表示的是子节点是否是参数类型,就是上面的param或者catchAll

priority含义是子节点的个数,一个节点的子节点个数越多,优先级越高,其path的第一个字符在父节点的indices的位置就越靠前,在其父节点的children也越靠前。

这种子节点根据priority排序比较合理,因为如果每个接口被访问的可能性相同的话,优先比较子节点多的节点,会比较早的匹配。比如说字符a开头的uri模式有10个,b开头的uri模式有1个,那么对于所有访问的 uri 中是a的可能性比较大。

fullPath:如果在一个节点产生过分叉,则fullPath就是所有子节点路径的共同前缀。如果在一个节点没有产生过分叉,则fullPath就是第一个插入节点的全长路径。(todo: 查看使用时候的作用)

方法解析

整体的插入

要插入的路径为path,当前的节点为n,现在需要的是把path在节点n中插入到合适的位置。所以需要找到pathn.path的最长公用前缀,用的方式如下,返回的i表示的是n.path[:i]path[:i]相同。

func longestCommonPrefix(a, b string) int {
   i := 0
   max := min(len(a), len(b))
   for i < max && a[i] == b[i] {
      i++
   }
   return i
}

如果返回的索引位置小于n.path,那么则会把n.path分割为两部分。分割后的n.path就只剩下和path相同的部分了。如果不小于则不需要分割了,因为n.path就是path的前缀了。

child := node{
   // path 会分割为两个部分,子节点的部分为 n.path[i:]
   path:      n.path[i:],
   wildChild: n.wildChild,
   indices:   n.indices,
   children:  n.children,
   handlers:  n.handlers,
   // 由于在给当前节点的添加 path 的时候,就增加了节点 n 的 priority
   // 但是呢,path 并没有添加到这个 child 之中,所以其 priority 需要调整回去
   priority:  n.priority - 1,
   fullPath:  n.fullPath,
}

// Update maxParams (max of all children)
for _, v := range child.children {
   if v.maxParams > child.maxParams {
      child.maxParams = v.maxParams
   }
}

n.children = []*node{&child}
n.indices = string([]byte{n.path[i]})
// 父节点部分的 path
n.path = path[:i]
n.handlers = nil
n.wildChild = false
n.fullPath = fullPath[:parentFullPathIndex+i]

如果返回的索引值小于path,也会把path分割,不相同的部分要继续寻找合适的位置了。这个时候要考虑的情况就比较多了。

情况1:如果节点n的子节点是一个参数节点(wildcard),则表示节点n只有一个节点,其类型是param或者catchAllpath的后续部分必须也是同样的参数类型,否则这两种类型就互相冲突了。假如path后续部分不是一个参数类型,那么就会出现一个位置上有两种类型了,比如说'/user/name'和'/user/:param',这两种模式是相互冲突的。具体到代码里面就如下了

parentFullPathIndex += len(n.path)
n = n.children[0]
n.priority++
// Update maxParams of the child node
if numParams > n.maxParams {
   n.maxParams = numParams
}
numParams--
// Check if the wildcard matches
if len(path) >= len(n.path) && n.path == path[:len(n.path)] {
   // check for longer wildcard, e.g. :name and :names
   // n.path == path 或者 path 中和n.path不同部分的第一个字符是'/'
   if len(n.path) >= len(path) || path[len(n.path)] == '/' {
      continue walk
   }
}
pathSeg := path
if n.nType != catchAll {
   pathSeg = strings.SplitN(path, "/", 2)[0]
}
prefix := fullPath[:strings.Index(fullPath, pathSeg)] + n.path
panic("'" + pathSeg +
   "' in new path '" + fullPath +
   "' conflicts with existing wildcard '" + n.path +
   "' in existing prefix '" + prefix +
   "'")

情况2:节点n是一个param类型,并且第一个不同的字符是/,并且只有一个子节点。参数类型子节点的情况只有两种,一种是没有子节点,一种是一个子节点。其实情况2,就是从情况1来的。

情况3:检查当前节点的子节点中,是否有一个子节点的第一个字符和path的第一个字符是否相同。有相同的话,就会找在对应的子节点中找到path中省下的可以匹配的部分了。由于有子节点的pathpath的第一个字符相同,所以此子节点部分的uri模式会添加一个节点,即其priority会加一,那么此节点的indiceschildren顺序也会对应的调整了。

for i, max := 0, len(n.indices); i < max; i++ {
   if c == n.indices[i] {
      parentFullPathIndex += len(n.path)
      // 调整 children 和 indices 顺序的作用
      i = n.incrementChildPrio(i)
      n = n.children[i]
      continue walk
   }
}

情况4:当前的子节点中,没有一个子节点的第一个字符和path的第一个字符相同(也有可能当前节点没有子节点)。此处负责添加的是uri中非参数类型的部分,所以有个if c != ':' && c != '*':的判断。

if c != ':' && c != '*' {
   // []byte for proper unicode char conversion, see #65
   n.indices += string([]byte{c})
   child := &node{
      maxParams: numParams,
      fullPath:  fullPath,
   }
   n.children = append(n.children, child)
   n.incrementChildPrio(len(n.indices) - 1)
   n = child
}
n.insertChild(numParams, path, fullPath, handlers)

新的子节点的插入

新的子节点的插入使用的是insertChild的函数,由于path是新的uri模式,在之前的不存在(如果存在,这个新增的path就是没有意义的,就会报错),所以会增加对应的节点路径。insertChild主要会对paramcatchAll做特殊的处理。所以就会有了外层的对于参数个数判断的for循环。如果参数不存在的话,直接给当前节点添加对应的pathhandlers就好了。

for numParams > 0 {
		...
}

每个for循环都会在第一步找到下一个参数的位置,使用的是下面这个函数,这个比较简单就不再赘述了

func findWildcard(path string) (wildcard string, i int, valid bool) {
   // Find start
   for start, c := range []byte(path) {
      // A wildcard starts with ':' (param) or '*' (catch-all)
      if c != ':' && c != '*' {
         continue
      }
      // Find end and check for invalid characters
      valid = true
      for end, c := range []byte(path[start+1:]) {
         switch c {
         case '/':
            return path[start : start+1+end], start, valid
         case ':', '*':
            valid = false
         }
      }
      return path[start:], start, valid
   }
   return "", -1, false
}

下面就会验证参数的有效性了,和代码上的注释一样。主要说下len(n.children)的判断,由于此节点是新增的,那么新增的节点的children长度必然为0。

if i < 0 { // No wildcard found
   break
}

// The wildcard name must not contain ':' and '*'
if !valid {
   panic("only one wildcard per path segment is allowed, has: '" +
      wildcard + "' in path '" + fullPath + "'")
}

// check if the wildcard has a name
if len(wildcard) < 2 {
   panic("wildcards must be named with a non-empty name in path '" + fullPath + "'")
}

// Check if this node has existing children which would be
// unreachable if we insert the wildcard here
if len(n.children) > 0 {
   panic("wildcard segment '" + wildcard +
      "' conflicts with existing children in path '" + fullPath + "'")
}

剩下的部分操作就要分情况了,因为参数类型有两种,所以要分为param类型和catchAll类型,操作过程就是不断的分割uri,也没什么好说的。