Golang:HttpRouter 源码分析(二)

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第3天,点击查看活动详情

前言

这里是第一篇: Golang:HttpRouter 源码分析(一)

虽然没人点赞,但还是要把坑填完~~

这是 Golang:HttpRouter 源码分析的第二篇文章,本篇文章将继续通过示例来分析路由树的构建过程。

源码分析

上一篇文章中,我们已经通过调试官方示例代码了解了一部分路由树的构建过程。那么,现在让我们先把建树过程补充完整。建议一起调试以下示例,有助于梳理程序调用过程,更方便理解。

addRouter

为了减少源码所占篇幅,提升阅读体验,我将源码放在了 码上掘金 里,以便查看。

示例

先来看 addRouter 部分,上一篇讲到了直接在公共前缀后添加子节点的部分,但还有一些与参数节点以及公共前缀包含多个节点的内容没有讲完,下面我们通过构造特殊示例来了解这两部分内容。

示例如下:

package main

import (
	"fmt"
	"log"
	"net/http"

	"github.com/julienschmidt/httprouter"
)

func Index(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
	fmt.Fprint(w, "Welcome!\n")
}

func Hello(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
	fmt.Fprintf(w, "hello, %s!\n", ps.ByName("name"))
}

func main() {
	router := httprouter.New()
	router.GET("/hello/", Index)
	router.GET("/hello/:name", Hello)
	router.GET("/hello/:name/config", Hello)
	router.GET("/hello/:name/city", Hello)
	router.GET("/hello/city", Index)

	log.Fatal(http.ListenAndServe(":8080", router))
}

第三个 GET 请求

我们直接从第三个 GET 请求 开始看。router.GET("/hello/:name/config", Hello)

// router.go

path = path[i:] // 刨去公共前缀后的部分

// 如果子节点是参数节点,检查是否发生冲突
if n.wildChild {
    n = n.children[0]  // 令n为子节点
    n.priority++

    // 更新子节点最大参数个数
    if numParams > n.maxParams {
            n.maxParams = numParams
    }
    numParams--  // 后续的参数个数减去一个

    // 检查参数节点是否完全相同
    if len(path) >= len(n.path) && n.path == path[:len(n.path)] &&
            // catchAll节点后面不能有子节点
            n.nType != catchAll &&
            // 确保两个路径完全一样,不会出现像 :name 和 :names 这样的情况
            (len(n.path) >= len(path) || path[len(n.path)] == '/') {
            continue walk  // 重复查找并去掉公共前缀的过程
    } else {
            // 否则发生冲突
            // ...
    }
}

之前提到过了,参数节点在树中比较特殊,它是其父节点的唯一子节点且不与父节点合并(为了记录参数)。在前缀相同的路径的同一位置处的参数只能有一个名字,并且不能有其他路径在这个位置与其重叠。还有一点,任意捕获参数后面不能再添加路径。

例如:

  • /hello/:name/config/hello/:names/city :无法判断中间的参数到底传给哪个名字;

  • /hello/:name/config/hello/user/city :无法判断中间那个到底是路径还是参数;

  • /hello/:name/config/hello/*all :任意捕获节点会把后面的路径全部当做参数,肯定会和其他节点冲突。

当然,如果硬要把它们写成所谓的特色功能也不是不可以,但那样的代码逻辑太复杂、太混乱了,无论是从规范性上,还是从使用上都没有必要那么做。

匹配完后,直接将 /config 作为参数节点的子节点即可。

- /hello/
    - :name           indices:"/"
        - /config

第四个 GET 请求

router.GET("/hello/:name/city", Hello)

直接跳到参数节点处理完成后,接下来就是对在参数节点后添加路径的特殊处理:

c := path[0]

// n为参数节点,n的子节点个数只能是0或1
if n.nType == param && c == '/' && len(n.children) == 1 {
        n = n.children[0]  // 直接令n为其子节点
        n.priority++
        continue walk  // 继续查找子节点和剩余路径的最大公共前缀
}

参数节点的子节点要么没有,要么只有一个,(或者说要么没'/',要么有'/')。当没有子节点时,就是上一个情况,直接在后面添加;当有子节点时,直接往下查找就行了,不需要依次查索引,所以这里就是一个特殊处理。

c=='/' 这个条件,我没看出来有什么功能。前面已经检查过合法性了,参数后面如果还有值,应该只可能是 '/' ,所以这个可能只是为了谨慎吧。

然后是对 /config/city 的处理

// 新公共前缀比原公共前缀短,需要将当前的节点按公共前缀分开
if i < len(n.path) {
        child := node{ // 这个节点是,将原节点路径按公共前缀拆开后,后面的部分
                path:      n.path[i:],
                wildChild: n.wildChild,
                nType:     static,
                indices:   n.indices,
                children:  n.children,
                handle:    n.handle,
                priority:  n.priority - 1,
        }

        // 更新新节点的最大参数个数为其所有子节点最大参数个数的最大值
        for i := range child.children {
                if child.children[i].maxParams > child.maxParams {
                        child.maxParams = child.children[i].maxParams
                }
        }
        // 将当前节点修改为新的公共前缀,将新节点作为子节点添加到当前节点中
        n.children = []*node{&child}
        n.indices = string([]byte{n.path[i]})
        n.path = path[:i]
        n.handle = nil
        n.wildChild = false
}

最后,直接插入新路径的公共前缀的后面的部分。

- /hello/
    - :name           indices:"/"
        - /c          indices:"oi"
            - onfig
            - ity

第五个请求

router.GET("/hello/city", Index) ,这是一个错误示范。

panic: 'city' in new path '/hello/city' conflicts with existing wildcard ':name' in existing prefix '/hello/:name'

我们来看看它的报错提示是怎么写的。

var pathSeg string  // 新路径出现冲突的部分(没有完成插入的部分)
if n.nType == catchAll {
        pathSeg = path // 对于任意捕获节点,冲突的部分以及其后面的全部,都是错误路径
} else {
        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 +
        "'")

剩下的两部分

addRoute 还有两个小部分没分析,逻辑比较简单,这里就不再写示例代码了。

第一个是处理,匹配完公共前缀后,如果该节点有子节点,则接下来该向哪条边走(或者新建一条边)的问题。

// 遍历当前节点的索引,判断是否和子节点有公共前缀
for i := 0; i < len(n.indices); i++ {
    if c == n.indices[i] {  //如果有,就向该节点走,并继续匹配公共前缀
            i = n.incrementChildPrio(i)
            n = n.children[i]
            continue walk
    }
}

第二个,就是当插入路径刚好与当前节点所表示的路径重合了,那么判断一下该节点是否已经有handle了(也就是判断这条路径是不是已经注册过了),没有的话直接添加handle就可以了。

else if i == len(path) {
        if n.handle != nil {
                panic("a handle is already registered for path '" + fullPath + "'")
        }
        n.handle = handle
}

到此为止,整个 addRoute 函数就分析完了。 撒花~

太累了,insertChild 就之后再分析吧。

小结

总结一下, 可以看出 HttpRouter 的作者将 addRouteinsertChild 两个功能解耦得非常干净,addRoute 函数本身的代码只负责对公共前缀的查找和对路由中已有路径的节点进行修改,不会涉及到新路径节点的添加,这一点是非常值得我们去学习的。

在下一篇中,我们会继续完成 insertChild 代码的分析,将路由树的构建过程分析完,敬请期待。

如果本篇文章对你有帮助的话,(或者觉得作者打这么多字挺辛苦的话),不要忘记点个赞哦,感谢~

猜你喜欢

转载自juejin.im/post/7126128040672755742
今日推荐