Gin 源碼學(xué)習(xí)(三)丨路由是如何構(gòu)建和匹配的?

在前兩篇文章 Gin 源碼學(xué)習(xí)(一)丨請求中 URL 的參數(shù)是如何解析的?Gin 源碼學(xué)習(xí)(二)丨請求體中的參數(shù)是如何解析的? 中,都是圍繞著對請求中所攜帶參數(shù)的解析來對 Gin 的源碼進(jìn)行學(xué)習(xí)的。

在這一篇文章中,將講解前兩篇文章中的實(shí)現(xiàn)前提,也是 Gin 的核心功能之一,路由。

那么,帶著 "Gin 中路由是如何構(gòu)建的" 和 "Gin 是如何進(jìn)行路由匹配的" 這兩個(gè)問題,來開始 Gin 源碼學(xué)習(xí)的第三篇:路由是如何構(gòu)建和匹配的?

Go 版本:1.14

Gin 版本:v1.5.0

目錄

  • 路由結(jié)構(gòu)
  • 路由的構(gòu)建
  • 路由的匹配
  • 小結(jié)

路由結(jié)構(gòu)

router := gin.Default()
router := gin.New()

在使用 Gin 的時(shí)候,我們一般會(huì)使用以上兩種方式中的其中一種來創(chuàng)建 Gin 的引擎 gin.Engine,那么,這個(gè)引擎,到底是個(gè)什么東西呢?我們一起來看一下 gin.Engine 結(jié)構(gòu)體的定義以及 gin.Default()gin.New() 函數(shù):

type Engine struct {
    RouterGroup
    trees methodTrees
    // 省略多數(shù)無相關(guān)屬性
}

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 中的許多與我們主題無相關(guān)的屬性,如:重定向配置 RedirectTrailingSlashRedirectFixedPath,無路由處理函數(shù)切片 noRouteallNoRoute,HTML templates 相關(guān)渲染配置 delimsHTMLRender 等。

從上面的 gin.Engine 結(jié)構(gòu)體中,可以發(fā)現(xiàn)其嵌入了一個(gè) RouterGroup 結(jié)構(gòu)體,以及還有一個(gè) methodTrees 類型的屬性 trees

gin.Default() 函數(shù)內(nèi)部調(diào)用了 gin.New() 函數(shù)來創(chuàng)建 Gin 的路由引擎,然后為該引擎添加了 Logger()Recovery() 兩個(gè)中間件。

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

接下來我們來看一下 gin.Engine 結(jié)構(gòu)體中所引用到的 RouterGroupmethodTree 的結(jié)構(gòu)定義:

// 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 內(nèi)部用于配置路由器,其與前綴處理函數(shù)(中間件)數(shù)組相關(guān)聯(lián)。

Handlers 是一個(gè)類型為 HandlersChain 的屬性,而 HandlersChain 類型定義的是一個(gè) HandlerFunc 類型的切片,最后 HandlerFunc 類型則是 Gin 中間件使用的處理函數(shù),即其為 Gin 的處理函數(shù)對象,所以 RouterGroup.Handlers 為 Gin 的處理函數(shù)(中間件)切片;

basePath 則表示該 RouterGroup 所對應(yīng)的路由前綴;

engine 則是該 RouterGroup 對應(yīng) gin.Engine 的引用;

root 表示該 RouterGroup 是否為根,在路由的構(gòu)建中說明。

接下來是 methodTrees 類型,是一種 methodTree 類型的切片,而 methodTree 含有兩個(gè)屬性 methodroot,這是位于 Gin 路由結(jié)構(gòu)頂端的方法樹,其 method 屬性表示請求的方法類型,如:GETPOSTPUT 等,而 root 屬性則指向?qū)?yīng)路由樹的根節(jié)點(diǎn)。

下面我們來看一下這個(gè) node 結(jié)構(gòu)體的結(jié)構(gòu)定義:

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 內(nèi)部,使用查找樹 Trie 存儲(chǔ)路由結(jié)構(gòu),所以 node 也滿足查找樹 Trie 節(jié)點(diǎn)的表示結(jié)構(gòu)。

假如創(chuàng)建了兩個(gè)請求方法類型相同的路由 /use/uso,以該方法樹的根節(jié)點(diǎn)為例,path 表示當(dāng)前節(jié)點(diǎn)的前綴路徑,此處為 /usindices 表示當(dāng)前節(jié)點(diǎn)的孩子節(jié)點(diǎn)索引,此處為 eochildren 則用于保存當(dāng)前節(jié)點(diǎn)的孩子節(jié)點(diǎn)切片,此處存儲(chǔ)了 patheo 的兩個(gè)節(jié)點(diǎn);handlers 保存當(dāng)前 path 的處理函數(shù)切片,此處由于沒有創(chuàng)建針對 /us 的處理函數(shù),因此為 nilpriority 表示當(dāng)前節(jié)點(diǎn)的優(yōu)先級(jí),孩子節(jié)點(diǎn)數(shù)量越多,優(yōu)先級(jí)越高,用于調(diào)整索引和孩子節(jié)點(diǎn)切片順序,提高查找效率;nType 表示當(dāng)前節(jié)點(diǎn)的類型,Gin 定義了四種類型,staticrootparamcatchAllstatic 表示普通節(jié)點(diǎn),root 表示根節(jié)點(diǎn),param 表示通配符節(jié)點(diǎn),匹配以 : 開頭的參數(shù),catchAll 同為通配符節(jié)點(diǎn),匹配以 /* 開頭的參數(shù),與 param 不同之處在于 catchAll 會(huì)匹配 /* 后的所有內(nèi)容;maxParams 表示該路由可匹配到參數(shù)的最多數(shù)量;wildChild 用于判斷當(dāng)前節(jié)點(diǎn)的孩子節(jié)點(diǎn)是否為通配符節(jié)點(diǎn);fullPath 表示當(dāng)前節(jié)點(diǎn)對應(yīng)的完整路徑。

下面以一個(gè)具體例子結(jié)合圖片來看一下這個(gè)路由樹的結(jié)構(gòu):

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層,為什么會(huì)有兩個(gè) path"" 的節(jié)點(diǎn)以及兩個(gè) nTypecatchAll 的節(jié)點(diǎn)呢?帶著這個(gè)問題,我們來學(xué)習(xí) Gin 是如何構(gòu)建路由樹的。

路由的構(gòu)建

我們先來看一下上面源代碼中的 router.GET(relativePath, handlers)router.POST(relativePath, handlers)router.DELETE(relativePath, handlers) 函數(shù):

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)
}

從源代碼中可以發(fā)現(xiàn)它們實(shí)際上都是對 group.handle(httpMethod, relativePath, handlers) 函數(shù)的調(diào)用,只不過傳入的 httpMethod 不同,我們來看一下 group.handle(httpMethod, relativePath, handlers) 函數(shù)相關(guān)的源代碼:

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
}

首先以傳遞進(jìn)來的相對路徑 relativePath 作為參數(shù),調(diào)用 group.calculateAbsolutePath(relativePath) 函數(shù)計(jì)算并獲取絕對路徑 absolutePath,在 group.calculateAbsolutePath(relativePath) 函數(shù)中使用該 RouterGroupbasePath 結(jié)合傳遞進(jìn)來的相對路徑參數(shù) relativePath 調(diào)用 joinPaths(absolutePath, relativePath) 函數(shù)進(jìn)行路徑合并操作。

然后以傳遞進(jìn)來的處理函數(shù)切片 handlers 作為參數(shù),調(diào)用 group.combineHandlers(handlers) 函數(shù),合并處理函數(shù),在 group.combineHandlers(handlers) 函數(shù)中使用該 RouterGroup 自身的 Handlers 的長度與傳遞進(jìn)來的 handlers 的長度創(chuàng)建新的處理函數(shù)切片,并先將 group.Handlers 復(fù)制到新創(chuàng)建的處理函數(shù)切片中,再將 handlers 復(fù)制進(jìn)去,最后將合并后的處理函數(shù)切片返回并重新賦值給 handlers

對一個(gè)處理函數(shù)切片來說,一般除了最后一個(gè)處理函數(shù)之外的其他處理函數(shù)都為中間件,如果使用 gin.Default() 創(chuàng)建路由引擎,那么此處的 Handlers 正常情況下包括 Logger()Recovery() 兩個(gè)中間件。

接下來看一下核心的 group.engine.addRoute(method, path, handlers) 函數(shù)的源代碼:

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)
}

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

然后是通過傳進(jìn)來的 method 參數(shù),即 HTTP 方法類型,作為參數(shù)來獲取對應(yīng)方法樹的根節(jié)點(diǎn),如果獲取到的根節(jié)點(diǎn)為 nil,則表示不存在該方法樹,這時(shí)創(chuàng)建一個(gè)新的根節(jié)點(diǎn)作為新方法樹的根節(jié)點(diǎn),并將該新的方法樹追加至該引擎的方法樹切片中,最后使用傳遞進(jìn)來的 pathhandlers 作為參數(shù),調(diào)用該根節(jié)點(diǎn)內(nèi)置的 addRoute(path, handlers) 函數(shù),下面,我們來看一下該函數(shù)的源代碼:

func (n *node) addRoute(path string, handlers HandlersChain) {
    fullPath := path
    n.priority++
    // 根據(jù) path 中的 "/" 和 "*" 計(jì)算 param 數(shù)量
    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.
            // 計(jì)算 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,表示需要進(jìn)行節(jié)點(diǎn)分裂
            // 假如 path="/user/:id", n.path="/users"
            // 由于 i=5 < len(n.path), 則對 n 進(jìn)行分裂, 為其添加 path="s" 的孩子節(jié)點(diǎn)
            if i < len(n.path) {
                child := node{
                    path:      n.path[i:],
                    wildChild: n.wildChild,
                    indices:   n.indices,
                    children:  n.children,  // 將 n 節(jié)點(diǎn)中的所有 children 轉(zhuǎn)移至 child.children 中
                    handlers:  n.handlers,
                    priority:  n.priority - 1,
                    fullPath:  n.fullPath,
                }

                // Update maxParams (max of all children)
                // 更新該 child 節(jié)點(diǎn)的 maxParams
                for i := range child.children {
                    if child.children[i].maxParams > child.maxParams {
                        child.maxParams = child.children[i].maxParams
                    }
                }

                // 修改 n 中的 children 僅為當(dāng)前創(chuàng)建的 child 節(jié)點(diǎn)
                n.children = []*node{&child}
                // []byte for proper unicode char conversion, see #65
                // 修改 n 中的索引 indices 為分裂節(jié)點(diǎn)的首字符
                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
            // 將新節(jié)點(diǎn)添加至 n 的子節(jié)點(diǎn)
            // 假設(shè) n{path: "/", fullPath: "/user/:id", wildChild: true}, path="/:id/*action"
            // 則 i=1, i < path
            // 這時(shí) n 不需要分裂子節(jié)點(diǎn), 并且新節(jié)點(diǎn)將成為 n 的子孫節(jié)點(diǎn)
            if i < len(path) {
                // 同樣以 n{path: "/", fullPath: "/user/:id", wildChild: true}, path="/:id/*action" 為例
                // path=":id/*action"
                path = path[i:]

                // 如果 n 為通配符節(jié)點(diǎn), 即 nType 為 param 或 catchAll 的上一個(gè)節(jié)點(diǎn)
                if n.wildChild {
                    // 無需再對 n 進(jìn)行匹配, 直接移動(dòng)當(dāng)前父節(jié)點(diǎn)完整路徑游標(biāo)
                    parentFullPathIndex += len(n.path)
                    // 將 n 設(shè)置為 n 的子節(jié)點(diǎn) (通配符節(jié)點(diǎn)只會(huì)有一個(gè)子節(jié)點(diǎn))
                    n = n.children[0]
                    // 增加新的 n 的優(yōu)先級(jí)
                    n.priority++

                    // Update maxParams of the child node
                    // 更新新的 n 的最大可匹配參數(shù)值 maxParams
                    if numParams > n.maxParams {
                        n.maxParams = numParams
                    }
                    // 由于已遇到通配符節(jié)點(diǎn), 因此當(dāng)前要添加 path 的 numParams 減 1
                    numParams--

                    // Check if the wildcard matches
                    // 檢查通配符是否匹配
                    // 如當(dāng)前 n.path 已匹配至 ":id"
                    // 而 path 為 ":id/*action"
                    // 此時(shí) 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
                        // 繼續(xù)檢查更長的通配符
                        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
                // 假設(shè) n={path: ":id", fullPath: "/user/:id", indices: "/", nType=param}, path="/:post/*action", fullPath="/user/:id/:post/*action"
                // 如果 n 還存在孩子節(jié)點(diǎn), 則將 n 修改為其孩子節(jié)點(diǎn), 從該孩子節(jié)點(diǎn)繼續(xù)為 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 的索引, 若存在則將該索引對應(yīng)的節(jié)點(diǎn)賦值給 n, 從該節(jié)點(diǎn)繼續(xù)為 path 匹配合適位置
                // 假設(shè) 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
                // 假設(shè) n={path: "/user", fullPath: "/user"}, path="/:id", fullPath="/user/:id"
                // 那么直接將該 path 為 "/:id", fullPath 為 "/user/:id" 的新節(jié)點(diǎn)添加至 n 的子節(jié)點(diǎ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 孩子節(jié)點(diǎn)的優(yōu)先級(jí)
                    n.incrementChildPrio(len(n.indices) - 1)
                    n = child
                }
                // 將該 path 添加至 n 的孩子節(jié)點(diǎ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
        // 當(dāng)前樹為空, 直接將該 path 添加至 n 的孩子節(jié)點(diǎn)中
        n.insertChild(numParams, path, fullPath, handlers)
        // 設(shè)置該節(jié)點(diǎn)為 root 節(jié)點(diǎn)
        n.nType = root
    }
}

該部分的源代碼內(nèi)容有點(diǎn)多,而且有點(diǎn)繞,建議配合第一部分末尾給出的路由樹圖觀看,其中 n.incrementChildPrio(post) 函數(shù)用于為新組合的子節(jié)點(diǎn)添加優(yōu)先級(jí),并且在必要時(shí),對索引以及子節(jié)點(diǎn)切片進(jìn)行重新排序,n.insertChild(numParams, path, fullPath, handlers) 函數(shù)用于創(chuàng)建新節(jié)點(diǎn),同時(shí)設(shè)置其節(jié)點(diǎn)類型,處理函數(shù)等,并將其插入至 n 的子節(jié)點(diǎn)中。

以上是 Gin 路由樹的構(gòu)建過程,該部分稍微比較復(fù)雜,且需要對查找樹 Trie 有一定了解。

路由的匹配

講完路由的構(gòu)建,我們來看看 Gin 是如何實(shí)現(xiàn)路由匹配的,看一下下面的這段代碼:

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
}

從源代碼中可以發(fā)現(xiàn),Gin 內(nèi)部實(shí)際上調(diào)用了 Go 自帶函數(shù)庫 net/http 庫中的 http.ListenAndServe(addr, handler) 函數(shù),并且該函數(shù)的 handlerHandler 接口類型,其源代碼如下:

type Handler interface {
    ServeHTTP(ResponseWriter, *Request)
}

由此,我們可以知道,在 Gin 的 Engine 結(jié)構(gòu)中,實(shí)現(xiàn)了該接口,所以,我們只需把關(guān)注點(diǎn)放到 Gin 實(shí)現(xiàn) Handler 接口的 ServeHTTP(ResponseWriter, *Request) 函數(shù)中即可,下面我們來看一下 Gin 對該接口的實(shí)現(xiàn)源代碼:

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)
}

首先是從引擎的對象池中獲取一個(gè) Gin 的上下文對象,并對其屬性進(jìn)行重置操作,至于 Gin 上下文的內(nèi)容這里不做展開討論,在本系列的后續(xù)文章中,會(huì)與 Go 自帶函數(shù)庫中的 context 庫結(jié)合討論。

然后以該 Context 對象作為參數(shù)調(diào)用 engine.handleHTTPRequest(c) 函數(shù)對請求進(jìn)行處理,最后再將該 Context 重新放入該 Gin 引擎的對象池中。下面來看一下該函數(shù)的源代碼,在本系列的第一篇文章 Gin 源碼學(xué)習(xí)(一)丨請求中 URL 的參數(shù)是如何解析的? 中有對其稍微介紹過,所以我們這里同樣,只針對路由匹配的內(nèi)容來對其進(jìn)行講解:

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++ {
        // 遍歷方法樹切片, 獲取與請求方法相同的方法樹的根節(jié)點(diǎn)
        if t[i].method != httpMethod {
            continue
        }
        root := t[i].root
        // Find route in tree
        // 根據(jù)請求 URI 從該方法樹中進(jìn)行路由匹配并獲取請求參數(shù)
        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 不為 "/"
        // 則判斷是否開啟重定向配置, 若開啟, 則進(jìn)行重定向操作
        if httpMethod != "CONNECT" && rPath != "/" {
            if value.tsr && engine.RedirectTrailingSlash {
                redirectTrailingSlash(c)
                return
            }
            if engine.RedirectFixedPath && redirectFixedPath(c, root, engine.RedirectFixedPath) {
                return
            }
        }
        break
    }

    // 如果開啟 HandleMethodNotAllowed, 則在其他請求類型的方法樹中進(jìn)行匹配
    if engine.HandleMethodNotAllowed {
        for _, tree := range engine.trees {
            if tree.method == httpMethod {
                continue
            }
            // 如果在其他請求類型的方法樹中能夠匹配到該請求 URI, 并且處理函數(shù)切片不為空, 則返回 405 錯(cuò)誤
            if value := tree.root.getValue(rPath, nil, unescape); value.handlers != nil {
                c.handlers = engine.allNoMethod
                serveError(c, http.StatusMethodNotAllowed, default405Body)
                return
            }
        }
    }
    // 返回 404 錯(cuò)誤
    c.handlers = engine.allNoRoute
    serveError(c, http.StatusNotFound, default404Body)
}

從上面源代碼中,我們可以發(fā)現(xiàn),路由的匹配操作,是在 root.getValue(rPath, po, unescape) 函數(shù)中進(jìn)行的,下面我們來看一下該函數(shù)的源代碼并結(jié)合具體實(shí)例來對其進(jìn)行分析,該函數(shù)同樣在本系列的第一篇文章中出現(xiàn)過,此處僅對路由匹配的內(nèi)容進(jìn)行講解:

func (n *node) getValue(path string, po Params, unescape bool) (value nodeValue) {
    value.params = po
walk: // Outer loop for walking the tree
    // 使用 for 循環(huán)進(jìn)行節(jié)點(diǎn)訪問匹配操作
    for {
        // 判斷當(dāng)前請求的 path 長度是否比當(dāng)前節(jié)點(diǎn)的 n.path 長
        // 如果是, 則使用當(dāng)前節(jié)點(diǎn)的 n.path 與 path 進(jìn)行匹配
        if len(path) > len(n.path) {
            // 判斷當(dāng)前路由節(jié)點(diǎn)的 path 與請求的 path 前綴是否完全一致
            if path[:len(n.path)] == n.path {
                // 對請求的 path 進(jìn)行重新截取, 去除與當(dāng)前節(jié)點(diǎn)完全匹配的前綴部分
                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
                // 如果當(dāng)前節(jié)點(diǎn)不為通配符節(jié)點(diǎn)
                if !n.wildChild {
                    // 獲取請求 path 的第一個(gè)字符
                    c := path[0]
                    // 遍歷當(dāng)前路由節(jié)點(diǎn)的 indices, 判斷是否存在與請求 path 匹配的索引
                    for i := 0; i < len(n.indices); i++ {
                        if c == n.indices[i] {
                            // 如果存在, 將當(dāng)前路由節(jié)點(diǎn)修改為該子節(jié)點(diǎn)
                            n = n.children[i]
                            // 跳轉(zhuǎn)至 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
                // 當(dāng)前節(jié)點(diǎn)為通配符節(jié)點(diǎn)
                // 表示其僅有一個(gè)子節(jié)點(diǎn), 且節(jié)點(diǎn)類型為 param 或者 catchAll
                n = n.children[0]
                switch n.nType {
                case param: // 如果當(dāng)前路由節(jié)點(diǎn)類型為 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!
                    // 如果用于匹配參數(shù)的 end 下標(biāo)小于當(dāng)前請求 path 的長度
                    if end < len(path) {
                        // 如果當(dāng)前路由節(jié)點(diǎn)存在孩子節(jié)點(diǎn)
                        if len(n.children) > 0 {
                            // 對當(dāng)前請求 path 進(jìn)行重新截取
                            path = path[end:]
                            // 獲取當(dāng)前路由節(jié)點(diǎn)的孩子節(jié)點(diǎn)
                            n = n.children[0]
                            // 跳轉(zhuǎn)至 walk, 開始下一輪匹配
                            continue walk
                        }

                        // ... but we can't
                        value.tsr = len(path) == end+1
                        return
                    }

                    // 如果當(dāng)前的 handlers 不為空, 則返回
                    if value.handlers = n.handlers; value.handlers != nil {
                        value.fullPath = n.fullPath
                        return
                    }
                    // 如果當(dāng)前路由節(jié)點(diǎn)有一個(gè)子節(jié)點(diǎn)
                    if len(n.children) == 1 {
                        // No handle found. Check if a handle for this path + a
                        // trailing slash exists for TSR recommendation
                        // 沒有找到處理該請求 path 的處理函數(shù)
                        // 如果當(dāng)前路由節(jié)點(diǎn)的子節(jié)點(diǎn)的 path 為 "/" 且存在處理函數(shù)
                        // 則設(shè)置 value.tsr 為true
                        n = n.children[0]
                        value.tsr = n.path == "/" && n.handlers != nil
                    }

                    return

                case catchAll:  // 如果當(dāng)前路由節(jié)點(diǎn)的類型為 catchAll
                    // 直接將當(dāng)前的請求 path 存儲(chǔ)至 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 {  // 如果當(dāng)前請求的 path 與當(dāng)前節(jié)點(diǎn)的 path 相同
            // We should have reached the node containing the handle.
            // Check if this node has a handle registered.
            // 由于路由已匹配完成, 因此只需檢查當(dāng)前已創(chuàng)建的路由節(jié)點(diǎn)中是否存在處理函數(shù)
            // 如果存在處理函數(shù), 則直接返回
            if value.handlers = n.handlers; value.handlers != nil {
                value.fullPath = n.fullPath
                return
            }

            // 如果當(dāng)前匹配的路由節(jié)點(diǎn)中不存在處理函數(shù)
            // 且當(dāng)前請求的 path 為 "/", 并且當(dāng)前節(jié)點(diǎn)的子節(jié)點(diǎn)為 param 節(jié)點(diǎn)或 catchAll 節(jié)點(diǎn), 且當(dāng)前節(jié)點(diǎn)不為 root 節(jié)點(diǎn)
            // 則設(shè)置 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
            // 沒有找到匹配路由的處理函數(shù)
            // 檢查該路由節(jié)點(diǎn)是否存在 path 僅為 "/" 且處理函數(shù)不為空的子節(jié)點(diǎn), 或者節(jié)點(diǎn)類型為 catchAll 且處理函數(shù)不為空的子節(jié)點(diǎn), 若存在, 則設(shè)置 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
        // 當(dāng)前請求的 path 的長度比當(dāng)前路由節(jié)點(diǎn)的 path 的長度短
        // 嘗試在請求的 path 尾部添加 "/", 如果添加后的請求 path 與當(dāng)前路由節(jié)點(diǎn)的 path 相同, 且當(dāng)前路由節(jié)點(diǎn)存在處理函數(shù), 則設(shè)置 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
    }
}

例如,一個(gè) URI 為 /user/1/send 的 GET 請求的匹配過程,如下圖所示:

2.jpg

小結(jié)

這篇文章講解了 Gin 路由的結(jié)構(gòu)、構(gòu)建以及匹配過程,Gin 內(nèi)部使用查找樹 Trie 來存儲(chǔ)路由節(jié)點(diǎn)。

第一部分講解了 Gin 的路由結(jié)構(gòu),其中包括 Gin 引擎中使用到的屬性結(jié)構(gòu)以及 Gin 的方法樹,節(jié)點(diǎn)結(jié)構(gòu)等。

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

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

本系列的下一篇文章將對 Gin 的工作機(jī)制進(jìn)行講解,至此,Gin 源碼學(xué)習(xí)的第三篇也就到此結(jié)束了,感謝大家對本文的閱讀~~

歡迎掃描以下二維碼關(guān)注筆者的個(gè)人微信訂閱號(hào),準(zhǔn)時(shí)獲取文章更新通知:

qrcode.jpg
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,505評(píng)論 6 533
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,556評(píng)論 3 418
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,463評(píng)論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,009評(píng)論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,778評(píng)論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,218評(píng)論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,281評(píng)論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,436評(píng)論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,969評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,795評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,993評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,537評(píng)論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,229評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,659評(píng)論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,917評(píng)論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,687評(píng)論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,990評(píng)論 2 374

推薦閱讀更多精彩內(nèi)容