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

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

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

gin.Default() 函數內部調用了 gin.New() 函數來建立 Gin 的路由引擎,而後爲該引擎添加了 Logger()Recovery() 兩個中間件。this

gin.New() 函數用於建立 Gin 路由引擎,其主要用於爲該即將被建立的引擎作一些初始化配置。url

接下來咱們來看一下 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")
}
複製代碼

比較有疑惑的地方,多是 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 請求的匹配過程,以下圖所示:

小結

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

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

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

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

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

相關文章
相關標籤/搜索