注:本文已发布超过一年,请注意您所使用工具的相关版本是否适用
在上一篇文章当中阅读了 Go 语言的一个高性能的 Web 框架 Gin,Web 框架当中最重要的功能之一是路由,Gin 的路由就是由 httprouter 这个包实现的
地址
特性
- 基于基数树实现的高性能路由框架
- 仅支持精确匹配
- 不必关心 URL 结尾的斜线
- 路径自动校正,例如在 url 路径当中有
../,//的时候 - 可以在 URL 当中设置参数,例如
/user/:id - 零内存分配
- 不存在服务器崩溃,可以通过设置
panic handler使服务器从 panic 当中恢复 - 适合 API 构建
源码
两个问题
解决两个问题,就基本明白了这个路由框架
- 路由是是如何注册?如何保存的?
- 当请求到来之后,路由是如何匹配,如何查找的?
一个 Demo
还是从一个Hello World讲起
1 2 3 4 5 6 7
| func main() { r := httprouter.New() r.GET("/:name", func(writer http.ResponseWriter, request *http.Request, params httprouter.Params) { fmt.Fprintf(writer, "hello, %s!\n", params.ByName("name")) }) http.ListenAndServe(":8080",r) }
|
httprouter.New()初始化了一个 Router,下面直接看一下 Router 的结构
Router
在 Router 的源码当中有十分详尽的注释,这里按照我个人的理解注释一下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| // Router实现了Http.Handler接口,用于注册分发路由 type Router struct { // trees 是一个基数树集合,每一个HTTP方法对应一棵单独的路由树 // node是基数树的根节点 trees map[string]*node // 用于开启上文提到的自动处理URL尾部斜杆的特性 // 这个值为true时,如果/foo/没有被匹配到,会尝试匹配/foo RedirectTrailingSlash bool // 用于开启上文提到的路由校正的特性 // 这个值为true时,会对../和//这种路径进行校正 RedirectFixedPath bool // 这个值为true时,如果当前方法的路由没有被匹配到,会尝试匹配其他方法的路由, // 如果匹配到了则返回405,如果没有,就交给NotFound Handler处理 HandleMethodNotAllowed bool // 这个值为true时,将开启OPTIONS自动匹配,注意: 手动匹配优先级更高 HandleOPTIONS bool // 没有匹配到相应路由的时候会调用这个方法 // 如果没有注册这个方法会返回 NotFound NotFound http.Handler // 没有匹配到相应路由并且HandleMethodNotAllowed为true时会调用这个方法 MethodNotAllowed http.Handler // 用于从panic当中恢复 // 需要返回500错误,并且渲染相应的错误页面 PanicHandler func(http.ResponseWriter, *http.Request, interface{}) }
|
初始化 Router 之后看看路由是如何保存并且注册的
路由是如何保存的?
这里以官方 Readme 当中的例子说明:
如果注册了以下路由
1 2 3 4 5 6 7 8
| r.GET("/", f1) r.GET("/search/", f2) r.GET("/support/", f3) r.GET("/blog/", f4) r.GET("/blog/:post/", f5) r.GET("/about_us/", f6) r.GET("/about_us/team/", f7) r.GET("/contact/", f8)
|
那么这些路由会如下方所示,以一颗树的形式保存,并且这些路由的公共前缀会被抽离并且变为上一层节点
Priority 表示加上自身一共有多少个节点
Path 表示路径
Handle 表示路由注册的方法
1 2 3 4 5 6 7 8 9 10 11
| 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>
|
r.Handle
r.Get, r.Post等方法实质都是通过调用 r.Handle 实现的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| func (r *Router) Handle(method, path string, handle Handle) { // 路径注册必须从/开始,否则直接报错 if path[0] != '/' { panic("path must begin with '/' in path '" + path + "'") } // 路由树map不存在需要新建 if r.trees == nil { r.trees = make(map[string]*node) } // 获取当前方法所对应树的根节点,不存在则新建一个 root := r.trees[method] if root == nil { root = new(node) r.trees[method] = root } // 向路由树当中添加一条一条路由 root.addRoute(path, handle) }
|
node
路由是注册到一颗路由树当中的,先看看节点的源码,再来分析,是如何添加路由的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| type node struct { // 当前节点的路径 path string // 是否为参数节点,参数节点用:name表示 wildChild bool // 当前节点类型, 一共有4种 // static: 静态节点,默认类型 // root: 根节点 // param: 其他节点 // catchAll: 带有*的节点,这里*的作用和正则当中的*一样 nType nodeType // 当前路径上最大参数的个数,不能超过255 maxParams uint8 // 代表分支的首字母 // 上面的例子,当前节点为s // 那么indices = eu // ├s nil // |├earch\ *<2> // |└upport\ *<3> indices string // 孩子节点 children []*node // 注册的路由 handle Handle // 权重,表示当前节点加上所有子节点的数目 priority uint32 }
|
路由树是如何生成的?
未完待续
关注我获取更新
猜你喜欢