参数解析
主要的结构体如下
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的类型,有下面四种类型:
static是普通的字符串,也是默认的类型root就和名字一样了,是根节点。每个HTTP请求方法都会有一个根节点。param是参数类型,例如/user/:name中的namecatchAll也是参数类型,例如/user/*name则name可以是任何字符串。不过这种类型只能出现在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中插入到合适的位置。所以需要找到path和n.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或者catchAll。path的后续部分必须也是同样的参数类型,否则这两种类型就互相冲突了。假如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中省下的可以匹配的部分了。由于有子节点的path和path的第一个字符相同,所以此子节点部分的uri模式会添加一个节点,即其priority会加一,那么此节点的indices和children顺序也会对应的调整了。
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主要会对param和catchAll做特殊的处理。所以就会有了外层的对于参数个数判断的for循环。如果参数不存在的话,直接给当前节点添加对应的path和handlers就好了。
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,也没什么好说的。