在前兩篇文章 Gin 源碼學習(一)丨請求中 URL 的參數是如何解析的? 和 Gin 源碼學習(二)丨請求體中的參數是如何解析的? 中,都是圍繞著對請求中所攜帶參數的解析來對 Gin 的源碼進行學習的。
在這一篇文章中,將講解前兩篇文章中的實現前提,也是 Gin 的核心功能之一,路由。
那么,帶著 "Gin 中路由是如何構建的" 和 "Gin 是如何進行路由匹配的" 這兩個問題,來開始 Gin 源碼學習的第三篇:路由是如何構建和匹配的?
Go 版本:1.14
Gin 版本:v1.5.0
目錄
- 路由結構
- 路由的構建
- 路由的匹配
- 小結
路由結構
router := gin.Default()
router := gin.New()
在使用 Gin 的時候,我們一般會使用以上兩種方式中的其中一種來創建 Gin 的引擎 gin.Engine
,那么,這個引擎,到底是個什么東西呢?我們一起來看一下 gin.Engine
結構體的定義以及 gin.Default()
和 gin.New()
函數:
type Engine struct {
RouterGroup
trees methodTrees
// 省略多數無相關屬性
}
func Default() *Engine {
debugPrintWARNINGDefault()
engine := New()
engine.Use(Logger(), Recovery())
return engine
}
func New() *Engine {
debugPrintWARNINGNew()
engine := &Engine{
RouterGroup: RouterGroup{
Handlers: nil,
basePath: "/",
root: true,
},
FuncMap: template.FuncMap{},
RedirectTrailingSlash: true,
RedirectFixedPath: false,
HandleMethodNotAllowed: false,
ForwardedByClientIP: true,
AppEngine: defaultAppEngine,
UseRawPath: false,
UnescapePathValues: true,
MaxMultipartMemory: defaultMultipartMemory,
trees: make(methodTrees, 0, 9),
delims: render.Delims{Left: "{{", Right: "}}"},
secureJsonPrefix: "while(1);",
}
engine.RouterGroup.engine = engine
engine.pool.New = func() interface{} {
return engine.allocateContext()
}
return engine
}
此處省略 gin.Engine
中的許多與我們主題無相關的屬性,如:重定向配置 RedirectTrailingSlash
和 RedirectFixedPath
,無路由處理函數切片 noRoute
和 allNoRoute
,HTML templates 相關渲染配置 delims
和 HTMLRender
等。
從上面的 gin.Engine
結構體中,可以發現其嵌入了一個 RouterGroup
結構體,以及還有一個 methodTrees
類型的屬性 trees
。
在 gin.Default()
函數內部調用了 gin.New()
函數來創建 Gin 的路由引擎,然后為該引擎添加了 Logger()
和 Recovery()
兩個中間件。
gin.New()
函數用于創建 Gin 路由引擎,其主要用于為該即將被創建的引擎做一些初始化配置。
接下來我們來看一下 gin.Engine
結構體中所引用到的 RouterGroup
和 methodTree
的結構定義:
// RouterGroup is used internally to configure router, a RouterGroup is associated with
// a prefix and an array of handlers (middleware).
type RouterGroup struct {
Handlers HandlersChain
basePath string
engine *Engine
root bool
}
// HandlersChain defines a HandlerFunc array.
type HandlersChain []HandlerFunc
// HandlerFunc defines the handler used by gin middleware as return value.
type HandlerFunc func(*Context)
type methodTrees []methodTree
type methodTree struct {
method string
root *node
}
從源代碼中給的注釋,我們可以知道 RouterGroup
在 Gin 內部用于配置路由器,其與前綴和處理函數(中間件)數組相關聯。
Handlers
是一個類型為 HandlersChain
的屬性,而 HandlersChain
類型定義的是一個 HandlerFunc
類型的切片,最后 HandlerFunc
類型則是 Gin 中間件使用的處理函數,即其為 Gin 的處理函數對象,所以 RouterGroup.Handlers
為 Gin 的處理函數(中間件)切片;
basePath
則表示該 RouterGroup
所對應的路由前綴;
engine
則是該 RouterGroup
對應 gin.Engine
的引用;
root
表示該 RouterGroup
是否為根,在路由的構建中說明。
接下來是 methodTrees
類型,是一種 methodTree
類型的切片,而 methodTree
含有兩個屬性 method
和 root
,這是位于 Gin 路由結構頂端的方法樹,其 method
屬性表示請求的方法類型,如:GET
,POST
,PUT
等,而 root
屬性則指向對應路由樹的根節點。
下面我們來看一下這個 node
結構體的結構定義:
type node struct {
path string
indices string
children []*node
handlers HandlersChain
priority uint32
nType nodeType
maxParams uint8
wildChild bool
fullPath string
}
const (
static nodeType = iota // default
root
param
catchAll
)
在 Gin 內部,使用查找樹 Trie
存儲路由結構,所以 node
也滿足查找樹 Trie
節點的表示結構。
假如創建了兩個請求方法類型相同的路由 /use
和 /uso
,以該方法樹的根節點為例,path
表示當前節點的前綴路徑,此處為 /us
;indices
表示當前節點的孩子節點索引,此處為 eo
;children
則用于保存當前節點的孩子節點切片,此處存儲了 path
為 e
和 o
的兩個節點;handlers
保存當前 path
的處理函數切片,此處由于沒有創建針對 /us
的處理函數,因此為 nil
;priority
表示當前節點的優先級,孩子節點數量越多,優先級越高,用于調整索引和孩子節點切片順序,提高查找效率;nType
表示當前節點的類型,Gin 定義了四種類型,static
,root
,param
和 catchAll
,static
表示普通節點,root
表示根節點,param
表示通配符節點,匹配以 :
開頭的參數,catchAll
同為通配符節點,匹配以 /*
開頭的參數,與 param
不同之處在于 catchAll
會匹配 /*
后的所有內容;maxParams
表示該路由可匹配到參數的最多數量;wildChild
用于判斷當前節點的孩子節點是否為通配符節點;fullPath
表示當前節點對應的完整路徑。
下面以一個具體例子結合圖片來看一下這個路由樹的結構:
func main() {
router := gin.Default()
router.GET("/users", func(c *gin.Context) {})
router.GET("/user/:id", func(c *gin.Context) {})
router.GET("/user/:id/*action", func(c *gin.Context) {})
router.POST("/create", func(c *gin.Context) {})
router.POST("/deletes", func(c *gin.Context) {})
router.POST("/deleted", func(c *gin.Context) {})
router.DELETE("/use", func(c *gin.Context) {})
router.DELETE("/uso", func(c *gin.Context) {})
router.Run(":8000")
}
比較有疑惑的地方,可能是 GET 方法的路由樹的第4~6層,為什么會有兩個 path
為 ""
的節點以及兩個 nType
為 catchAll
的節點呢?帶著這個問題,我們來學習 Gin 是如何構建路由樹的。
路由的構建
我們先來看一下上面源代碼中的 router.GET(relativePath, handlers)
,router.POST(relativePath, handlers)
和 router.DELETE(relativePath, handlers)
函數:
func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes {
return group.handle("GET", relativePath, handlers)
}
func (group *RouterGroup) POST(relativePath string, handlers ...HandlerFunc) IRoutes {
return group.handle("POST", relativePath, handlers)
}
func (group *RouterGroup) DELETE(relativePath string, handlers ...HandlerFunc) IRoutes {
return group.handle("DELETE", relativePath, handlers)
}
從源代碼中可以發現它們實際上都是對 group.handle(httpMethod, relativePath, handlers)
函數的調用,只不過傳入的 httpMethod
不同,我們來看一下 group.handle(httpMethod, relativePath, handlers)
函數相關的源代碼:
func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes {
absolutePath := group.calculateAbsolutePath(relativePath)
handlers = group.combineHandlers(handlers)
group.engine.addRoute(httpMethod, absolutePath, handlers)
return group.returnObj()
}
func (group *RouterGroup) calculateAbsolutePath(relativePath string) string {
return joinPaths(group.basePath, relativePath)
}
func (group *RouterGroup) combineHandlers(handlers HandlersChain) HandlersChain {
finalSize := len(group.Handlers) + len(handlers)
if finalSize >= int(abortIndex) {
panic("too many handlers")
}
mergedHandlers := make(HandlersChain, finalSize)
copy(mergedHandlers, group.Handlers)
copy(mergedHandlers[len(group.Handlers):], handlers)
return mergedHandlers
}
首先以傳遞進來的相對路徑 relativePath
作為參數,調用 group.calculateAbsolutePath(relativePath)
函數計算并獲取絕對路徑 absolutePath
,在 group.calculateAbsolutePath(relativePath)
函數中使用該 RouterGroup
的 basePath
結合傳遞進來的相對路徑參數 relativePath
調用 joinPaths(absolutePath, relativePath)
函數進行路徑合并操作。
然后以傳遞進來的處理函數切片 handlers
作為參數,調用 group.combineHandlers(handlers)
函數,合并處理函數,在 group.combineHandlers(handlers)
函數中使用該 RouterGroup
自身的 Handlers
的長度與傳遞進來的 handlers
的長度創建新的處理函數切片,并先將 group.Handlers
復制到新創建的處理函數切片中,再將 handlers
復制進去,最后將合并后的處理函數切片返回并重新賦值給 handlers
。
對一個處理函數切片來說,一般除了最后一個處理函數之外的其他處理函數都為中間件,如果使用 gin.Default()
創建路由引擎,那么此處的 Handlers
正常情況下包括 Logger()
和 Recovery()
兩個中間件。
接下來看一下核心的 group.engine.addRoute(method, path, handlers)
函數的源代碼:
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)
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)
}
首先是對傳進來的三個參數 method
,path
和 handlers
進行斷言,分別是 path
要以 "/"
為前綴,method
不能為空字符串,handlers
切片的長度必須大于 0;
然后是通過傳進來的 method
參數,即 HTTP 方法類型,作為參數來獲取對應方法樹的根節點,如果獲取到的根節點為 nil
,則表示不存在該方法樹,這時創建一個新的根節點作為新方法樹的根節點,并將該新的方法樹追加至該引擎的方法樹切片中,最后使用傳遞進來的 path
和 handlers
作為參數,調用該根節點內置的 addRoute(path, handlers)
函數,下面,我們來看一下該函數的源代碼:
func (n *node) addRoute(path string, handlers HandlersChain) {
fullPath := path
n.priority++
// 根據 path 中的 "/" 和 "*" 計算 param 數量
numParams := countParams(path)
parentFullPathIndex := 0
// non-empty tree
if len(n.path) > 0 || len(n.children) > 0 {
walk:
for {
// Update maxParams of the current node
if numParams > n.maxParams {
n.maxParams = numParams
}
// Find the longest common prefix.
// This also implies that the common prefix contains no ':' or '*'
// since the existing key can't contain those chars.
// 計算 path 與 n.path 的公共前綴長度
// 假如 path="/user/:id", n.path="/users"
// 則他們的公共前綴 i=5
i := 0
max := min(len(path), len(n.path))
for i < max && path[i] == n.path[i] {
i++
}
// Split edge
// 如果 i < n.path,表示需要進行節點分裂
// 假如 path="/user/:id", n.path="/users"
// 由于 i=5 < len(n.path), 則對 n 進行分裂, 為其添加 path="s" 的孩子節點
if i < len(n.path) {
child := node{
path: n.path[i:],
wildChild: n.wildChild,
indices: n.indices,
children: n.children, // 將 n 節點中的所有 children 轉移至 child.children 中
handlers: n.handlers,
priority: n.priority - 1,
fullPath: n.fullPath,
}
// Update maxParams (max of all children)
// 更新該 child 節點的 maxParams
for i := range child.children {
if child.children[i].maxParams > child.maxParams {
child.maxParams = child.children[i].maxParams
}
}
// 修改 n 中的 children 僅為當前創建的 child 節點
n.children = []*node{&child}
// []byte for proper unicode char conversion, see #65
// 修改 n 中的索引 indices 為分裂節點的首字符
n.indices = string([]byte{n.path[i]})
// 修改 n.path 為分裂位置之前的路徑值
n.path = path[:i]
n.handlers = nil
n.wildChild = false
n.fullPath = fullPath[:parentFullPathIndex+i]
}
// Make new node a child of this node
// 將新節點添加至 n 的子節點
// 假設 n{path: "/", fullPath: "/user/:id", wildChild: true}, path="/:id/*action"
// 則 i=1, i < path
// 這時 n 不需要分裂子節點, 并且新節點將成為 n 的子孫節點
if i < len(path) {
// 同樣以 n{path: "/", fullPath: "/user/:id", wildChild: true}, path="/:id/*action" 為例
// path=":id/*action"
path = path[i:]
// 如果 n 為通配符節點, 即 nType 為 param 或 catchAll 的上一個節點
if n.wildChild {
// 無需再對 n 進行匹配, 直接移動當前父節點完整路徑游標
parentFullPathIndex += len(n.path)
// 將 n 設置為 n 的子節點 (通配符節點只會有一個子節點)
n = n.children[0]
// 增加新的 n 的優先級
n.priority++
// Update maxParams of the child node
// 更新新的 n 的最大可匹配參數值 maxParams
if numParams > n.maxParams {
n.maxParams = numParams
}
// 由于已遇到通配符節點, 因此當前要添加 path 的 numParams 減 1
numParams--
// Check if the wildcard matches
// 檢查通配符是否匹配
// 如當前 n.path 已匹配至 ":id"
// 而 path 為 ":id/*action"
// 此時 n.path=":id" == path[:len(n.path)]=":id"
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
}
}
pathSeg := path
if n.nType != catchAll {
pathSeg = strings.SplitN(path, "/", 2)[0]
}
prefix := fullPath[:strings.Index(fullPath, pathSeg)] + n.path
panic("'" + pathSeg +
"' in new path '" + fullPath +
"' conflicts with existing wildcard '" + n.path +
"' in existing prefix '" + prefix +
"'")
}
c := path[0]
// slash after param
// 假設 n={path: ":id", fullPath: "/user/:id", indices: "/", nType=param}, path="/:post/*action", fullPath="/user/:id/:post/*action"
// 如果 n 還存在孩子節點, 則將 n 修改為其孩子節點, 從該孩子節點繼續為 path 匹配合適位置
if n.nType == param && c == '/' && len(n.children) == 1 {
parentFullPathIndex += len(n.path)
n = n.children[0]
n.priority++
continue walk
}
// Check if a child with the next path byte exists
// 檢查 n 中是否存在符合 path 的索引, 若存在則將該索引對應的節點賦值給 n, 從該節點繼續為 path 匹配合適位置
// 假設 n={path: "/user", fullPath: "/user", indices: "/s"}, path="/:id/*action", c="/"
for i := 0; i < len(n.indices); i++ {
if c == n.indices[i] {
parentFullPathIndex += len(n.path)
i = n.incrementChildPrio(i)
n = n.children[i]
continue walk
}
}
// Otherwise insert it
// 假設 n={path: "/user", fullPath: "/user"}, path="/:id", fullPath="/user/:id"
// 那么直接將該 path 為 "/:id", fullPath 為 "/user/:id" 的新節點添加至 n 的子節點中
if c != ':' && c != '*' {
// []byte for proper unicode char conversion, see #65
n.indices += string([]byte{c})
child := &node{
maxParams: numParams,
fullPath: fullPath,
}
n.children = append(n.children, child)
// 增加 n 孩子節點的優先級
n.incrementChildPrio(len(n.indices) - 1)
n = child
}
// 將該 path 添加至 n 的孩子節點中
n.insertChild(numParams, path, fullPath, handlers)
return
} else if i == len(path) { // Make node a (in-path) leaf
if n.handlers != nil {
panic("handlers are already registered for path '" + fullPath + "'")
}
n.handlers = handlers
}
return
}
} else { // Empty tree
// 當前樹為空, 直接將該 path 添加至 n 的孩子節點中
n.insertChild(numParams, path, fullPath, handlers)
// 設置該節點為 root 節點
n.nType = root
}
}
該部分的源代碼內容有點多,而且有點繞,建議配合第一部分末尾給出的路由樹圖觀看,其中 n.incrementChildPrio(post)
函數用于為新組合的子節點添加優先級,并且在必要時,對索引以及子節點切片進行重新排序,n.insertChild(numParams, path, fullPath, handlers)
函數用于創建新節點,同時設置其節點類型,處理函數等,并將其插入至 n 的子節點中。
以上是 Gin 路由樹的構建過程,該部分稍微比較復雜,且需要對查找樹 Trie
有一定了解。
路由的匹配
講完路由的構建,我們來看看 Gin 是如何實現路由匹配的,看一下下面的這段代碼:
func main() {
router := gin.Default()
router.GET("/users", func(c *gin.Context) {})
router.GET("/user/:id", func(c *gin.Context) {})
router.GET("/user/:id/*action", func(c *gin.Context) {})
router.POST("/create", func(c *gin.Context) {})
router.POST("/deletes", func(c *gin.Context) {})
router.POST("/deleted", func(c *gin.Context) {})
router.DELETE("/use", func(c *gin.Context) {})
router.DELETE("/uso", func(c *gin.Context) {})
router.Run(":8000")
}
func (engine *Engine) Run(addr ...string) (err error) {
defer func() { debugPrintError(err) }()
address := resolveAddress(addr)
debugPrint("Listening and serving HTTP on %s\n", address)
err = http.ListenAndServe(address, engine)
return
}
從源代碼中可以發現,Gin 內部實際上調用了 Go 自帶函數庫 net/http
庫中的 http.ListenAndServe(addr, handler)
函數,并且該函數的 handler
為 Handler
接口類型,其源代碼如下:
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
由此,我們可以知道,在 Gin 的 Engine
結構中,實現了該接口,所以,我們只需把關注點放到 Gin 實現 Handler
接口的 ServeHTTP(ResponseWriter, *Request)
函數中即可,下面我們來看一下 Gin 對該接口的實現源代碼:
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
c := engine.pool.Get().(*Context)
c.writermem.reset(w)
c.Request = req
c.reset()
engine.handleHTTPRequest(c)
engine.pool.Put(c)
}
首先是從引擎的對象池中獲取一個 Gin 的上下文對象,并對其屬性進行重置操作,至于 Gin 上下文的內容這里不做展開討論,在本系列的后續文章中,會與 Go 自帶函數庫中的 context
庫結合討論。
然后以該 Context
對象作為參數調用 engine.handleHTTPRequest(c)
函數對請求進行處理,最后再將該 Context
重新放入該 Gin 引擎的對象池中。下面來看一下該函數的源代碼,在本系列的第一篇文章 Gin 源碼學習(一)丨請求中 URL 的參數是如何解析的? 中有對其稍微介紹過,所以我們這里同樣,只針對路由匹配的內容來對其進行講解:
func (engine *Engine) handleHTTPRequest(c *Context) {
httpMethod := c.Request.Method
rPath := c.Request.URL.Path
// 省略部分代碼
// Find root of the tree for the given HTTP method
t := engine.trees
for i, tl := 0, len(t); i < tl; i++ {
// 遍歷方法樹切片, 獲取與請求方法相同的方法樹的根節點
if t[i].method != httpMethod {
continue
}
root := t[i].root
// Find route in tree
// 根據請求 URI 從該方法樹中進行路由匹配并獲取請求參數
value := root.getValue(rPath, c.Params, unescape)
// 如果獲取到的 value.handlers 不為 nil, 表示路由樹中存在處理該 URI 的路由
if value.handlers != nil {
c.handlers = value.handlers
c.Params = value.params
c.fullPath = value.fullPath
c.Next()
c.writermem.WriteHeaderNow()
return
}
// 如果無匹配路由, 并且請求方法不為 "CONNECT", 請求的 URI 不為 "/"
// 則判斷是否開啟重定向配置, 若開啟, 則進行重定向操作
if httpMethod != "CONNECT" && rPath != "/" {
if value.tsr && engine.RedirectTrailingSlash {
redirectTrailingSlash(c)
return
}
if engine.RedirectFixedPath && redirectFixedPath(c, root, engine.RedirectFixedPath) {
return
}
}
break
}
// 如果開啟 HandleMethodNotAllowed, 則在其他請求類型的方法樹中進行匹配
if engine.HandleMethodNotAllowed {
for _, tree := range engine.trees {
if tree.method == httpMethod {
continue
}
// 如果在其他請求類型的方法樹中能夠匹配到該請求 URI, 并且處理函數切片不為空, 則返回 405 錯誤
if value := tree.root.getValue(rPath, nil, unescape); value.handlers != nil {
c.handlers = engine.allNoMethod
serveError(c, http.StatusMethodNotAllowed, default405Body)
return
}
}
}
// 返回 404 錯誤
c.handlers = engine.allNoRoute
serveError(c, http.StatusNotFound, default404Body)
}
從上面源代碼中,我們可以發現,路由的匹配操作,是在 root.getValue(rPath, po, unescape)
函數中進行的,下面我們來看一下該函數的源代碼并結合具體實例來對其進行分析,該函數同樣在本系列的第一篇文章中出現過,此處僅對路由匹配的內容進行講解:
func (n *node) getValue(path string, po Params, unescape bool) (value nodeValue) {
value.params = po
walk: // Outer loop for walking the tree
// 使用 for 循環進行節點訪問匹配操作
for {
// 判斷當前請求的 path 長度是否比當前節點的 n.path 長
// 如果是, 則使用當前節點的 n.path 與 path 進行匹配
if len(path) > len(n.path) {
// 判斷當前路由節點的 path 與請求的 path 前綴是否完全一致
if path[:len(n.path)] == n.path {
// 對請求的 path 進行重新截取, 去除與當前節點完全匹配的前綴部分
path = path[len(n.path):]
// If this node does not have a wildcard (param or catchAll)
// child, we can just look up the next child node and continue
// to walk down the tree
// 如果當前節點不為通配符節點
if !n.wildChild {
// 獲取請求 path 的第一個字符
c := path[0]
// 遍歷當前路由節點的 indices, 判斷是否存在與請求 path 匹配的索引
for i := 0; i < len(n.indices); i++ {
if c == n.indices[i] {
// 如果存在, 將當前路由節點修改為該子節點
n = n.children[i]
// 跳轉至 walk, 開始下一輪匹配
continue walk
}
}
// Nothing found.
// We can recommend to redirect to the same URL without a
// trailing slash if a leaf exists for that path.
value.tsr = path == "/" && n.handlers != nil
return
}
// handle wildcard child
// 當前節點為通配符節點
// 表示其僅有一個子節點, 且節點類型為 param 或者 catchAll
n = n.children[0]
switch n.nType {
case param: // 如果當前路由節點類型為 param
// find param end (either '/' or path end)
end := 0
for end < len(path) && path[end] != '/' {
end++
}
// save param value
if cap(value.params) < int(n.maxParams) {
value.params = make(Params, 0, n.maxParams)
}
i := len(value.params)
value.params = value.params[:i+1] // expand slice within preallocated capacity
value.params[i].Key = n.path[1:]
val := path[:end]
if unescape {
var err error
if value.params[i].Value, err = url.QueryUnescape(val); err != nil {
value.params[i].Value = val // fallback, in case of error
}
} else {
value.params[i].Value = val
}
// we need to go deeper!
// 如果用于匹配參數的 end 下標小于當前請求 path 的長度
if end < len(path) {
// 如果當前路由節點存在孩子節點
if len(n.children) > 0 {
// 對當前請求 path 進行重新截取
path = path[end:]
// 獲取當前路由節點的孩子節點
n = n.children[0]
// 跳轉至 walk, 開始下一輪匹配
continue walk
}
// ... but we can't
value.tsr = len(path) == end+1
return
}
// 如果當前的 handlers 不為空, 則返回
if value.handlers = n.handlers; value.handlers != nil {
value.fullPath = n.fullPath
return
}
// 如果當前路由節點有一個子節點
if len(n.children) == 1 {
// No handle found. Check if a handle for this path + a
// trailing slash exists for TSR recommendation
// 沒有找到處理該請求 path 的處理函數
// 如果當前路由節點的子節點的 path 為 "/" 且存在處理函數
// 則設置 value.tsr 為true
n = n.children[0]
value.tsr = n.path == "/" && n.handlers != nil
}
return
case catchAll: // 如果當前路由節點的類型為 catchAll
// 直接將當前的請求 path 存儲至 value.params 中
// save param value
if cap(value.params) < int(n.maxParams) {
value.params = make(Params, 0, n.maxParams)
}
i := len(value.params)
value.params = value.params[:i+1] // expand slice within preallocated capacity
value.params[i].Key = n.path[2:]
if unescape {
var err error
if value.params[i].Value, err = url.QueryUnescape(path); err != nil {
value.params[i].Value = path // fallback, in case of error
}
} else {
value.params[i].Value = path
}
value.handlers = n.handlers
value.fullPath = n.fullPath
return
default:
panic("invalid node type")
}
}
} else if path == n.path { // 如果當前請求的 path 與當前節點的 path 相同
// We should have reached the node containing the handle.
// Check if this node has a handle registered.
// 由于路由已匹配完成, 因此只需檢查當前已創建的路由節點中是否存在處理函數
// 如果存在處理函數, 則直接返回
if value.handlers = n.handlers; value.handlers != nil {
value.fullPath = n.fullPath
return
}
// 如果當前匹配的路由節點中不存在處理函數
// 且當前請求的 path 為 "/", 并且當前節點的子節點為 param 節點或 catchAll 節點, 且當前節點不為 root 節點
// 則設置 tsr(trailing slash redirect, 尾部斜線重定向) 為 true, 并返回
if path == "/" && n.wildChild && n.nType != root {
value.tsr = true
return
}
// No handle found. Check if a handle for this path + a
// trailing slash exists for trailing slash recommendation
// 沒有找到匹配路由的處理函數
// 檢查該路由節點是否存在 path 僅為 "/" 且處理函數不為空的子節點, 或者節點類型為 catchAll 且處理函數不為空的子節點, 若存在, 則設置 tsr 為 true, 并返回
for i := 0; i < len(n.indices); i++ {
if n.indices[i] == '/' {
n = n.children[i]
value.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
// 當前請求的 path 的長度比當前路由節點的 path 的長度短
// 嘗試在請求的 path 尾部添加 "/", 如果添加后的請求 path 與當前路由節點的 path 相同, 且當前路由節點存在處理函數, 則設置 tsr 為 true, 并返回
value.tsr = (path == "/") ||
(len(n.path) == len(path)+1 && n.path[len(path)] == '/' &&
path == n.path[:len(n.path)-1] && n.handlers != nil)
return
}
}
例如,一個 URI 為 /user/1/send
的 GET 請求的匹配過程,如下圖所示:
小結
這篇文章講解了 Gin 路由的結構、構建以及匹配過程,Gin 內部使用查找樹 Trie
來存儲路由節點。
第一部分講解了 Gin 的路由結構,其中包括 Gin 引擎中使用到的屬性結構以及 Gin 的方法樹,節點結構等。
第二部分講解了 Gin 路由的構建過程,其中最核心的是 n.addRoute(path, handlers)
函數,要看懂其實現,需對查找樹 Trie
有一定了解,否則可能會稍微有點吃力。
第三部分講解了 Gin 路由的匹配過程,其匹配過程也與查找樹查找字典類似。
本系列的下一篇文章將對 Gin 的工作機制進行講解,至此,Gin 源碼學習的第三篇也就到此結束了,感謝大家對本文的閱讀~~
歡迎掃描以下二維碼關注筆者的個人微信訂閱號,準時獲取文章更新通知: