Go Gin原始碼學習(四)
基數樹
這次學習的是Gin中的路由,在學習原始碼一種我們看到了Gin的路由是它的特色。然而基礎資料使用了基數樹也提供了效能的保障。因為路由這部分比較獨立而且邏輯相對複雜,所以需要單獨學習。 首先我們需要了解的是基數樹, 百度百科中的解釋 其中有一個圖可以讓我們更加直觀的看到資料是如何儲存的。 基數樹,相當於是一種字首樹。對於基數樹的每個節點,如果該節點是確定的子樹的話,就和父節點合併。基數樹可用來構建關聯陣列。 在上面的圖裡也可以看到,資料結構會把所有相同字首都提取 剩餘的都作為子節點。
基數樹在Gin中的應用
從上面可以看到基數樹是一個字首樹,圖中也可以看到資料結構。那基數樹在Gin中是如何應用的呢?舉一個例子其實就能看得出來 router.GET("/support", handler1) router.GET("/search", handler2) router.GET("/contact", handler3) router.GET("/group/user/", handler4) router.GET("/group/user/test", handler5) 最終結果為:
/ (handler = nil, indices = "scg") s (handler = nil, indices = "ue") upport (handler = handler1, indices = "") earch (handler = handler2, indices = "") contact (handler = handler3, indices = "") group/user/ (handler = handler4, indices = "u") uest (handler = handler5, indices = "")
可以看到 router使用get方法添加了5個路由,實際儲存結果就是上面顯示的。我特地在後面加上了每個節點中的handler和indices。 indices是有序儲存所有子節點的第一個字元形成的字串 。為什麼要特意突出這個欄位,因為在查詢子節點下面是否包含path的時候不需要迴圈子節點,只需要迴圈這個欄位就可以知道是否包含。這樣的操作也可以提升一些效率。
原始碼檢視
先看一下節點的物件的定義和如何呼叫的,需要注意的是indices這個欄位 上面已經提到了它的作用
type node struct { // 儲存這個節點上的URL路徑 // 例如上圖中的search和support, 共同的parent節點的path="s" // 後面兩個節點的path分別是"earch"和"upport" path string // 判斷當前節點路徑是不是引數節點, 例如上圖的:post部分就是wildChild節點 wildChild bool // 節點型別包括static, root, param, catchAll // static: 靜態節點, 例如上面分裂出來作為parent的s // root: 如果插入的節點是第一個, 那麼是root節點 // catchAll: 有*匹配的節點 // param: 除上面外的節點 nType nodeType // 記錄路徑上最大引數個數 maxParams uint8 // 和children[]對應, 儲存的是分裂的分支的第一個字元 // 例如search和support, 那麼s節點的indices對應的"eu" // 代表有兩個分支, 分支的首字母分別是e和u indices string // 儲存孩子節點 children []*node // 當前節點的處理函式 handle Handle // 優先順序 priority uint32 } //RouterGrou實現的GET方法呼叫了handler func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes { return group.handle("GET", relativePath, handlers) } func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes { //方法計算出路徑,把group中的basepath和relativepath 合併在一起 absolutePath := group.calculateAbsolutePath(relativePath) //合併handler 把group中新增的中介軟體和傳入的handlers合併起來 handlers = group.combineHandlers(handlers) //呼叫addRoute 新增router group.engine.addRoute(httpMethod, absolutePath, handlers) return group.returnObj() }
接下來我們需要看的是addRoute這個方法了,方法體比較長。其實大多的邏輯都在處理帶引數的節點,真正核心的邏輯其實並不多。我把主要的邏輯都寫上了註釋應該還是比較容易理解的。如果看不懂其實一步步debug幾次也能幫助理解。
func (engine *Engine) addRoute(method, path string, handlers HandlersChain) { assert1(path[0] == '/', "path must begin with '/'") assert1(method != "", "HTTP method can not be empty") assert1(len(handlers) > 0, "there must be at least one handler") debugPrintRoute(method, path, handlers) //獲取method的樹的根節點,每個method都有一個根節點,比如GET,POST 都會維護一個根節點 root := engine.trees.get(method) //如果沒有則建立一個節點 if root == nil { root = new(node) engine.trees = append(engine.trees, methodTree{method: method, root: root}) } //正式新增路由 root.addRoute(path, handlers) } func (n *node) addRoute(path string, handlers HandlersChain) { //記錄原始path fullPath := path n.priority++ //統計path中包含多少引數 就是判斷`:`,`*`的數量 最多255個 numParams := countParams(path) //判斷節點是否為空 if len(n.path) > 0 || len(n.children) > 0 { walk: for { // 更新最大引數數量 if numParams > n.maxParams { n.maxParams = numParams } // 找到相同字首 迴圈次數 是取 path 和 n.path 長度的小那個長度 i := 0 max := min(len(path), len(n.path)) //迴圈判斷是否字元相同,相同則i++ 直到最後 for i < max && path[i] == n.path[i] { i++ } //判斷是否有字首相同,如果有相同的則把目前這個節點提取出來作為子節點 //再把相同字首的path部分作為 父節點 //比如n的path = romaned 現在新增路由的path = romanus 相同字首為 roman //步驟為: //1. 提取ed 新建一個child節點 把原來n的屬性都複製過去 //2. 把原來的n的path改為相同字首:roman 為indices新增 子節點的第一個字元:e if i < len(n.path) { child := node{ path:n.path[i:], wildChild: n.wildChild, indices:n.indices, children:n.children, handlers:n.handlers, priority:n.priority - 1, } // Update maxParams (max of all children) for i := range child.children { if child.children[i].maxParams > child.maxParams { child.maxParams = child.children[i].maxParams } } n.children = []*node{&child} // []byte for proper unicode char conversion, see #65 n.indices = string([]byte{n.path[i]}) n.path = path[:i] n.handlers = nil n.wildChild = false } //原先的節點n現在已經分成2個節點了 結構為: //roman 父節點 //ed子節點[0] //那麼現在需要把傳入的路由新增到這個父節點中 //最終結構為 //roman 父節點 //ed 子節點[0] //us 子節點[1] // 其中還有一些情況需要自呼叫 相當於遞迴 舉例說明: //roman //ed //uie //當判斷父節點n 本來就有一個uie子節點 這時候uie和us 又有相同字首u 這個時候需要把這個u再次提取出來作為父節點 所以需要遞迴呼叫walk //最終結果為 三層結構 //roman //ed //u //ie //s //還有一種情況是如果是帶有引數的路由 則也會再次呼叫walk if i < len(path) { path = path[i:] if n.wildChild { n = n.children[0] n.priority++ // Update maxParams of the child node if numParams > n.maxParams { n.maxParams = numParams } numParams-- // Check if the wildcard matches if len(path) >= len(n.path) && n.path == path[:len(n.path)] { // check for longer wildcard, e.g. :name and :names if len(n.path) >= len(path) || path[len(n.path)] == '/' { continue walk } } panic("path segment '" + path + "' conflicts with existing wildcard '" + n.path + "' in path '" + fullPath + "'") } c := path[0] // slash after param if n.nType == param && c == '/' && len(n.children) == 1 { n = n.children[0] n.priority++ continue walk } // Check if a child with the next path byte exists for i := 0; i < len(n.indices); i++ { if c == n.indices[i] { i = n.incrementChildPrio(i) n = n.children[i] continue walk } } // Otherwise insert it if c != ':' && c != '*' { // []byte for proper unicode char conversion, see #65 n.indices += string([]byte{c}) child := &node{ maxParams: numParams, } n.children = append(n.children, child) n.incrementChildPrio(len(n.indices) - 1) n = child } n.insertChild(numParams, path, fullPath, handlers) return } else if i == len(path) { if n.handlers != nil { panic("handlers are already registered for path '" + fullPath + "'") } n.handlers = handlers } return } } else { // 節點為空,直接新增直接新增路由 n.insertChild(numParams, path, fullPath, handlers) n.nType = root } } //新增節點函式 主要處理包含引數節點 func (n *node) insertChild(numParams uint8, path string, fullPath string, handlers HandlersChain) { var offset int // already handled bytes of the 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] { // the wildcard name must not contain ':' and '*' case ':', '*': panic("only one wildcard per path segment is allowed, has: '" + path[i:] + "' in path '" + fullPath + "'") default: end++ } } //檢查這個節點是否存在子節點,如果我們在這裡插入萬用字元,子節點將是不可訪問的 if len(n.children) > 0 { panic("wildcard route '" + path[i:end] + "' conflicts with existing children in path '" + fullPath + "'") } // check if the wildcard has a name if end-i < 2 { panic("wildcards must be named with a non-empty name in path '" + fullPath + "'") } // 引數型別 相當於註冊路由時候帶有: if c == ':' { // split path at the beginning of the wildcard if i > 0 { n.path = path[offset:i] offset = i } child := &node{ nType:param, maxParams: numParams, } n.children = []*node{child} n.wildChild = true n = child n.priority++ numParams-- if end < max { n.path = path[offset:end] offset = end child := &node{ maxParams: numParams, priority:1, } n.children = []*node{child} n = child } } else { //如果是萬用字元* if end != max || numParams > 1 { panic("catch-all routes are only allowed at the end of the path in path '" + fullPath + "'") } 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-- if path[i] != '/' { panic("no / before catch-all in path '" + fullPath + "'") } n.path = path[offset:i] // first node: catchAll node with empty path 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, handlers:handlers, priority:1, } n.children = []*node{child} return } } // 插入路由 如果不包含引數節點 offset為0 n.path = path[offset:] n.handlers = handlers }
最後我們要看下根據path獲取router的方法getRouter。這個方法還是比較簡單的,註釋基本也能明白。
//根據path查詢路由的方法 func (n *node) getValue(path string, po Params, unescape bool) (handlers HandlersChain, p Params, tsr bool) { p = po walk: for { if len(path) > len(n.path) { if path[:len(n.path)] == n.path { path = path[len(n.path):] // 判斷如果不是引數節點 // 那path的第一個字元 迴圈對比indices中的每個字元查詢到子節點 if !n.wildChild { c := path[0] for i := 0; i < len(n.indices); i++ { if c == n.indices[i] { n = n.children[i] continue walk } } tsr = path == "/" && n.handlers != nil return } // handle wildcard child n = n.children[0] switch n.nType { case param: // 如果是普通':'節點, 那麼找到/或者path end, 獲得引數 end := 0 for end < len(path) && path[end] != '/' { end++ } // save param value if cap(p) < int(n.maxParams) { p = make(Params, 0, n.maxParams) } i := len(p) p = p[:i+1] // expand slice within preallocated capacity p[i].Key = n.path[1:] val := path[:end] if unescape { var err error if p[i].Value, err = url.QueryUnescape(val); err != nil { p[i].Value = val // fallback, in case of error } } else { p[i].Value = val } // 如果引數還沒處理完, 繼續walk if end < len(path) { if len(n.children) > 0 { path = path[end:] n = n.children[0] continue walk } // ... but we can't tsr = len(path) == end+1 return } // 否則獲得handle返回就OK if handlers = n.handlers; handlers != nil { return } if len(n.children) == 1 { // No handle found. Check if a handle for this path + a // trailing slash exists for TSR recommendation n = n.children[0] tsr = n.path == "/" && n.handlers != nil } return case catchAll: // *匹配所有引數 if cap(p) < int(n.maxParams) { p = make(Params, 0, n.maxParams) } i := len(p) p = p[:i+1] // expand slice within preallocated capacity p[i].Key = n.path[2:] if unescape { var err error if p[i].Value, err = url.QueryUnescape(path); err != nil { p[i].Value = path // fallback, in case of error } } else { p[i].Value = path } handlers = n.handlers return default: 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. if handlers = n.handlers; handlers != 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 for i := 0; i < len(n.indices); i++ { if n.indices[i] == '/' { n = n.children[i] tsr = (len(n.path) == 1 && n.handlers != nil) || (n.nType == catchAll && n.children[0].handlers != 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.handlers != nil) return } }
總結
Gin的路由是它的特色,其實就是因為他的儲存結構。基數樹的儲存結構可以很快的查詢到對應路由並且執行到handler。避免了每次請求迴圈所有路由的邏輯,提升了Gin整體的效能。試想如果一個大型專案中GET路由有100個,如果每次請求都去迴圈100次查詢效能會很差,如果使用基數樹的儲存方式可能只需要經過幾次的查詢。
Gin路由程式碼很長,其中大部分是處理帶有引數的節點的邏輯。下一次的學習中,還是老規矩,自己模仿著寫一個基數樹儲存結構的路由查詢邏輯。去除掉那些引數邏輯只留下主要核心邏輯。