go gin最左路由前缀树

gin也用了一段时间了,写个文总结一下路由部分吧,以免忘记。

关键名称:最左最短前缀树。

假设,最开始的路由route.GET("/R1R2R3R4R5..........Rn", func(c *gin.Context) {} ),这个时候树还是空的,直接调用n.insertChild(path, fullPath, handlers)函数。

对于/R1R2R3R4R5..........Rn,分几种情况:

一、没有通配符:和*

直接生成一个结点。

二、无*号,但至少有一个冒号":",第一个冒号所在位置Ri,有如下推论:

  • n >= i + 1 比如/user/: 就是非法,必须带有参数名。

  • 设集合A = { j | j (i+1, n] && Rj = : } ,且A从小大到排序;

    集合B = { k | k (i+1, n] && Rk = / } ,且B从小大到排序。

    A与B分四种情况

    A与B分四种情况

    1. A = ,B = 此时生成的路由树如下,W表示wildcard,虚线表示不挂载handlers。

     

    1. A = ,B 取 k = Min(B) 此时生成的路由树如下

       

    1. A ,B = 非法,不能在单个路径上(即用/分隔的路径)存在多个通配符,比如 /user:name:id。

    2. A ,B 这种情况最复杂,需要考查 j、k的关系,取 j = Min(A),k = Min(B)

      • j < k 非法,比如/user/:name:

      • j > k 以k的分界线,先生成k之前的部分,之后再以k为起点,递归的从1开始生成子结点

        以/user:name/uid:uid为例,生成的路由树如下:

     

三、无:号,至少有一个*,第一个*号所在位置Ri,有如下推论

  • 有且仅有一个* ,gin规定路径中只能有一个*,比如 /user/*name/*id 是非法;

  • Ri-1 = /,gin规定在*之前,必须使用/开头,比如 /user*name 是非法;

  • *只能在路径的最后面,比如/user/*name/id 是非法;

注意,r.GET("*name", func(c *gin.Context){}) 在内部gin会加上/,开成 /*name路径。

形成的路由树以及一个例子/user/name/*name,其中左边绿色小方框的是fullPath,右边橙色的是indices

四、有:号,且有*号,根据三的结论,*号只能有一个,且*号只能在路径最后面,此时可以这样处理

  • 设集合A = { j | j [1, n] && Rj = : } ,且A从小大到排序,取 j = Max(A);

    集合B = { k | k (k+1, n] && Rk = / } ,且B从小大到排序,取 k = Min(B);

    k 即为最后一个:号后面的第一个/位置;

    分成两个子串:T = /R1R2R3....Rk-1 和 Y = RkRk+1....Rn,T只有:号,Y只有*号。

    对于T,按照前面一二递归处理;对于Y,按照三处理,即可。

    下图为例子 /user/name/:name/id/:id/sex/*sex

到这里,单个路由树的规则已经有了。接下来是在已有路由树的基础上添加路由,导致的结点分裂、移动。

设已有的路由树如下:

要添加的路径为 S = S1S2S3...Sm,R与S的最长公共子串为C = C1C2C3...Ci,i[1, Min(n, m)],i、m、n三者关系讨论:

只有 ① ② ③ ④ 有效,其他都是无效的组合。

  • 对于①,此时需要将R分裂,C(即 R1R2R3...Ri)作为父结点,Ri+1...Rn子结点

  • 对于 ③,相当于无效路由;
  • 对于 ④,按照下面的规则:

      1. 截取 P = Si+1...Sm

      2. 如果当前结点R是个param结点,Si+1 = /,且 R有子结点(有的话只会有一个子结点):

        S = P,R = children[0];

        回到起点;

      3. 如果在当前结点R的indices(param结点不会有indices)中能找到相同的Si+1值,说明在R的子结点中,有相同的前缀:

        找到该子结点Rc

        增加Rc的权值,并根据新的权值,重新排列R的子结点们;

        S = P,R = Rc

        回到起点;

      4. 如果Si+1 != :,且Si+1 != *:

        为当前结点R添加indices;

        在当前结点R后面添加添加子结点Si+1Si+2...Sm,添加方式与前面生成路由树的过程一样,会增加当前结点R的权重,重新排列;

        最后返回;

         

      5. 如果当前结点R的W值(R.wildcard) = true:

        W值为true,只能是param和catchAll两种情况,且两种情况的模式是这样:

        对于catchAll模式,直接panic退出。因为catchAll是通配后缀所有的字符串,gin规定不能在其后面添加路径;

        对于param模式,取R的子结点Rc(唯一的子结点),即Rc = R.child,接着判断 P是否要比Rc长,且P是否以Rc打头,且 Rc打头后第一个字符是 /,即进行以下判断:

        取 L = len(Rc),len(P) >= L && P[:L] == Rc && P[L] = /

        如果条件不满足,则直接panic退出,因为gin不允许存在存在相同param模式的路径,比如(/:name 与 /:namekk)。

        如果条件满足,S = P,R = Rc

        回到起点;

         

 

 

至此完成!

可以在gin里加上代码,打印出层级路由树:

func (engine *Engine) PrintTrees() {
  for _, tree := range engine.trees {
    // debugPrint("%-6s %-25s --> %s (%d handlers)\n", httpMethod, absolutePath, handlerName, nuHandlers)
    fmt.Fprintf(DefaultWriter, "method: %s\n  root: ", tree.method)
    printNode(tree.root, 0)

    fmt.Println()
  }
}

func printNode(n *node, indent int) {
  content := fmt.Sprintf("path(%s) fullPath(%s) prio(%d) indices(%s) wildChild(%t) handlers(%d) nType(%s)\n", n.path, n.fullPath, n.priority, n.indices, n.wildChild, len(n.handlers), getNtype(n.nType))
  format := fmt.Sprintf("%%%ds", len(content) + indent)

  fmt.Fprintf(DefaultWriter, format, content)

  if indent == 0 {
    indent += 10
  } else {
    indent += 2
  }

  for _, c := range n.children {
    printNode(c, indent)
  }
}

 

posted on 2022-04-11 18:25  留校察看  阅读(127)  评论(0编辑  收藏  举报

导航