Gin 是目前應用比較普遍的Golang web 框架。 目前,Github Star 數已經達到了3.8w. 框架的實現很是簡單,可定製性很是強,性能也比較好,深受golang開發者的喜好。Gin 提供了web開發的一些基本功能。如路由,中間件,日誌,參數獲取等,本文主要從源碼的角度分析Gin的路由實現。node
Gin 的路由功能是基於 https://github.com/julienschmidt/httprouter
這個項目實現的。目前也有不少其餘Web框架也基於該路由框架作了二次開發。git
在 Gin 中,爲了兼容不一樣路由的引擎,定義了 IRoutes 和 IRouter 接口,便於替換其餘的路由實現。(目前默認是httprouter)github
下面是一個路由的接口定義golang
type IRoutes interface { Use(...HandlerFunc) IRoutes Handle(string, string, ...HandlerFunc) IRoutes Any(string, ...HandlerFunc) IRoutes GET(string, ...HandlerFunc) IRoutes POST(string, ...HandlerFunc) IRoutes DELETE(string, ...HandlerFunc) IRoutes PATCH(string, ...HandlerFunc) IRoutes PUT(string, ...HandlerFunc) IRoutes OPTIONS(string, ...HandlerFunc) IRoutes HEAD(string, ...HandlerFunc) IRoutes StaticFile(string, string) IRoutes Static(string, string) IRoutes StaticFS(string, http.FileSystem) IRoutes } type HandlerFunc func(*Context)
HandlerFunc 是一個方法類型的定義,咱們定義的路由其實就是一個路徑與HandlerFunc 的映射關係。
從上面的定義能夠看出,IRoutes 主要定義了一些基於http方法、靜態方法的路徑和一組方法的映射。 Use
方法是針對此路由的全部路徑映射一組方法,在使用上是爲了給這些路由添加中間件。web
除了上面的定義外,Gin 還有路由組的抽象。框架
type IRouter interface { IRoutes Group(string, ...HandlerFunc) *RouterGroup }
路由組是在IRoutes 的基礎上,有了組的概念,組下面還能夠掛在不一樣的組。組的概念能夠很好的管理一組路由,路由組能夠本身定義一套Handler方法(即一組中間件)。oop
我的認爲IRouter的定義Group 應該返回 IRouter,這樣能夠把路由組更加抽象,也不會改變現有服務的使用。期待看下Gin源碼何時會按照這種定義方法修改過來。post
在Gin框架中,路由由 RouterGroup 實現。咱們從構造和路由查找兩個方面分析路由的實現。性能
路由的本質就是在給定 路徑與Handler映射關係 的前提下,當提供新的url時,給出對應func 的過程。其中可能須要從url中提取參數,或者按照 *
匹配 url 的狀況。學習
首先,咱們看下Gin中路由結構的定義。
// gin engine type Engine struct { RouterGroup // ... 其餘字段 trees methodTrees } // 每一個 http 方法定義一個森林 type methodTrees []methodTree type methodTree struct { method string root *node } // 路由組的定義 type RouterGroup struct { Handlers HandlersChain basePath string engine *Engine root bool }
從定義中能夠看出,其實Gin 的 Engine 是複用了 RouterGroup。對於不一樣的 http method,都經過一個森林來存儲路由數據。
下面是森林上每一個節點的定義:
type node struct { path string // 當前路徑 indices string // 對應children 的前綴 wildChild bool // 多是帶參數的,或者是 * 的,因此是野節點 nType nodeType // 參數節點,靜態節點 priority uint32 // 優先級 ,優先級高的放在children 放在前面。 children []*node // 子節點 handlers HandlersChain // 調用鏈 fullPath string // 全路徑 }
從代碼實現上得知,這個森林實際上是一個壓縮版本的Trie樹,每一個節點會存儲前綴相同的路徑數據。下面,咱們經過代碼來學習下路由的添加和刪除。
路由的添加,就是將path路徑添加到定義的Trie樹種,將handlers 添加到對應的node 節點。
func (n *node) addRoute(path string, handlers HandlersChain) { // 初始化和維護優先級 for { // 查找前綴 i := longestCommonPrefix(path, n.path) // 原有路徑長的狀況下 // 節點n 的 path 變爲了公共前綴 // 原有n 的path 路徑變爲了現有n 的子節點 // 當添加的path長的狀況 // 須要分狀況討論: // 1. 若是是一個帶參數的路徑,校驗是否後續路徑不一樣,若是不一樣則繼續掃描下一段路徑 // 2. 若是是帶 * 的路徑, 則直接報錯 // 3. 若是已經有對應的首字母,修改當前node節點,並繼續掃描,並掃描下一段路徑 // 4. 若是非參數或者 * 匹配的方法,則插入一個子節點路徑,並完成掃描 // 最後註冊handlers,添加fullPath n.handlers = handlers n.fullPath = fullPath return } }
從上面的代碼註釋能夠看出,路由的添加,主要是經過不斷對比當前節點的path和添加的path,作添加節點或者節點變動的操做,達到添加path的目的。
在服務請求時,路由的責任就是給定一個url請求,拿到節點保存的handlers,以及url中包含的參數值。下面是對一個url 的解析實現。
type nodeValue struct { handlers HandlersChain params *Params tsr bool fullPath string } func (n *node) getValue(path string, params *Params, unescape bool) (value nodeValue) { walk: // Outer loop for walking the tree for { prefix := n.path // 若是比當前節點路徑要長: // - 非參數類型或模糊匹配的URL,若是和當前節點前綴匹配,直接查看 node 的子節點 // - 參數化的node, 按照 / 分割提取參數,若是未結束,則繼續匹配剩下的路徑,不然返回結果。 // - * 匹配的node,將剩餘的路徑添加到 param 中直接返回。 // 若是和當前節點相等,那就直接返回便可。 // 這裏還作了非本方法的路徑匹配,用戶返回http 方法錯誤的異常報告。 } }
下面經過一個例子,方便咱們快速理解router的實現。
加入下面的一個路徑:
/search/
/support/
/blog/:post/
/about-us/team/
/contact/
在樹中,咱們看到的樣子以下:
Path \ ├s |├earch\ |└upport\ ├blog\ | └:post | └\ ├about-us\ | └team\ └contact\
在作路由查找時,經過路徑不斷匹配,找到對應的子節點。拿到對應子節點下的handler。完成路由的匹配。
net/http
庫的, net/http
庫中handler 的實現是針對不一樣的 http method 的,因此須要在engine 中針對不一樣的method 提供不一樣的trie 樹。