Go Web 框架 Gin 路由的學(xué)習(xí)
Gin 的路由功能是基于 https://github.com/julienschmidt/httprouter 這個(gè)項(xiàng)目實(shí)現(xiàn)的。目前也有很多其他Web框架也基于該路由框架做了二次開發(fā)。
http 路由的接口
在 Gin 中,為了兼容不同路由的引擎,定義了 IRoutes 和 IRouter 接口,便于替換其他的路由實(shí)現(xiàn)。(目前默認(rèn)是httprouter)
下面是一個(gè)路由的接口定義:
type IRoutes interface {
Use(...HandlerFunc) IRoutes
Handle(string, string, ...HandlerFunc) IRoutes
Any(string, ...HandlerFunc) IRoutes
GET(string, ...HandlerFunc) IRoutes
POST(string, ...HandlerFunc) IRoutes
DELETE(string, ...HandlerFunc) IRoutes
PATCH(string, ...HandlerFunc) IRoutes
PUT(string, ...HandlerFunc) IRoutes
OPTIONS(string, ...HandlerFunc) IRoutes
HEAD(string, ...HandlerFunc) IRoutes
StaticFile(string, string) IRoutes
Static(string, string) IRoutes
StaticFS(string, http.FileSystem) IRoutes
}
type HandlerFunc func(*Context)
HandlerFunc 是一個(gè)方法類型的定義,我們定義的路由其實(shí)就是一個(gè)路徑與HandlerFunc 的映射關(guān)系。從上面的定義可以看出,IRoutes 主要定義了一些基于http方法、靜態(tài)方法的路徑和一組方法的映射。Use 方法是針對(duì)此路由的所有路徑映射一組方法,在使用上是為了給這些路由添加中間件。
除了上面的定義外,Gin 還有路由組的抽象。
type IRouter interface {
IRoutes
Group(string, ...HandlerFunc) *RouterGroup
}
路由組是在IRoutes 的基礎(chǔ)上,有了組的概念,組下面還可以掛在不同的組。組的概念可以很好的管理一組路由,路由組可以自己定義一套Handler方法(即一組中間件)。
個(gè)人認(rèn)為IRouter的定義Group 應(yīng)該返回 IRouter,這樣可以把路由組更加抽象,也不會(huì)改變現(xiàn)有服務(wù)的使用。期待看下Gin源碼什么時(shí)候會(huì)按照這種定義方法修改過(guò)來(lái)。
在Gin框架中,路由由 RouterGroup 實(shí)現(xiàn)。我們從構(gòu)造和路由查找兩個(gè)方面分析路由的實(shí)現(xiàn)。
路由實(shí)現(xiàn)
路由的本質(zhì)就是在給定 路徑與Handler映射關(guān)系 的前提下,當(dāng)提供新的url時(shí),給出對(duì)應(yīng)func 的過(guò)程。其中可能需要從url中提取參數(shù),或者按照 * 匹配 url 的情況。
首先,我們看下Gin中路由結(jié)構(gòu)的定義。
// gin engine
type Engine struct {
RouterGroup
// ... 其他字段
trees methodTrees
}
// 每個(gè) http 方法定義一個(gè)森林
type methodTrees []methodTree
type methodTree struct {
method string
root *node
}
// 路由組的定義
type RouterGroup struct {
Handlers HandlersChain
basePath string
engine *Engine
root bool
}
從定義中可以看出,其實(shí)Gin 的 Engine 是復(fù)用了 RouterGroup。對(duì)于不同的 http method,都通過(guò)一個(gè)森林來(lái)存儲(chǔ)路由數(shù)據(jù)。下面是森林上每個(gè)節(jié)點(diǎn)的定義:
type node struct {
path string // 當(dāng)前路徑
indices string // 對(duì)應(yīng)children 的前綴
wildChild bool // 可能是帶參數(shù)的,或者是 * 的,所以是野節(jié)點(diǎn)
nType nodeType // 參數(shù)節(jié)點(diǎn),靜態(tài)節(jié)點(diǎn)
priority uint32 // 優(yōu)先級(jí) ,優(yōu)先級(jí)高的放在children 放在前面。
children []*node // 子節(jié)點(diǎn)
handlers HandlersChain // 調(diào)用鏈
fullPath string // 全路徑
}
從代碼實(shí)現(xiàn)上得知,這個(gè)森林其實(shí)是一個(gè)壓縮版本的Trie樹,每個(gè)節(jié)點(diǎn)會(huì)存儲(chǔ)前綴相同的路徑數(shù)據(jù)。下面,我們通過(guò)代碼來(lái)學(xué)習(xí)下路由的添加和刪除。
路由的添加
路由的添加,就是將path路徑添加到定義的Trie樹種,將handlers 添加到對(duì)應(yīng)的node 節(jié)點(diǎn)。
func (n *node) addRoute(path string, handlers HandlersChain) {
// 初始化和維護(hù)優(yōu)先級(jí)
for {
// 查找前綴
i := longestCommonPrefix(path, n.path)
// 原有路徑長(zhǎng)的情況下
// 節(jié)點(diǎn)n 的 path 變?yōu)榱斯睬熬Y
// 原有n 的path 路徑變?yōu)榱爽F(xiàn)有n 的子節(jié)點(diǎn)
// 當(dāng)添加的path長(zhǎng)的情況
// 需要分情況討論:
// 1. 如果是一個(gè)帶參數(shù)的路徑,校驗(yàn)是否后續(xù)路徑不同,如果不同則繼續(xù)掃描下一段路徑
// 2. 如果是帶 * 的路徑, 則直接報(bào)錯(cuò)
// 3. 如果已經(jīng)有對(duì)應(yīng)的首字母,修改當(dāng)前node節(jié)點(diǎn),并繼續(xù)掃描,并掃描下一段路徑
// 4. 如果非參數(shù)或者 * 匹配的方法,則插入一個(gè)子節(jié)點(diǎn)路徑,并完成掃描
// 最后注冊(cè)handlers,添加fullPath
n.handlers = handlers
n.fullPath = fullPath
return
}
}
從上面的代碼注釋可以看出,路由的添加,主要是通過(guò)不斷對(duì)比當(dāng)前節(jié)點(diǎn)的path和添加的path,做添加節(jié)點(diǎn)或者節(jié)點(diǎn)變更的操作,達(dá)到添加path的目的。
路徑查找
在服務(wù)請(qǐng)求時(shí),路由的責(zé)任就是給定一個(gè)url請(qǐng)求,拿到節(jié)點(diǎn)保存的handlers,以及url中包含的參數(shù)值。下面是對(duì)一個(gè)url 的解析實(shí)現(xiàn)。
type nodeValue struct {
handlers HandlersChain
params *Params
tsr bool
fullPath string
}
func (n *node) getValue(path string, params *Params, unescape bool) (value nodeValue) {
walk: // Outer loop for walking the tree
for {
prefix := n.path
// 如果比當(dāng)前節(jié)點(diǎn)路徑要長(zhǎng):
// - 非參數(shù)類型或模糊匹配的URL,如果和當(dāng)前節(jié)點(diǎn)前綴匹配,直接查看 node 的子節(jié)點(diǎn)
// - 參數(shù)化的node, 按照 / 分割提取參數(shù),如果未結(jié)束,則繼續(xù)匹配剩下的路徑,否則返回結(jié)果。
// - * 匹配的node,將剩余的路徑添加到 param 中直接返回。
// 如果和當(dāng)前節(jié)點(diǎn)相等,那就直接返回即可。
// 這里還做了非本方法的路徑匹配,用戶返回http 方法錯(cuò)誤的異常報(bào)告。
}
}
一個(gè)例子
下面通過(guò)一個(gè)例子,方便我們快速理解router的實(shí)現(xiàn)。
加入下面的一個(gè)路徑:
/search/
/support/
/blog/:post/
/about-us/team/
/contact/
在樹中,我們看到的樣子如下:
Path
\
├s
|├earch\
|└upport\
├blog\
| └:post
| └\
├about-us\
| └team\
└contact\
在做路由查找時(shí),通過(guò)路徑不斷匹配,找到對(duì)應(yīng)的子節(jié)點(diǎn)。拿到對(duì)應(yīng)子節(jié)點(diǎn)下的handler。完成路由的匹配。
總結(jié)
httprouter 沒(méi)有實(shí)現(xiàn)了routergroup功能,只是實(shí)現(xiàn)了router 的功能,在gin中做了實(shí)現(xiàn) 通過(guò)Trie樹實(shí)現(xiàn)路由是比較基礎(chǔ)的一種實(shí)現(xiàn)方法,除了這種方法外,還可以考慮通過(guò)正則的方式提取路由。 Gin http 服務(wù)是基于 Go 的 net/http庫(kù)的,net/http庫(kù)中handler 的實(shí)現(xiàn)是針對(duì)不同的 http method 的,所以需要在engine 中針對(duì)不同的method 提供不同的trie 樹。在添加路由時(shí),如果使用了 any 方法,則在每個(gè)http method 下都會(huì)添加一樣的路徑。 middleware 本質(zhì)上只是一個(gè) HandlerFunc.
推薦閱讀
