在前兩篇文章 Gin 源碼學習(一)丨請求中 URL 的參數是如何解析的? 和 Gin 源碼學習(二)丨請求體中的參數是如何解析的? 中,都是圍繞着對請求中所攜帶參數的解析來對 Gin 的源碼進行學習的。node
在這一篇文章中,將講解前兩篇文章中的實現前提,也是 Gin 的核心功能之一,路由。數組
那麼,帶着 "Gin 中路由是如何構建的" 和 "Gin 是如何進行路由匹配的" 這兩個問題,來開始 Gin 源碼學習的第三篇:路由是如何構建和匹配的?app
Go 版本:1.14函數
Gin 版本:v1.5.0oop
router := gin.Default()
router := gin.New()
複製代碼
在使用 Gin 的時候,咱們通常會使用以上兩種方式中的其中一種來建立 Gin 的引擎 gin.Engine
,那麼,這個引擎,究竟是個什麼東西呢?咱們一塊兒來看一下 gin.Engine
結構體的定義以及 gin.Default()
和 gin.New()
函數:post
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
。ui
在 gin.Default()
函數內部調用了 gin.New()
函數來建立 Gin 的路由引擎,而後爲該引擎添加了 Logger()
和 Recovery()
兩個中間件。this
gin.New()
函數用於建立 Gin 路由引擎,其主要用於爲該即將被建立的引擎作一些初始化配置。url
接下來咱們來看一下 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 源碼學習的第三篇也就到此結束了,感謝你們對本文的閱讀~~