gin源碼閱讀之四 -- gin的路由算法

gin的是路由算法其實(shí)就是一個(gè)Trie樹(也就是前綴樹). 有關(guān)數(shù)據(jù)結(jié)構(gòu)的可以自己去網(wǎng)上找相關(guān)資料查看.

注冊路由預(yù)處理

我們在使用gin時(shí)通過下面的代碼注冊路由

普通注冊

router.POST("/somePost", func(context *gin.Context) {
    context.String(http.StatusOK, "some post")
})

使用中間件

router.Use(Logger())

使用Group

v1 := router.Group("v1")
{
    v1.POST("login", func(context *gin.Context) {
        context.String(http.StatusOK, "v1 login")
    })
}

這些操作, 最終都會在反應(yīng)到gin的路由樹上

具體實(shí)現(xiàn)

// routergroup.go:L72-77
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()
}

在調(diào)用POST, GET, HEAD等路由HTTP相關(guān)函數(shù)時(shí), 會調(diào)用handle函數(shù)

如果調(diào)用了中間件的話, 會調(diào)用下面函數(shù)

func (group *RouterGroup) Use(middleware ...HandlerFunc) IRoutes {
    group.Handlers = append(group.Handlers, middleware...)
    return group.returnObj()
}

如果使用了Group的話, 會調(diào)用下面函數(shù)

func (group *RouterGroup) Group(relativePath string, handlers ...HandlerFunc) *RouterGroup {
    return &RouterGroup{
        Handlers: group.combineHandlers(handlers),
        basePath: group.calculateAbsolutePath(relativePath),
        engine:   group.engine,
    }
}

重點(diǎn)關(guān)注下面兩個(gè)函數(shù):

// routergroup.go:L208-217
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
}
func (group *RouterGroup) calculateAbsolutePath(relativePath string) string {
    return joinPaths(group.basePath, relativePath)
}

func joinPaths(absolutePath, relativePath string) string {
    if relativePath == "" {
        return absolutePath
    }

    finalPath := path.Join(absolutePath, relativePath)
    appendSlash := lastChar(relativePath) == '/' && lastChar(finalPath) != '/'
    if appendSlash {
        return finalPath + "/"
    }
    return finalPath
}

joinPaths函數(shù)里面有段代碼, 很有意思, 我還以為是寫錯(cuò)了. 主要是path.Join的用法.

finalPath := path.Join(absolutePath, relativePath)
appendSlash := lastChar(relativePath) == '/' && lastChar(finalPath) != '/'

在當(dāng)路由是/user/這種情況就滿足了lastChar(relativePath) == '/' && lastChar(finalPath) != '/'. 主要原因是path.Join(absolutePath, relativePath)之后, finalPath是user

綜合來看, 在預(yù)處理階段

  1. 在調(diào)用中間件的時(shí)候, 是將某個(gè)路由的handler處理函數(shù)和中間件的處理函數(shù)都放在了Handlers的數(shù)組中
  2. 在調(diào)用Group的時(shí)候, 是將路由的path上面拼上Group的值. 也就是/user/:name, 會變成v1/user:name

真正注冊

// routergroup.go:L72-77
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()
}

調(diào)用group.engine.addRoute(httpMethod, absolutePath, handlers)將預(yù)處理階段的結(jié)果注冊到gin Engine的trees上

gin路由樹簡單介紹

gin的路由樹算法是一棵前綴樹. 不過并不是只有一顆樹, 而是每種方法(POST, GET ...)都有自己的一顆樹

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)
        engine.trees = append(engine.trees, methodTree{method: method, root: root})
    }
    root.addRoute(path, handlers)
}

gin 路由最終的樣子大概是下面的樣子

image
type node struct {
    path      string
    indices   string
    children  []*node
    handlers  HandlersChain
    priority  uint32
    nType     nodeType
    maxParams uint8
    wildChild bool
}

其實(shí)gin的實(shí)現(xiàn)不像一個(gè)真正的樹, children []*node所有的孩子都放在這個(gè)數(shù)組里面, 利用indices, priority變相實(shí)現(xiàn)一棵樹

獲取路由handler

當(dāng)服務(wù)端收到客戶端的請求時(shí), 根據(jù)pathtrees匹配到相關(guān)的路由, 拿到相關(guān)的處理handlers

...
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
    handlers, params, tsr := root.getValue(path, c.Params, unescape) // 看這里
    if handlers != nil {
        c.handlers = handlers
        c.Params = params
        c.Next()
        c.writermem.WriteHeaderNow()
        return
    }
    if httpMethod != "CONNECT" && path != "/" {
        if tsr && engine.RedirectTrailingSlash {
            redirectTrailingSlash(c)
            return
        }
        if engine.RedirectFixedPath && redirectFixedPath(c, root, engine.RedirectFixedPath) {
            return
        }
    }
    break
}
...

主要在下面這個(gè)函數(shù)里面調(diào)用程序注冊的路由處理函數(shù)

func (c *Context) Next() {
    c.index++
    for c.index < int8(len(c.handlers)) {
        c.handlers[c.index](c)
        c.index++
    }
}

gin的路由采用的前綴樹, 由這里還想到有后綴樹, B樹, B-樹, B+樹, 平衡樹, 紅黑樹, 這些樹都是面試中經(jīng)常問到的. 后面開專門的文章介紹這些樹

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

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