httprouter 源码分析

作者: BroQiang

发布于 2019-04-17 | 最后更新 2019-04-17


关于 httprouter 本身就不过多说了,可以直接去查看源码及 README 。

这个包相对还是比较简单了,只有几个文件,并且除了标准库没有外部的依赖。难理解的就是基数树,需要算法基础。

抛砖引玉,有不对的地方望指出,我及时修改。

入口

使用的是代码追踪的方式,可以从官方给的 demo 来入手:

package main

import (
    "fmt"
    "github.com/julienschmidt/httprouter"
    "net/http"
    "log"
)

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("/", Index)
    router.GET("/hello/:name", Hello)

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

可以看到,这个 demo 还是比较简单的, 主要就做了几件事:

  • 定义了两个业务处理的函数(虽然只做了简单的字符串输出)。

  • 初始化路由, httprouter.New()

  • 将上面的两个函数注册到路由中

  • 使用 httprouter 的 handler 启动服务

下面就从 main 函数开始看,通过 New() 函数初始化了一下 httprouter ,然后调用了两次 GET 方法,分别传入了 Index 和 Hello 的业务处理函数。

下面就追踪到 New() 中,看看它到底做了什么,这个函数也很简单, 只是初始化了一下 Router 结构,将几个参数的默认值设置为 true ,并返回了 *Router (指针)。

func New() *Router {
    return &Router{
        RedirectTrailingSlash:  true,
        RedirectFixedPath:      true,
        HandleMethodNotAllowed: true,
        HandleOPTIONS:          true,
    }
}

这个 Route 是什么? 里面的参数又代表了什么,追踪到 Router 去看一下。

Router struct

先看看这个结构体里面包含了什么字段,顺便将注释翻译一下,这里就没有保留注释的原文,如果想要了解原文,可以直接去 源码 中查看即可。

// Router 是一个 http.Handler 可以通过定义的路由将请求分发给不同的函数
type Router struct {
    trees map[string]*node

    // 这个参数是否自动处理当访问路径最后带的 /,一般为 true 就行。
    // 例如: 当访问 /foo/ 时, 此时没有定义 /foo/ 这个路由,但是定义了
    // /foo 这个路由,就对自动将 /foo/ 重定向到 /foo (GET 请求
    // 是 http 301 重定向,其他方式的请求是 http 307 重定向)。
    RedirectTrailingSlash bool

    // 是否自动修正路径, 如果路由没有找到时,Router 会自动尝试修复。
    // 首先删除多余的路径,像 ../ 或者 // 会被删除。
    // 然后将清理过的路径再不区分大小写查找,如果能够找到对应的路由, 将请求重定向到
    // 这个路由上 ( GET 是 301, 其他是 307 ) 。
    RedirectFixedPath bool

    // 用来配合下面的 MethodNotAllowed 参数。
    HandleMethodNotAllowed bool

    // 如果为 true ,会自动回复 OPTIONS 方式的请求。
    // 如果自定义了 OPTIONS 路由,会使用自定义的路由,优先级高于这个自动回复。
    HandleOPTIONS bool


    // 路由没有匹配上时调用这个 handler 。
    // 如果没有定义这个 handler ,就会返回标准库中的 http.NotFound 。
    NotFound http.Handler

    // 当一个请求是不被允许的,并且上面的 HandleMethodNotAllowed 设置为 ture 的时候,
    // 如果这个参数没有设置,将使用状态为 with http.StatusMethodNotAllowed 的 http.Error
    // 在 handler 被调用以前,为允许请求的方法设置 "Allow" header 。
    MethodNotAllowed http.Handler

    // 当出现 panic 的时候,通过这个函数来恢复。会返回一个错误码为 500 的 http error
    // (Internal Server Error) ,这个函数是用来保证出现 painc 服务器不会崩溃。
    PanicHandler func(http.ResponseWriter, *http.Request, interface{})
}

现在可以看到, Router 这个结构就是 httprouter 的一个核心的部分,这里定义了路由的一些初始配置,基本通过注释就可以知道它们是做什么用的,上面还有个 trees 是这个结构最核心的内容,竟然没注释(原文就没有注释),这里面保存的就是注册的路由,按照 method (POST,GET …) 方法分开,每一个方法对应一个基数树(radix tree)。这个树比较复杂,暂时先不去理会,简单的理解成将路由保存进来即可。

在 Router 结构和 New() 函数之间还有一行比较有意思的写法:

var _ http.Handler = New()

这个其实就是通过 New() 函数初始化一个 Router ,并且指定 Router 实现了 http.Handler 接口 ,如果 New() 没有实现 http.Handler 接口,在编译的时候就会报错了。这里只是为了验证一下, New() 函数的返回值并不需要,所以就把它赋值给 _ ,相当于是给丢弃了。

到这里,我们就可以看到了, Router 也是基于 http.Handler 做的实现,如果要实现 http.Handler 接口,就必须实现 ServeHTTP(w http.ResponseWriter, req *http.Request) 这个方法,下面就可以去追踪下 ServerHTTP 都做了什么。

ServerHTTP

这个代码比较长,就将分析的步骤写在注释中了,关于基数树放在最后说明。

// ServeHTTP makes the router implement the http.Handler interface.
func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    // 在最开始,就做了 panic 的处理,这样可以保证业务代码出现问题的时候,不会导致服务器崩溃
    // 这里就是简单的在浏览器返回一个 500 错误,如果想在 panic 的时候自己处理
    // 只需要将 r.PanicHandler = func(http.ResponseWriter, *http.Request, interface{})
    // 重写, 添加上自己的处理逻辑即可。
    if r.PanicHandler != nil {
        defer r.recv(w, req)
    }

    // 通过 Request 获取当前请求的路径
    path := req.URL.Path

    // 到基数树中去查找匹配的路由
    // 首先看是否有请求的方法(比如 GET)是否存在路由,如果存在就继续寻找
    if root := r.trees[req.Method]; root != nil {

        // 如果路由匹配上了,从基数树中将路由取出
        if handle, ps, tsr := root.getValue(path); handle != nil {
            // 获取的是一个函数,签名 func(http.ResponseWriter, *http.Request, Params)
            // 这个就是我们向 httprouter 注册的函数
            handle(w, req, ps)
            // 处理完成后 return ,此次生命周期就结束了
            return

            // 当没有找到的时候,并且请求的方法不是 CONNECT 并且 路径不是 / 的时候
        } else if req.Method != "CONNECT" && path != "/" {
            // 这里就要做重定向处理, 默认是 301
            code := 301 // Permanent redirect, request with GET method

            // 如果请求的方式不是 GET 就将 http 的响应码设置成 307
            if req.Method != "GET" {
                // Temporary redirect, request with same method
                // As of Go 1.3, Go does not support status code 308.
                // 看上面的注释的意思貌似是作者想使用 308(永久重定向),但是 Go 1.3
                // 不支持 308 ,所以使用了一个临时重定向。 为什么不能用 301 呢?
                // 因为 308 和 307 不允许请求方法从 POST 更改为 GET
                code = 307
            }

            // tsr 返回值是一个 bool 值,用来判断是否需要重定向, getValue 返回来的
            // RedirectTrailingSlash 这个就是初始化时候定义的,只有为 true 才会处理
            if tsr && r.RedirectTrailingSlash {
                // 如果 path 的长度大于 1,只有大于 1 才会出现这种情况,如 p/,path/
                // 并且路径的最后是 / 的时候将最后的 / 去除
                if len(path) > 1 && path[len(path)-1] == '/' {
                    req.URL.Path = path[:len(path)-1]
                } else {
                    // 如果不是 / 结尾的,给结尾添加一个 /
                    // 假设定义了一个路由 '/foo/' ,并且没有定义路由 /foo ,
                    // 实际访问的是 '/foo', 将 /foo 重定向到 /foo/
                    req.URL.Path = path + "/"
                }

                // 将处理过的路由重定向, 这个是一个 http 标准包里面的方法
                http.Redirect(w, req, req.URL.String(), code)
                return
            }

            // Try to fix the request path
            // 路由没有找到,重定向规则也不符合,这里会尝试修复路径
            // 需要在初始化的时候定义 RedirectFixedPath 为 true,允许修复
            if r.RedirectFixedPath {
                // 这里就是在处理 Router 里面说的,将路径通过 CleanPath 方法去除多余的部分
                // 并且 RedirectTrailingSlash 为 ture 的时候,去匹配路由
                // 比如: 定义了一个路由 /foo , 但实际访问的是 ////FOO ,就会被重定向到 /foo
                fixedPath, found := root.findCaseInsensitivePath(
                    CleanPath(path),
                    r.RedirectTrailingSlash,
                )
                if found {
                    req.URL.Path = string(fixedPath)
                    http.Redirect(w, req, req.URL.String(), code)
                    return
                }
            }
        }
    }

    // 路由也没有匹配,重定向也没有找到,修复后仍然没有匹配,就到了这里

    // 如果没有任何路由匹配,请求方式又是 OPTIONS, 并且允许响应 OPTIONS
    // 就会给 Header 设置一个 Allow 头,返回去
    if req.Method == "OPTIONS" && r.HandleOPTIONS {
        // Handle OPTIONS requests
        if allow := r.allowed(path, req.Method); len(allow) > 0 {
          w.Header().Set("Allow", allow)
            return
        }
    } else {
        // 如果不是 OPTIONS 或者 不允许 OPTIONS 时
        // Handle 405
        // 如果初始化的时候 HandleMethodNotAllowed 为 ture
        if r.HandleMethodNotAllowed {
            // 返回 405 响应,通过 allowed() 方法来处理 405 时 allow的值。
            // 大概意思是这样的,比如定义了一个 POST 方法的路由 POST("/foo",...)
            // 但是调用却是通过 GET 方式,这是就会给调用者返回一个包含 POST 的 405
            if allow := r.allowed(path, req.Method); len(allow) > 0 {
                w.Header().Set("Allow", allow)
                if r.MethodNotAllowed != nil {
                    // 这里默认就会返回一个字符串 Method Not Allowed
                    // 可以自定义 r.MethodNotAllowed = 自定义的 http.Handler 实现
                    // 来按照需求响应, allowd 方法也不太难,就是各种判断和查找,就不分析了。
                    r.MethodNotAllowed.ServeHTTP(w, req)
                } else {
                    http.Error(w,
                        http.StatusText(http.StatusMethodNotAllowed),
                        http.StatusMethodNotAllowed,
                    )
                }
                return
            }
        }
    }

    // Handle 404
    // 如果什么的没有找到,就只会返回一个 404 了
    // 如果定义了 NotFound ,就会调用自定义的
    if r.NotFound != nil {
        // 如果需要自定义,在初始化之后给 NotFound 赋一个值就可以了。
        // 可以简单的通过 http.HandlerFunc 包装一个 handler ,例如:
        // router.NotFound = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        //         w.Write([]byte("什么都没有找到"))
        // })
        //
        r.NotFound.ServeHTTP(w, req)
    } else {
        // 如果没有定义,就返回一个 http 标准库的  NotFound 函数,返回一个 404 page not found
        http.NotFound(w, req)
    }

到这里基本也就清除了它的基本调用的流程, 就是定义一个 Router 结构,并且使它实现 http.Handler 接口, 也就是添加一个 ServeHTTP 方法。 然后在 ServeHTTP 中去做路由的处理。

路由设置

现在为止,看到的都是路由的调用,那些路由又是哪里来的呢? 其实如果看了 router.go 这个文件,就可以在里面发现,GET, POST, PUT 等方法,就是这些方法来设置的。

还记得开始的时候,引用了一个官方的 demo,里面就有两个设置路由的例子:

...


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

...

router.GET("/", Index)

下面就追踪这个 GET 方法,看看它到底做了什么:

func (r *Router) GET(path string, handle Handle) {
    r.Handle("GET", path, handle)
}

...

// DELETE is a shortcut for router.Handle("DELETE", path, handle)
func (r *Router) DELETE(path string, handle Handle) {
    r.Handle("DELETE", path, handle)
}

...

可以看到,从 GET 方法一直到 DELETE 方法,几乎长得都一样,也没做其它事情,就是调用了一下 Handle 方法,继续追踪 Handle 方法。

// 通过给定的路径和方法,注册一个新的 handle 请求,对于 GET, POST, PUT, PATCH 和 DELETE
// 请求,相对应的方法,直接调用这个方法也是可以的,只需要多传入第一个参数即可。
// 官方给的建议是: 在批量加载或使用非标准的自定义方法时候使用。
//
// 它有三个参数,第一个是请求方式(GET,POST……), 第二个是路由的路径, 前两个参数都是字符串类型
// 第三个参数就是我们要注册的函数,比如上面的 Index 函数,可以追踪一下,在这个文件的最上面
// 有 Handle 的定义,它是一个函数类型,只要和这个 Handle 的签名一直的都可以作为参数
func (r *Router) Handle(method, path string, handle Handle) {

    // 验证路径是否是 / 开头的,如: /foo 就是可以的, foo 就会 panic,编译的时候机会出错
    if path[0] != '/' {
        panic("path must begin with '/' in path '" + path + "'")
    }

    // 因为 map 是一个指针类型,必须初始化才可以使用,这里做一个判断,
    // 如果从来没有注册过路由,要先初始化 tress 属性的 map
    if r.trees == nil {
        r.trees = make(map[string]*node)
    }

    // 因为路由是一个基数树,全部是从根节点开始,如果第一次调用注册方法的时候跟是不存在的,
    // 就注册一个根节点, 这里是每一种请求方法是一个根节点,会存在多个树。
    root := r.trees[method]

    // 根节点存在就直接调用,不存在就初始化一个
    if root == nil {
        // 需要注意的是,这里使用的 new 来初始化,所以 root 是一个指针。
        root = new(node)
        r.trees[method] = root
    }

    // 向 root 中添加路由,树的具体操作在后面单独去分析。
    root.addRoute(path, handle)
}

现在,路由就已经注册,可以使用了。 看一下上面函数的第三个参数 Handle ,追踪一下

type Handle func(http.ResponseWriter, *http.Request, Params)

可以看到,这个签名和 http.HandlerFunc 的签名比较像,用途其实也是一样的,只不过多了一个 Params 参数。这个参数就是用来支持路径中的参数绑定的, 比如: /hello/:name ,可以通过 Params.ByName("name") 来获取路径绑定参数的值。其实这个 Params 就是一个 Param 的切片, 也可以自己将所有的值遍历出来,追踪过去看一下就很容易明白了:

// Parram 是一个 URL 参数,包含了一个 Key 和 一个 Value 。
// Key 就是注册路由时候的使用的字符串,如: "/hello/:name" ,这个 Key 就是 name
// Value 就是访问路径中获取到的值, 如: localhost:8080/hello/BroQiang ,
// 此时 Value 就是 BroQiang
type Param struct {
    Key   string
    Value string
}

// Params 就是一个 Param 的切片,这样就可以看出来, URL 参数可以设置多个了。
// 它是在 tree 的 GetValue() 方法调用的时候设置的,一会分析树的时候可以看到。
// 这个切片是有顺序的,第一个设置的参数就是切片的第一个值,所以通过索引获取值是安全的
type Params []Param

// ByName 返回传入的 name 的值,如果没有找到就会返回一个空字符串
// 这里就是做了个循环,一担找到就将值返回。
func (ps Params) ByName(name string) string {
    for i := range ps {
        if ps[i].Key == name {
            return ps[i].Value
        }
    }
    return ""
}

定义静态文件目录

到此时,我们会发现整个 router.go 文件貌似都已经追踪完了,还整下两个函数没有提到: 和 LookupServeFiles

Lookup 暂时没有发现哪里用到了,一会查看其他文件的时候看看有没有用到的地方。先留个坑,最后再填。

下面来看看 ServeFiles 这个方法。

// 这个是用来定义静态文件的,比如 js, css, 图片等
// path 是定义的 URL 路径,必须是 /*filepath 这种格式
// root 对应的是系统中的目录或文件系统,因为这个函数其实是使用的 http 标准库中的 FileServer
// 来实现的文件服务,所以 root 参数要传入一个 http.FileSystem 类型的参数,可以通过
// http.Dir("/etc/...") 这种方式转换一下,因为 Dir 实现了 http.FileSystem 接口,
// 直接使用它来转换就行了。
//
// 示例: 比如定义了路由 router.ServeFiles("/public/*filepath", http.Dir("/etc"))
// 此时访问 localhost:8080/public/passwd , 就可以将 /etc/passwd 文件的内容显示了
func (r *Router) ServeFiles(path string, root http.FileSystem) {
    // 就是因为这三行代码的处理,是验证路径是否 /xxx/*filepath 这种结构定义
    // 因为标准库要求必须这样设置路径,这里也就这样了,
    // 个人还真没想到为什么要这样处理,应该有什么意义吧
    if len(path) < 10 || path[len(path)-10:] != "/*filepath" {
        panic("path must end with /*filepath in path '" + path + "'")
    }

    fileServer := http.FileServer(root)

    r.GET(path, func(w http.ResponseWriter, req *http.Request, ps Params) {
        req.URL.Path = ps.ByName("filepath")
        fileServer.ServeHTTP(w, req)
    })
}

这个方法其实最终也是调用的标准库,并且不太灵活,看需求,不用也是可以的,比如需要给静态文件设置缓存,调用这个方法就没法实现了,不过可以自己包装一下 http.FileServer 就可以了,详细参考这个 Issues#40

到这里 httprouter 的基本路由处理已经读完,在不考虑性能的情况下,完全可以模仿这个来实现一个自己的路由了。如果是一个小的服务,没有几个路由,其实完全不用考虑,没必要使用树,直接一个 map 就可以了。标准库的 ServeMux 中的 muxEntry 也只有简单的两个字段(所以支持的功能比较少)。

Radix tree

这个就是这个路由号称最快的关键部分(这个没去核实过,也不用去纠结)。开始读源代码前,先了解下原理(这个就是之前一直没去读源码的原因),详细见官方给的说明: How does it work?

这里简单的解读下,路由使用了一个有共同前缀的一个树结构,这个树就是一个压缩前缀树( compact prefix tree ) 或者就叫基数树( Radix tree )。也就是具有共同前缀的节点拥有相同的父节点,官方给出的一个 GET 请求的例子,通过示例就比较好理解了:

图示,直接拔过来的。

Priority   Path             Handle
9          \                *<1>
3          ├s               nil
2          |├earch\         *<2>
1          |└upport\        *<3>
2          ├blog\           *<4>
1          |    └:post      nil
1          |         └\     *<5>
2          ├about-us\       *<6>
1          |        └team\  *<7>
1          └contact\        *<8>


// 这个图相当于注册了下面这几个路由
GET("/search/", func1)
GET("/support/", func2)
GET("/blog/:post/", func3)
GET("/about-us/", func4)
GET("/about-us/team/", func5)
GET("/contact/", func6)

通过上面的示例可以看出:

  • *<数字> 代表一个 handler 函数的内存地址(指针)

  • search 和 support 拥有共同的父节点 s ,并且 s 是没有对应的 handle 的, 只有叶子节点(就是最后一个节点,下面没有子节点的节点)才会注册 handler 。

  • 从根开始,一直到叶子节点,才是路由的实际路径。

  • 路由搜索的顺序是从上向下,从左到右的顺序,为了快速找到尽可能多的路由,包含子节点越多的节点,优先级越高。

node

大概了解下,开始读代码,详细去看看。一样是先找一个入口,上面在分析的时候已经留了好几个坑, 可以慢慢填了。

首先在 router.go 中找到

...
type Router struct {
    trees map[string]*node

    ...
}

这里是一个 map , 每一种请求方式(GET,POST ……) 单独管理一颗树,官方说这样比每个节点中去保存方法节省空间,并且查找的时候速度会更快。

接下来看看这个 node 是什么:

type node struct {
    // 当前节点的 URL 路径
    // 如上面图中的例子的首先这里是一个 /
    // 然后 children 中会有 path 为 [s, blog ...] 等的节点
    // 然后 s 还有 children node [earch,upport] 等,就不再说明了
    path      string

    // 判断当前节点路径是不是含有参数的节点, 上图中的 :post 的上级 blog 就是wildChild节点
    wildChild bool

    // 节点类型: static, root, param, catchAll
    // static: 静态节点, 如上图中的父节点 s (不包含 handler 的)
    // root: 如果插入的节点是第一个, 那么是root节点
    // catchAll: 有*匹配的节点
    // param: 参数节点,比如上图中的 :post 节点
    nType     nodeType

    // path 中的参数最大数量,最大只能保存 255 个(超过这个的情况貌似太难见到了)
    // 这里是一个非负的 8 进制数字,最大也只能是 255 了
    maxParams uint8

    // 和下面的 children 对应,保留的子节点的第一个字符
    // 如上图中的 s 节点,这里保存的就是 eu (earch 和 upport)的首字母
    indices   string

    // 当前节点的所有直接子节点
    children  []*node

    // 当前节点对应的 handler
    handle    Handle

    // 优先级,查找的时候会用到,表示当前节点加上所有子节点的数目
    priority  uint32
}

现在已经知道了 node 是用来做什么的了,就是用来保存树结构的,初始化的时候是一个空的树,没有任何数据, 接着看看怎么向这颗树中添加数据。

直接读注册路由的时候,Handle 方法中的 root.addRoute(path, handle) 没有去分析,现在就开始啃这个了, 追踪过去看看:

代码很长,真不喜欢读这种结构的代码,看起来比较累。

// addRoute 将传入的 handle 添加到路径中
// 需要注意,这个操作不是并发安全的!!!!
func (n *node) addRoute(path string, handle Handle) {
    fullPath := path
    // 请求到这个方法,就给当前节点的权重 + 1
    n.priority++

    // 计算传入的路径参数的数量
    numParams := countParams(path)

    // 如果树不是空的
    // 判断的条件是当前节点的 path 的字符串长度和子节点的数量全部都大于 0
    // 就是说如果当前节点是一个空的节点,或者当前的节点是一个叶子节点,就直接
    // 进入 else 在当前节点下面添加子节点
    if len(n.path) > 0 || len(n.children) > 0 {
    // 定义一个 lable ,循环里面可以直接 break 到这里,适合这种嵌套的比较深的
    walk:
        for {
            // 如果传入的节点的最大参数的数量大于当前节点记录的数量,替换
            if numParams > n.maxParams {
                n.maxParams = numParams
            }

            // 查找最长的共同的前缀
            // 公共前缀不包含 ":" 或 "*"
            i := 0

            // 将最大值设置成长度较小的路径的长度
            max := min(len(path), len(n.path))

            // 循环,计算出当前节点和添加的节点共同前缀的长度
            for i < max && path[i] == n.path[i] {
                i++
            }

            // 如果相同前缀的长度比当前节点保存的 path 短
            // 比如当前节点现在的 path 是 sup ,添加的节点的 path 是 search
            // 它们相同的前缀就变成了 s , s 比 sup 要短,符合 if 的条件,要做处理
            if i < len(n.path) {
                // 将当前节点的属性定义到一个子节点中,没有注释的属性不变,保持原样
                child := node{
                    // path 是当前节点的 path 去除公共前缀长度的部分
                    path:      n.path[i:],
                    wildChild: n.wildChild,
                    // 将类型更改为 static ,默认的,没有 handler 的节点
                    nType:     static,
                    indices:   n.indices,
                    children:  n.children,
                    handle:    n.handle,
                    // 权重 -1
                    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}

                // 获取子节点的首字母,因为上面分割的时候是从 i 的位置开始分割
                // 所以 n.path[i] 可以去除子节点的首字母,理论上去 child.path[0] 也是可以的
                // 这里的 n.path[i] 取出来的是一个 uint8 类型的数字(代表字符),
                // 先用 []byte 包装一下数字再转换成字符串格式
                n.indices = string([]byte{n.path[i]})

                // 更新当前节点的 path 为新的公共前缀
                n.path = path[:i]

                // 将 handle 设置为 nil
                n.handle = nil

                // 肯定没有参数了,已经变成了一个没有 handle 的节点了
                n.wildChild = false
            }

            // 将新的节点添加到此节点的子节点, 这里是新添加节点的子节点
            if i < len(path) {
                // 截取掉公共部分,剩余的是子节点
                path = path[i:]

                // 如果当前路径有参数
                // 如果进入了上面 if i < len(n.path) 这个条件,这里就不会成立了
                // 因为上一个 if 中将 n.wildChild 重新定义成了 false
                // 什么情况会进入到这里呢 ?
                // 1. 上面的 if 不生效,也就是说不会有新的公共前缀, n.path = i 的时候
                // 2. 当前节点的 path 是一个参数节点就是像这种的 :post
                // 就是定义路由时候是这种形式的: blog/:post/update
                //
                if n.wildChild {
                    // 如果进入到了这里,证明这是一个参数节点,类似 :post 这种
                    // 不会这个节点进行处理,直接将它的子节点赋值给当前节点
                    // 比如: :post/ ,只要是参数节点,必有子节点,哪怕是
                    // blog/:post 这种,也有一个 / 的子节点
                    n = n.children[0]

                    // 又插入了一个节点,权限再次 + 1
                    n.priority++

                    // 更新当前节点的最大参数个数
                    if numParams > n.maxParams {
                        n.maxParams = numParams
                    }

                    // 更改方法中记录的参数个数,个人猜测,后面的逻辑还会用到
                    numParams--

                    // 检查通配符是否匹配
                    // 这里的 path 已经变成了去除了公共前缀的后面部分,比如
                    // :post/update , 就是 /update
                    // 这里的 n 也已经是 :post 这种的下一级的节点,比如 / 或者 /u 等等
                    // 如果添加的节点的 path >= 当前节点的 path &&
                    // 当前节点的 path 长度和添加节点的前面相同数量的字符是相等的, &&
                    if len(path) >= len(n.path) && n.path == path[:len(n.path)] &&
                        // 简单更长的通配符,
                        // 当前节点的 path >= 添加节点的 path ,其实有第一个条件限制,
                        // 这里也只有 len(n.path) == len(path) 才会成立,
                        // 就是当前节点的 path 和 添加节点的 path 相等 ||
                        // 添加节点的 path 减去当前节点的 path 之后是 /
                        // 例如: n.path = name, path = name 或
                        // n.path = name, path = name/ 这两种情况
                        (len(n.path) >= len(path) || path[len(n.path)] == '/') {

                        // 跳出当前循环,进入下一次循环
                        // 再次循环的时候
                        // 1. if i < len(n.path) 这里就不会再进入了,现在 i == len(n.path)
                        // 2. if n.wildChild 也不会进入了,当前节点已经在上次循环的时候改为 children[0]
                        continue walk
                    } else {
                        // 当不是 n.path = name, path = name/ 这两种情况的时候,
                        // 代表通配符冲突了,什么意思呢?
                        // 简单的说就是通配符部分只允许定义相同的或者 / 结尾的
                        // 例如:blog/:post/update,再定义一个路由 blog/:postabc/add,
                        // 这个时候就会冲突了,是不被允许的,blog 后面只可以定义
                        // :post 或 :post/ 这种,同一个位置不允许使用多种通配符
                        // 这里的处理是直接 panic 了,如果想要支持,可以尝试重写下面部分代码


                        // 下面做的事情就是组合 panic 用到的提示信息
                        var pathSeg string

                        // 如果当前节点的类型是有*匹配的节点
                        if n.nType == catchAll {
                            pathSeg = path
                        } else {
                            // 如果不是,将 path 做字符串分割
                            // 这个是通过 / 分割,最多分成两个部分,然后取第一部分的值
                            // 例如: path = "name/hello/world"
                            // 分割两部分就是 name 和 hello/world , pathSeg = name
                            pathSeg = strings.SplitN(path, "/", 2)[0]
                        }

                        // 通过传入的原始路径来处理前缀, 可以到上面看下,方法进入就定义了这个变量
                        // 在原始路径中提取出 pathSeg 前面的部分在拼接上 n.path
                        // 例如: n.path = ":post" , fullPath="/blog/:postnew/add"
                        // 这时的 prefix = "/blog/:post"
                        prefix := fullPath[:strings.Index(fullPath, pathSeg)] + n.path

                        // 最终的提示信息就会生成类似这种:
                        // panic: ':postnew' in new path '/blog/:postnew/update/' \
                        // conflicts with existing wildcard ':post' in existing \
                        // prefix '/blog/:post'
                        // 就是说已经定义了 /blog/:post 这种规则的路由,
                        // 再定义 /blog/:postnew 这种就不被允许了
                        panic("'" + pathSeg +
                            "' in new path '" + fullPath +
                            "' conflicts with existing wildcard '" + n.path +
                            "' in existing prefix '" + prefix +
                            "'")
                    }
                }


                // 如果没有进入到上面的参数节点,当前节点不是一个参数节点 :post 这种
                c := path[0]

                // slash after param
                // 如果当前节点是一个参数节点,如 /:post && 是 / 开头的 && 只有一个子节点
                if n.nType == param && c == '/' && len(n.children) == 1 {
                    // /:post 这种节点不做处理,直接拿这个节点的子节点去匹配
                    n = n.children[0]
                    // 权重 + 1 , 因为新的节点会变成这个节点的子节点
                    n.priority++

                    // 结束当前循环,再次进行匹配
                    // 比如: /:post/add 当前节点是 /:post 有个子节点 /add
                    // 新添加的节点是 /:post/update ,再次循环的时候将会用
                    // /add 和 /update 进行匹配, 就相当于是两次新的匹配,
                    // 由于 node 本身是一个指针,所以它们一直都会在 /:post 下面搞事情
                    continue walk
                }

                // 检查添加的 path 的首字母是否保存在在当前节点的 indices 中
                for i := 0; i < len(n.indices); i++ {
                    // 如果存在
                    if c == n.indices[i] {
                        // 这里处理优先级和排序的问题,把这个方法看完再去查看这个方法干了什么
                        i = n.incrementChildPrio(i)

                        // 将当前的节点替换成它对应的子节点
                        n = n.children[i]

                        // 结束当前循环,继续下一次
                        // 此时的当前 node n,已经变成了它的子字节,下次循环就从这个子节点开始处理了
                        // 比如:
                        // 当前节点是 s , 包含两个节点 earch 和 upport
                        // 这时 indices 就会有字母 e 和 u 并且是和子节点 earch 和 uppor 相对应
                        // 新添加的节点如果叫 subject , 与当前节点匹配去除公共前缀 s 后, 就变成了
                        // ubject ,这时 n = upport 这个节点了,path = ubject
                        // 下一次循环就拿 upport 这个 n 和 ubject 这个 path 去开始下次的匹配
                        continue walk
                    }
                }

                // 如果上面 for 中也没有匹配上,就将新添加的节点插入

                // 如果第一个字符不是通配符 : 并且不是 *
                if c != ':' && c != '*' {
                    // []byte for proper unicode char conversion, see #65
                    // 将 path 的首字母 c 拼接到 indices 中
                    n.indices += string([]byte{c})

                    // 初始化一个新的节点
                    child := &node{
                        maxParams: numParams,
                    }

                    // 将这个新初始化的节点添加到当前节点的子节点中
                    n.children = append(n.children, child)

                    // 这个应该还是在处理优先级,一会再去看这个方法
                    n.incrementChildPrio(len(n.indices) - 1)

                    // 将当前节点替换成新生成的节点
                    n = child
                }

                // 用当前节点发起插入子节点的动作
                // 注意这个 n 已经替换成了上面新初始化的 child 了,只初始化了 maxParams
                // 属性,相当于是一个空的节点。insertChild 这个方法一会再去具体查看,
                // 现在只要知道它在做具体的插入子节点的动作就行,先把这个方法追踪完。
                n.insertChild(numParams, path, fullPath, handle)
                return

            // 如果公共前缀和当前添加的路径长度相等
            } else if i == len(path) { // Make node a (in-path) leaf
                // 如果当前节点不是 static 类型的, 就是已经注册了 handle 的节点
                // 就证明已经注册过这个路由,直接 panic 了
                if n.handle != nil {
                    panic("a handle is already registered for path '" + fullPath + "'")
                }

                // 如果是 nil 的,就是没有注册,证明这个节点是之前添加别的节点时候拆分出来的共同前缀
                // 例如: 添加过两个截点 /submit 和 /subject ,处理后就会有一个 static 的前缀 sub
                // 这是再添加一个 /sub 的路由,就是这里的情况了。

                n.handle = handle
            }

            // 这个新的节点被添加了, 出现了 return , 只有出现这个才会正常退出循环,一次添加完成。
            return
        }
    } else { // Empty tree
        // 如果 n 是一个空格的节点,就直接调用插入子节点方法
        n.insertChild(numParams, path, fullPath, handle)

        // 并且它只有第一次插入的时候才会是空的,所以将 nType 定义成 root
        n.nType = root
    }
}

incrementChildPrio

上面分析 addRoute 的时候调用了两次这个方法,大概知道它是处理优先级的,下面就追踪到这个方法,看看它到底干了什么:

// increments priority of the given child and reorders if necessary
// 通过之前两次的调用,我们知道,这个 pos 都是 n.indices 中指定字符的索引,也就是位置
func (n *node) incrementChildPrio(pos int) int {
    // 因为 children 和 indices 是同时添加的,所以索引是相同的
    // 可以通过 pos 代表的位置找到, 将对应的子节点的优先级 + 1
    n.children[pos].priority++
    prio := n.children[pos].priority

    // 调整位置(向前移动)
    newPos := pos

    // 这里的操作就是将 pos 位置的子节点移动到最前面,增加优先级,比如:
    // 原本的节点是 [a, b, c, d], 传入的是 2 ,就变成 [c, a, b, d]
    // 这只是个示例,实际情况还要考虑 n.children[newPos-1].priority < prio ,不一定是移动全部
    for newPos > 0 && n.children[newPos-1].priority < prio {
        // swap node positions
        n.children[newPos-1], n.children[newPos] = n.children[newPos], n.children[newPos-1]

        newPos--
    }

    // build new index char string
    // 重新组织 n.indices 这个索引字符串,和上面做的是相同的事,只不过是用切割 slice 方式进行的
    // 从代码本身来说上面的部分也可以通过切割 slice 完成,不过移动应该是比切割组合 slice 效率高吧
    // 感觉是这样,没测试过,有兴趣的可以测试下。
    if newPos != pos {
        n.indices = n.indices[:newPos] + // unchanged prefix, might be empty
            n.indices[pos:pos+1] + // the index char we move
            n.indices[newPos:pos] + n.indices[pos+1:] // rest without char at 'pos'
    }

    // 最后返回的是调整后的位置,保证外面调用这个方法之后通过索引找到的对应的子节点是正确的
    return newPos
}

代码看完,大概总结一下,这个方法主要是在处理节点之前的关系,添加或者修改已经存在的,拼接出树的结构,真正写入插入节点的数据是在这个方法处理完关系后,调用 insertChild 方法来完成的。

insertChild

现在就再追踪下,看看它是怎么插入数据的:

打开之后看一下,又是很长…… 有没有大脑翁的一下,仔细一行一行看吧。

// 它传入的几个参数,我们可以回到 addRoute 看看都传给它什么了
// numParams 参数个数
// path 插入的子节点的路径
// fullPath 完整路径,就是注册路由时候的路径,没有被处理过的
// 注册路由对应的 handle 函数
func (n *node) insertChild(numParams uint8, path, fullPath string, handle Handle) {
    var offset int // 已经处理过的路径的所有字节数

    // find prefix until first wildcard (beginning with ':'' or '*'')
    // 查找前缀,知道第一个通配符( 以 ':' 或 '*' 开头
    // 就是要将 path 遍历,提取出参数
    // 只要不是通配符开头的就不做处理,证明这个路由是没有参数的路由
    for i, max := 0, len(path); numParams > 0; i++ {
        c := path[i]

        // 如果不是 : 或 * 跳过本次循环,不做任何处理
        if c != ':' && c != '*' {
            continue
        }

        // 查询通配符后面的字符,直到查到 '/' 或者结束
        end := i + 1

        for end < max && path[end] != '/' {
            switch path[end] {
            // 通配符后面的名称不能包含 : 或 * , 如 ::name 或 :*name 不允许定义
            case ':', '*':
                panic("only one wildcard per path segment is allowed, has: '" +
                    path[i:] + "' in path '" + fullPath + "'")
            default:
                end++
            }
        }

        // 检查通配符所在的位置,是否已经有子节点,如果有,就不能再插入
        // 例如: 已经定义了 /hello/name , 就不能再定义 /hello/:param
        if len(n.children) > 0 {
            panic("wildcard route '" + path[i:end] +
                "' conflicts with existing children in path '" + fullPath + "'")
        }

        // 检查通配符是否有一个名字
        // 上面定义 end = i+1 , 后面的 for 又执行了 ++ 操作,所以通配符 : 或 * 后面最少
        // 要有一个字符, 如: :a 或 :name , :a 的时候 end 就是 i+2
        // 所以如果 end - i < 2 ,就是通配符后面没有对应的名称, 就会 panic
        if end-i < 2 {
            panic("wildcards must be named with a non-empty name in path '" + fullPath + "'")
        }

        // 如果 c 是 : 通配符的时候
        if c == ':' { // param
            // split path at the beginning of the wildcard
            // 从 offset 的位置,到查询到通配符的位置分割 path
            // 并把分割出来的路径定义到节点的 path 属性
            if i > 0 {
                n.path = path[offset:i]
                // 开始的位置变成了通配符所在的位置
                offset = i
            }

            // 将参数部分定义成一个子节点
            child := &node{
                nType:     param,
                maxParams: numParams,
            }

            // 用新定义的子节点初始化一个 children 属性
            n.children = []*node{child}
            // 标记上当前这个节点是一个包含参数的节点的节点
            n.wildChild = true

            // 将新创建的节点定义为当前节点,这个要想一下,到这里这种操作已经有不少了
            // 因为一直都是指针操作,修改都是指针的引用,所以定义好的层级关系不会被改变
            n = child
            // 新的节点权重 +1
            n.priority++
            // 最大参数个数 - 1
            numParams--

            // if the path doesn't end with the wildcard, then there
            // will be another non-wildcard subpath starting with '/'
            // 这个 end 有可能是结束或者下一个 / 的位置
            // 如果小于路径的最大长度,代表还包含子路径(也就是说后面还有子节点)
            if end < max {
                // 将通配符提取出来,赋值给 n.path, 现在 n.path 是 :name 这种格式的字符串
                n.path = path[offset:end]
                // 将起始位置移动到 end 的位置
                offset = end

                // 定义一个子节点,无论后面还有没有子节点 :name 这种格式的路由后面至少还有一个 /
                // 因为参数类型的节点不会保存 handler
                child := &node{
                    maxParams: numParams,
                    priority:  1,
                }

                // 将初始化的子节点赋值给当前节点
                n.children = []*node{child}

                // 当前节点又变成了新的子节点(到此时的 n 的身份已经转变了几次了,看这段代表的时候
                // 脑中要有一颗树,实在想不出来的话可以按照开始的图的结构,将节点的变化记录到一张纸上,
                // 然后将每一次的转变标记出来,就完全能明白了)
                n = child

                // 如果进入倒了这个 if ,执行到这里,就还会进入到下一次循环,将可能生成出来的参数再次去匹配
            }

            // 如果走到了这里,并且是循环的最后一次,就是已经将当前节点 n 定义成了叶子节点
            // 就可以进入到最下面代码部分,进行插入了
            // (上面的注释说明的位置是 else 上面,不是为下面的 else 做的注释)


        // 进入到 else ,就是包含 * 号的路由了
        } else { // catchAll
            // 这里的意思是, * 匹配的路径只允许定义在路由的最后一部分
            // 比如 : /hello/*world 是允许的, /hello/*world/more 这种就会 painc
            // 这种路径就是会将 hello/ 后面的所有内容变成 world 的变量
            // 比如地址栏输入: /hello/one/two/more ,获取到的参数 world = one/twq/more
            // 不会再将后面的 / 作为路径处理了
            if end != max || numParams > 1 {
                panic("catch-all routes are only allowed at the end of the path in path '" + fullPath + "'")
            }

            // 这种情况是,新定义的 * 通配符路由和其他已经定义的路由冲突了
            // 例如已经定义了一个 /hello/bro , 又定义了一个 /hello/*world ,此时就会 panic 了
            if len(n.path) > 0 && n.path[len(n.path)-1] == '/' {
                panic("catch-all conflicts with existing handle for the path segment root in path '" + fullPath + "'")
            }

            // currently fixed width 1 for '/'
            i--
            // 这里是查询通配符前面是否有 / 没有 / 是不行的,panic
            if path[i] != '/' {
                panic("no / before catch-all in path '" + fullPath + "'")
            }

            n.path = path[offset:i]

            // first node: catchAll node with empty path
            // 后面的套路基本和之前看到的类似,就是定义一个子节点,保存通配符前面的路径,
            // 有变化的就是将 nType 定义为 catchAll,就是说代表这是一个  * 号匹配的路由
            child := &node{
                wildChild: true,
                nType:     catchAll,
                maxParams: 1,
            }
            n.children = []*node{child}
            n.indices = string(path[i])
            n = child
            n.priority++

            // second node: node holding the variable
            // 将下面的节点再添加到上面,不过 * 号路由不会再有下一级的节点了,因为它会将后面的
            // 的所有内容当做变量,即使它是个 / 符号
            child = &node{
                path:      path[i:],
                nType:     catchAll,
                maxParams: 1,
                handle:    handle,
                priority:  1,
            }
            n.children = []*node{child}

            // 这里 return 了,看下上面,因为已经将 handle 保存,查到了叶子节点,所以就直接结束了当前方法
            return
        }
    }

    // insert remaining path part and handle to the leaf
    // 这里给所有的处理的完成后的节点(不包含 * 通配符方式)的 handle 和 path 赋值
    n.path = path[offset:]
    n.handle = handle
}

代码读完,简单分析下,就是将没有了上级管理的路由(关系被 addRoute 方法处理了), 交给这个方法处理,处理的时候会根据实际情况搞出新的关系,并最终在叶子节点写入注册的数据(主要是 path 和 handler)

getValue

前面已经分析完成怎么向树中插入数据,现在不知道是否还记得,在 ServeHTTP 方法中还用到了个从树中取数据的方法 getValue, 其实插入如果搞明白了,这个取的方法理解起来就很容易了,按照插入时定义的规则,取出来即可,直接看代码:

// 通过传入的参数的路径,寻找指定的路由
// 参数: 字符串类型的路径
// 返回值:
// Handle 通过路径找到的 handler , 如果没有找到,会返回一个 nil
// Params 如果路由注册的是参数路由,这里会将参数及值返回,是一个 Param 结构的 slice
// tsr 是一个 boolean 类型的标志,告诉调用者这个路由是否可以被重定向,配合 Router
// 里面的 RedirectTrailingSlash 属性
func (n *node) getValue(path string) (handle Handle, p Params, tsr bool) {
walk: // outer loop for walking the tree
    for {
        // 如果要找的路径长度大于节点的长度
        // 入了根目录 / 一般情况都会满足这个条件,进入到这里走一圈
        if len(path) > len(n.path) {
            // 如果寻找的路径和节点保存的路径有相同的前缀
            if path[:len(n.path)] == n.path {
                // 将寻找路径的前缀部分去除
                path = path[len(n.path):]
                // 如果当前的节点不包含通配符(子节点有 : 或者 * 这种的),就可以直接去子节点继续搜索。
                if !n.wildChild {
                    // 通过查找路径的首字符,快速找到包含这个字符的子节点(性能提升的地方就体现出来了)
                    c := path[0]
                    for i := 0; i < len(n.indices); i++ {
                        // 如果找到了,就继续去 indices 中对应的子节点去找
                        // 并且进入下一次循环
                        if c == n.indices[i] {
                            n = n.children[i]
                            continue walk
                        }
                    }

                    // 如果进入到了这里,代表没有找到对应的子节点

                    // 如果寻找路径正好是 / ,因为去除了公共路径
                    // 假设寻找的是 /hello/ , n.path = hello, 这时 path 就是 /
                    // 当前的节点如果注册了 handle ,就证明这个是一个路由,tsr 就会变成 true
                    // 调用者就知道可以将路由重定向到 /hello 了,下次再来就可以找到这个路由了
                    // 可以去 Router.ServeHTTP 方法看下,是不是会发起一个重定向
                    tsr = (path == "/" && n.handle != nil)

                    // 返回,此时会返回 (nil,[],true|false) 这 3 个值
                    return
                }

                // 处理是通配符节点(包含参数或*)的情况
                // 如果当前节点是通配符节点,子节点肯定只有一个,因为 :name 这种格式的只允许注册一个
                // 所以可以把当前节点替换成 :name 或 *name 这种子节点
                n = n.children[0]
                switch n.nType {
                // 当子节点是参数类型的时候,:name 这种格式的
                case param:
                    // find param end (either '/' or path end)
                    // 查找出寻找路径中到结束或者 / 以前的部分所在的位置
                    // 比如是 :name 或者 :name/more ,end 就会等于 4
                    end := 0
                    for end < len(path) && path[end] != '/' {
                        end++
                    }

                    // save param value
                    // 保存参数的值, 因为参数是一个 slice ,使用前要初始化一下
                    if p == nil {
                        // lazy allocation
                        p = make(Params, 0, n.maxParams)
                        // 现在 p 的值是 []Param
                    }
                    // 下面两行是将 slice 的长度扩展
                    // 如果第一次来, p 是一个空格, i = 0
                    // p = p[:0+1] p 取的是一个位置 0 到 1 的切片,因为切片前开后闭,
                    // 所以它的长度就变成了 1, 下面就可以向切片添加一个值了
                    // 第二次来就是在原有的长度再扩展 1 的长度,然后再添加一个值,
                    // 这样这个参数就会永远是一个有效的最小长度,应该可以提高性能 ?
                    // 不清楚作者为什么这样做,直接 append 不可以吗? 有兴趣的可以去确定下。
                    i := len(p)
                    p = p[:i+1] // expand slice within preallocated capacity

                    // 这个就是给 Parm 赋值, Key 就是去掉 : 后的值,比如 :name 就是 name
                    // Value 就是路径到结束位置 end 的值
                    p[i].Key = n.path[1:]
                    p[i].Value = path[:end]

                    // we need to go deeper!
                    // 上面已经获取了第一个值,但是有可能还会有两个或者更多,所以还要继续处理
                    // 如果之前找到第一个值的位置比寻找路径的长度小,就是还有 :name 后面还有内容需要去匹配
                    if end < len(path) {
                        // 如果还存在子节点, 就将 path 和 node 替换成子节点的,跳出当前循环再找一遍
                        if len(n.children) > 0 {
                            path = path[end:]
                            n = n.children[0]
                            continue walk
                        }

                        // ... but we can't
                        tsr = (len(path) == end+1)
                        // 这里返回的是 (nil, [Parm{name, 路径上的值}], true|false)
                        return
                    }

                    // 如果当前节点保存了 handle ,就将 handle 返回给调用者,去发起业务逻辑
                    if handle = n.handle; handle != nil {
                        return
                    } else if len(n.children) == 1 {
                        // No handle found. Check if a handle for this path + a
                        // trailing slash exists for TSR recommendation
                        // 确认它是否有一个 / 的子节点,如果有 tsr = true
                        n = n.children[0]
                        tsr = (n.path == "/" && n.handle != nil)
                    }

                    // 这里返回的是 [nil, [找到的参数...], true|false]
                    return

                // 如果是 * 这种方式的路由, 就是直接处理参数,并返回,因为它肯定是最后一个节点
                // 所以也不会出现子节点,及时路径上后面根再多的 / 也都只是参数的值
                case catchAll:
                    // save param value
                    if p == nil {
                        // lazy allocation
                        p = make(Params, 0, n.maxParams)
                    }
                    i := len(p)
                    p = p[:i+1] // expand slice within preallocated capacity
                    p[i].Key = n.path[2:]
                    p[i].Value = path

                    handle = n.handle
                    return

                default:
                    // 没想到什么情况会出现无效的类型, 如果出现了就直接 panic
                    // 现在这个 panic 不会导致服务器崩溃,因为在 ServeHTTP 中做了 Recovery
                    panic("invalid node type")
                }
            }
        // 如果查询的路径和节点保存的路径相同
        } else if path == n.path {
            // We should have reached the node containing the handle.
            // Check if this node has a handle registered.
            // 检查下这个节点是否包含 handle ,如果包含就返回
            // 会有不包含的情况,比如恰巧这个就是一个相同前缀的节点
            if handle = n.handle; handle != nil {
                return
            }

            // 如果查找的路径是 / 又不是根节点,还是个参数节点,就允许重定向
            if path == "/" && n.wildChild && n.nType != root {
                tsr = true
                return
            }

            // No handle found. Check if a handle for this path + a
            // trailing slash exists for trailing slash recommendation
            // 如果没有找到,但是有一个 / 的子节点,也允许它重定向
            // 就是这个意思,比如定义了一个 /hello/ 路由,但是访问的是 /hello
            // 也允许它重定向到 /hello/ 上
            for i := 0; i < len(n.indices); i++ {
                if n.indices[i] == '/' {
                    n = n.children[i]
                    tsr = (len(n.path) == 1 && n.handle != nil) ||
                        (n.nType == catchAll && n.children[0].handle != nil)
                    return
                }
            }

            return
        }

        // Nothing found. We can recommend to redirect to the same URL with an
        // extra trailing slash if a leaf exists for that path
        // 是否允许重定向的一个验证
        tsr = (path == "/") ||
            (len(n.path) == len(path)+1 && n.path[len(path)] == '/' &&
                path == n.path[:len(n.path)-1] && n.handle != nil)
        return
    }
}

到这里源码就分析完了,整理的代码量不算多, router.go 文件还好理解,基本看一遍就清除,这个 tree.go ,看了几个小时才基本上看明白,逻辑有点复杂,而且也不是很习惯这个写法。 搞明白了之后再用的时候遇到问题也是很清楚哪里出现的,并且定义路由的时候也知道要怎么去定义了,那种可以支持,那种不可以支持。