Gin 源碼學習(三)丨路由是如何構建和匹配的?

在前兩篇文章 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 中的許多與我們主題無相關的屬性,如:重定向配置 RedirectTrailingSlashRedirectFixedPath,無路由處理函數切片 noRouteallNoRoute,HTML templates 相關渲染配置 delimsHTMLRender 等。

從上面的 gin.Engine 結構體中,可以發現其嵌入了一個 RouterGroup 結構體,以及還有一個 methodTrees 類型的屬性 trees

gin.Default() 函數內部調用了 gin.New() 函數來創建 Gin 的路由引擎,然后為該引擎添加了 Logger()Recovery() 兩個中間件。

gin.New() 函數用于創建 Gin 路由引擎,其主要用于為該即將被創建的引擎做一些初始化配置。

接下來我們來看一下 gin.Engine 結構體中所引用到的 RouterGroupmethodTree 的結構定義:

// 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 含有兩個屬性 methodroot,這是位于 Gin 路由結構頂端的方法樹,其 method 屬性表示請求的方法類型,如:GETPOSTPUT 等,而 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 表示當前節點的前綴路徑,此處為 /usindices 表示當前節點的孩子節點索引,此處為 eochildren 則用于保存當前節點的孩子節點切片,此處存儲了 patheo 的兩個節點;handlers 保存當前 path 的處理函數切片,此處由于沒有創建針對 /us 的處理函數,因此為 nilpriority 表示當前節點的優先級,孩子節點數量越多,優先級越高,用于調整索引和孩子節點切片順序,提高查找效率;nType 表示當前節點的類型,Gin 定義了四種類型,staticrootparamcatchAllstatic 表示普通節點,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")
}
1.jpg

比較有疑惑的地方,可能是 GET 方法的路由樹的第4~6層,為什么會有兩個 path"" 的節點以及兩個 nTypecatchAll 的節點呢?帶著這個問題,我們來學習 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) 函數中使用該 RouterGroupbasePath 結合傳遞進來的相對路徑參數 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)
}

首先是對傳進來的三個參數 methodpathhandlers 進行斷言,分別是 path 要以 "/" 為前綴,method 不能為空字符串,handlers 切片的長度必須大于 0;

然后是通過傳進來的 method 參數,即 HTTP 方法類型,作為參數來獲取對應方法樹的根節點,如果獲取到的根節點為 nil,則表示不存在該方法樹,這時創建一個新的根節點作為新方法樹的根節點,并將該新的方法樹追加至該引擎的方法樹切片中,最后使用傳遞進來的 pathhandlers 作為參數,調用該根節點內置的 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) 函數,并且該函數的 handlerHandler 接口類型,其源代碼如下:

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 請求的匹配過程,如下圖所示:

2.jpg

小結

這篇文章講解了 Gin 路由的結構、構建以及匹配過程,Gin 內部使用查找樹 Trie 來存儲路由節點。

第一部分講解了 Gin 的路由結構,其中包括 Gin 引擎中使用到的屬性結構以及 Gin 的方法樹,節點結構等。

第二部分講解了 Gin 路由的構建過程,其中最核心的是 n.addRoute(path, handlers) 函數,要看懂其實現,需對查找樹 Trie 有一定了解,否則可能會稍微有點吃力。

第三部分講解了 Gin 路由的匹配過程,其匹配過程也與查找樹查找字典類似。

本系列的下一篇文章將對 Gin 的工作機制進行講解,至此,Gin 源碼學習的第三篇也就到此結束了,感謝大家對本文的閱讀~~

歡迎掃描以下二維碼關注筆者的個人微信訂閱號,準時獲取文章更新通知:

qrcode.jpg
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容