在前兩篇文章 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)的屬性,如:重定向配置 RedirectTrailingSlash
和 RedirectFixedPath
,無路由處理函數(shù)切片 noRoute
和 allNoRoute
,HTML templates 相關(guān)渲染配置 delims
和 HTMLRender
等。
從上面的 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)體中所引用到的 RouterGroup
和 methodTree
的結(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è)屬性 method
和 root
,這是位于 Gin 路由結(jié)構(gòu)頂端的方法樹,其 method
屬性表示請求的方法類型,如:GET
,POST
,PUT
等,而 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)的前綴路徑,此處為 /us
;indices
表示當(dāng)前節(jié)點(diǎn)的孩子節(jié)點(diǎn)索引,此處為 eo
;children
則用于保存當(dāng)前節(jié)點(diǎn)的孩子節(jié)點(diǎn)切片,此處存儲(chǔ)了 path
為 e
和 o
的兩個(gè)節(jié)點(diǎn);handlers
保存當(dāng)前 path
的處理函數(shù)切片,此處由于沒有創(chuàng)建針對 /us
的處理函數(shù),因此為 nil
;priority
表示當(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 定義了四種類型,static
,root
,param
和 catchAll
,static
表示普通節(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")
}
比較有疑惑的地方,可能是 GET 方法的路由樹的第4~6層,為什么會(huì)有兩個(gè) path
為 ""
的節(jié)點(diǎn)以及兩個(gè) nType
為 catchAll
的節(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ù)中使用該 RouterGroup
的 basePath
結(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ù) method
,path
和 handlers
進(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)來的 path
和 handlers
作為參數(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ù)的 handler
為 Handler
接口類型,其源代碼如下:
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 請求的匹配過程,如下圖所示:
小結(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í)獲取文章更新通知: