gin框架的路由原始碼解析
前言
本文轉載至 https://www.liwenzhou.com/posts/Go/read_gin_sourcecode/ 可以直接去原文看, 比我這裡直觀
我這裡只是略微的修改
正文
gin的路由實現
使用 Radix Tree , 簡潔版的字首樹
字首樹
別名: 字典樹 / 單詞查詢樹 / 鍵樹
為什麼使用字首樹
-
url是有限的,不可能無限長
-
url是有規律的
-
url是一級一級的, restful 更是如此
比如部落格有的是按年和月分割 /2020/3/aaaa.html /2020/3/bbbb.html 此時使用字首樹更合適
gin的路由樹
基數樹/PAT位樹, 是一種更節省空間的字首樹, 對於基數樹的每個節點,如果該節點是唯一的子樹的話,就和父節點合併。
- 越常匹配的字首, 權重越大
- 因為字首樹的構建模式導致越長的路徑定位的時間越長, gin在註冊路由時越長的路由排的越前, 如果最長的節點能優先匹配, 那麼路由匹配所花的時間不一定比短路由更長
gin首先按照請求型別(POST/GET/...), 分為多個PAT樹, 每個PAT樹儲存這個請求型別下面註冊的路由, 路由又根據權重進行排序
路由樹節點
路由樹由一個個節點組成, gin的路由樹節點由結構體 node 表示, 其構造結構如下
// tree.go type node struct { // 節點路徑,比如上面的s,earch,和upport path string // 和children欄位對應, 儲存的是分裂的分支的第一個字元 // 例如search和support, 那麼s節點的indices對應的"eu" // 代表有兩個分支, 分支的首字母分別是e和u indices string // 兒子節點 children []*node // 處理函式鏈條(切片) handlers HandlersChain // 優先順序,子節點、子子節點等註冊的handler數量 priority uint32 // 節點型別,包括static, root, param, catchAll // static: 靜態節點(預設),比如上面的s,earch等節點 // root: 樹的根節點 // catchAll: 有*匹配的節點 // param: 引數節點 nType nodeType // 路徑上最大引數個數 maxParams uint8 // 節點是否是引數節點,比如上面的:post wildChild bool // 完整路徑 fullPath string }
請求的方法樹
在gin的路由中, 每一個 HTTP Method
(GET/POST/PUT/....) 都對應了一棵PAT樹, 在註冊路由時會呼叫 addRoute
函式
// gin.go func (engine *Engine) addRoute(method, path string, handlers HandlersChain) { // 獲取請求方法對應的樹 root := engine.trees.get(method) if root == nil { // 如果沒有就建立一個 root = new(node) root.fullPath = "/" engine.trees = append(engine.trees, methodTree{method: method, root: root}) } root.addRoute(path, handlers) }
而在gin中, 每一個 Method 對應的樹關係時是存放在一個切片中, engine.trees
的型別是 methodTrees
, 其定義如下
type methodTree struct {
method string
root *node
}
type methodTrees []methodTree // slice
而 engine.trees.get
方法如下,(就是for迴圈)
func (trees methodTrees) get(method string) *node {
for _, tree := range trees {
if tree.method == method {
return tree.root
}
}
return nil
}
使用切片而不是使用map
來儲存, 可能是考慮到節省記憶體, 而且HTTP請求一共就9種, 使用切片也比較合適, 效率也高, 初始化在gin的 engine
中
func New() *Engine {
debugPrintWARNINGNew()
engine := &Engine{
RouterGroup: RouterGroup{
Handlers: nil,
basePath: "/",
root: true,
},
// liwenzhou.com ...
// 初始化容量為9的切片(HTTP1.1請求方法共9種)
trees: make(methodTrees, 0, 9),
// liwenzhou.com...
}
engine.RouterGroup.engine = engine
engine.pool.New = func() interface{} {
return engine.allocateContext()
}
return engine
}
路由匹配
當新的請求進入gin時, 會先經過函式 ServeHTTP
// gin.go
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
// 這裡使用了物件池
c := engine.pool.Get().(*Context)
// 這裡有一個細節就是Get物件後做初始化
c.writermem.reset(w)
c.Request = req
c.reset()
engine.handleHTTPRequest(c) // 我們要找的處理HTTP請求的函式
engine.pool.Put(c) // 處理完請求後將物件放回池子
}
ServeHTTP
呼叫 handleHTTPRequest
函式(節選)
// gin.go
func (engine *Engine) handleHTTPRequest(c *Context) {
// 根據請求方法找到對應的路由樹
t := engine.trees
for i, tl := 0, len(t); i < tl; i++ {
if t[i].method != httpMethod {
continue
}
root := t[i].root
// 在路由樹中根據path查詢
value := root.getValue(rPath, c.Params, unescape)
if value.handlers != nil {
c.handlers = value.handlers
c.Params = value.params
c.fullPath = value.fullPath
c.Next() // 執行函式鏈條
c.writermem.WriteHeaderNow()
return
}
c.handlers = engine.allNoRoute
serveError(c, http.StatusNotFound, default404Body)
}
大致為先COPY一份路由的切片, 先找到與該請求對應的請求型別, 然後在這個請求型別的路由樹種使用 getValue 方法查詢對應的路由, 沒有則返回404