詳解go-zero是如何做路由管理的
go-zero 是一個微服務(wù)框架,包含了 web 和 rpc 兩大部分。
而對于 web 框架來說,路由管理是必不可少的一部分,那么本文就來探討一下 go-zero 的路由管理是怎么做的,具體采用了哪種技術(shù)方案。
路由管理方案
路由管理方案有很多種,具體應(yīng)該如何選擇,應(yīng)該根據(jù)使用場景,以及實現(xiàn)的難易程度做綜合分析,下面介紹常見的三種方案。
注意這里只是做一個簡單的概括性對比,更加詳細的內(nèi)容可以看這篇文章:HTTP Router 算法演進。
標(biāo)準(zhǔn)庫方案
最簡單的方案就是直接使用 map[string]func()
作為路由的數(shù)據(jù)結(jié)構(gòu),鍵為具體的路由,值為具體的處理方法。
//?路由管理數(shù)據(jù)結(jié)構(gòu) type?ServeMux?struct?{ ????mu????sync.RWMutex??????????//?對象操作讀寫鎖 ????m?????map[string]muxEntry???//?存儲路由映射關(guān)系 }
這種方案優(yōu)點就是實現(xiàn)簡單,性能較高;缺點也很明顯,占用內(nèi)存更高,更重要的是不夠靈活。
Trie Tree
Trie Tree 也稱為字典樹或前綴樹,是一種用于高效存儲和檢索、用于從某個集合中查到某個特定 key 的數(shù)據(jù)結(jié)構(gòu)。
Trie Tree 時間復(fù)雜度低,和一般的樹形數(shù)據(jù)結(jié)構(gòu)相比,Trie Tree 擁有更快的前綴搜索和查詢性能。
和查詢時間復(fù)雜度為 O(1)
常數(shù)的哈希算法相比,Trie Tree 支持前綴搜索,并且可以節(jié)省哈希函數(shù)的計算開銷和避免哈希值碰撞的情況。
最后,Trie Tree 還支持對關(guān)鍵字進行字典排序。
Radix Tree
Radix Tree(基數(shù)樹)是一種特殊的數(shù)據(jù)結(jié)構(gòu),用于高效地存儲和搜索字符串鍵值對,它是一種基于前綴的樹狀結(jié)構(gòu),通過將相同前綴的鍵值對合并在一起來減少存儲空間的使用。
Radix Tree 通過合并公共前綴來降低存儲空間的開銷,避免了 Trie Tree 字符串過長和字符集過大時導(dǎo)致的存儲空間過多問題,同時公共前綴優(yōu)化了路徑層數(shù),提升了插入、查詢、刪除等操作效率。
比如 Gin 框架使用的開源組件 HttpRouter 就是采用這個方案。
go-zero 路由規(guī)則
在使用 go-zero 開發(fā)項目時,定義路由需要遵守如下規(guī)則:
- 路由必須以
/
開頭 - 路由節(jié)點必須以
/
分隔 - 路由節(jié)點中可以包含
:
,但是:
必須是路由節(jié)點的第一個字符,:
后面的節(jié)點值必須要在結(jié)請求體中有path tag
聲明,用于接收路由參數(shù) - 路由節(jié)點可以包含字母、數(shù)字、下劃線、中劃線
接下來就讓我們深入到源碼層面,相信看過源碼之后,你就會更懂這些規(guī)則的意義了。
go-zero 源碼實現(xiàn)
首先需要說明的是,底層數(shù)據(jù)結(jié)構(gòu)使用的是二叉搜索樹,還不是很了解的同學(xué)可以看這篇文章:使用 Go 語言實現(xiàn)二叉搜索樹
節(jié)點定義
先看一下節(jié)點定義:
//?core/search/tree.go const?( ????colon?=?':' ????slash?=?'/' ) type?( ????//?節(jié)點 ????node?struct?{ ????????item?????interface{} ????????children?[2]map[string]*node ????} ????//?A?Tree?is?a?search?tree. ????Tree?struct?{ ????????root?*node ????} )
重點說一下 children
,它是一個包含兩個元素的數(shù)組,元素 0
存正常路由鍵,元素 1
存以 :
開頭的路由鍵,這些是 url 中的變量,到時候需要替換成實際值。
舉一個例子,有這樣一個路由 /api/:user
,那么 api
會存在 children[0]
,user
會存在 children[1]
。
具體可以看看這段代碼:
func?(nd?*node)?getChildren(route?string)?map[string]*node?{ ????//?判斷路由是不是以?:?開頭 ????if?len(route)?>?0?&&?route[0]?==?colon?{ ????????return?nd.children[1] ????} ????return?nd.children[0] }
路由添加
//?Add?adds?item?to?associate?with?route. func?(t?*Tree)?Add(route?string,?item?interface{})?error?{ ????//?需要路由以?/?開頭 ????if?len(route)?==?0?||?route[0]?!=?slash?{ ????????return?errNotFromRoot ????} ????if?item?==?nil?{ ????????return?errEmptyItem ????} ????//?把去掉?/?的路由作為參數(shù)傳入 ????err?:=?add(t.root,?route[1:],?item) ????switch?err?{ ????case?errDupItem: ????????return?duplicatedItem(route) ????case?errDupSlash: ????????return?duplicatedSlash(route) ????default: ????????return?err ????} } func?add(nd?*node,?route?string,?item?interface{})?error?{ ????if?len(route)?==?0?{ ????????if?nd.item?!=?nil?{ ????????????return?errDupItem ????????} ????????nd.item?=?item ????????return?nil ????} ????//?繼續(xù)判斷,看看是不是有多個?/ ????if?route[0]?==?slash?{ ????????return?errDupSlash ????} ????for?i?:=?range?route?{ ????????//?判斷是不是?/,目的就是去處兩個?/?之間的內(nèi)容 ????????if?route[i]?!=?slash?{ ????????????continue ????????} ????????token?:=?route[:i] ????????//?看看有沒有子節(jié)點,如果有子節(jié)點,就在子節(jié)點下面繼續(xù)添加 ????????children?:=?nd.getChildren(token) ????????if?child,?ok?:=?children[token];?ok?{ ????????????if?child?!=?nil?{ ????????????????return?add(child,?route[i+1:],?item) ????????????} ????????????return?errInvalidState ????????} ????????//?沒有子節(jié)點,那么新建一個 ????????child?:=?newNode(nil) ????????children[token]?=?child ????????return?add(child,?route[i+1:],?item) ????} ????children?:=?nd.getChildren(route) ????if?child,?ok?:=?children[route];?ok?{ ????????if?child.item?!=?nil?{ ????????????return?errDupItem ????????} ????????child.item?=?item ????}?else?{ ????????children[route]?=?newNode(item) ????} ????return?nil }
主要部分代碼都已經(jīng)加了注釋,其實這個過程就是樹的構(gòu)建,如果讀過之前那篇文章,那這里還是比較好理解的。
路由查找
先來看一段 match
代碼:
func?match(pat,?token?string)?innerResult?{ ????if?pat[0]?==?colon?{ ????????return?innerResult{ ????????????key:???pat[1:], ????????????value:?token, ????????????named:?true, ????????????found:?true, ????????} ????} ????return?innerResult{ ????????found:?pat?==?token, ????} }
這里有兩個參數(shù):
pat
:路由樹中存儲的路由token
:實際請求的路由,可能包含參數(shù)值
還是剛才的例子 /api/:user
,如果是 api
,沒有以 :
開頭,那就不會走 if
邏輯。
接下來匹配 :user
部分,如果實際請求的 url 是 /api/zhangsan
,那么會將 user
作為 key
,zhangsan
作為 value
保存到結(jié)果中。
下面是搜索查找代碼:
//?Search?searches?item?that?associates?with?given?route. func?(t?*Tree)?Search(route?string)?(Result,?bool)?{ ????//?第一步先判斷是不是?/?開頭 ????if?len(route)?==?0?||?route[0]?!=?slash?{ ????????return?NotFound,?false ????} ????var?result?Result ????ok?:=?t.next(t.root,?route[1:],?&result) ????return?result,?ok } func?(t?*Tree)?next(n?*node,?route?string,?result?*Result)?bool?{ ????if?len(route)?==?0?&&?n.item?!=?nil?{ ????????result.Item?=?n.item ????????return?true ????} ????for?i?:=?range?route?{ ????????//?和?add?里同樣的提取邏輯 ????????if?route[i]?!=?slash?{ ????????????continue ????????} ????????token?:=?route[:i] ????????return?n.forEach(func(k?string,?v?*node)?bool?{ ????????????r?:=?match(k,?token) ????????????if?!r.found?||?!t.next(v,?route[i+1:],?result)?{ ????????????????return?false ????????????} ????????????//?如果?url?中有參數(shù),會把鍵值對保存到結(jié)果中 ????????????if?r.named?{ ????????????????addParam(result,?r.key,?r.value) ????????????} ????????????return?true ????????}) ????} ????return?n.forEach(func(k?string,?v?*node)?bool?{ ????????if?r?:=?match(k,?route);?r.found?&&?v.item?!=?nil?{ ????????????result.Item?=?v.item ????????????if?r.named?{ ????????????????addParam(result,?r.key,?r.value) ????????????} ????????????return?true ????????} ????????return?false ????}) }
以上就是路由管理的大部分代碼,整個文件也就 200 多行,邏輯也并不復(fù)雜,通讀之后還是很有收獲的。
到此這篇關(guān)于詳解go-zero是如何做路由管理的的文章就介紹到這了,更多相關(guān)go-zero路由管理內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Go語言fsnotify接口實現(xiàn)監(jiān)測文件修改
這篇文章主要為大家介紹了Go語言fsnotify接口實現(xiàn)監(jiān)測文件修改的示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-06-06

Go語言學(xué)習(xí)之golang-jwt/jwt的教程分享

golang中for循環(huán)遍歷channel時需要注意的問題詳解