gin是什麼呢?
我們在github
上看看官方簡介
Gin is a web framework written in Go (Golang). It features a martini-like API with performance that is up to 40 times faster thanks to httprouter. If you need performance and good productivity, you will love Gin.
Gin 是用 Go 開發的一個微框架,Web框架,類似 Martinier 的 API,介面簡潔,效能極高,也因為 httprouter的效能提高了 40 倍。
如果你需要良好的表現和工作效率,你會喜歡Gin
。
gin有啥特性呢?
tag | 說明 |
---|---|
異常處理 | 服務始終可用,不會當機 。Gin 可以捕獲 panic,並恢復。而且有極為便利的機制處理HTTP請求過程中發生的錯誤。 |
路由分組 | 可以將需要授權和不需要授權的API分組,不同版本的API分組。 而且分組可巢狀,且效能不受影響。 例如v1/xxx/xxx v2/xxx/xxx |
渲染內建 | 原生支援JSON,XML和HTML 的渲染。 |
JSON | Gin 可以解析並驗證請求的JSON。這個特性對 Restful API 的開發尤其有用。 |
中介軟體 | HTTP 請求,可先經過一系列中介軟體處理就向日志Logger,Authorization等。 中介軟體機制也極大地提高了框架的可擴充套件性。 |
gin大致都包含了哪些知識點?
gin的實戰演練我們之前也有分享過,我們再來回顧一下,gin大致都包含了哪些知識點
:路由
和*路由
- query查詢引數
- 接收陣列和 Map
- Form 表單
- 單檔案和多檔案上傳
- 分組路由,以及路由巢狀
- 路由中介軟體
- 各種資料格式的支援,json、struct、xml、yaml、protobuf
- HTML模板渲染
- url重定向
- 非同步協程等等
要是朋友們對gin還有點興趣的話,可以點進來看看,這裡有具體的知識點對應的案例gin實戰演練
路由是什麼?
我們再來了解一下路由是什麼
路由器是一種連線多個網路或網段的網路裝置,它能將不同網路或網段之間的資料資訊進行“翻譯”,以使它們能夠相互“讀”懂對方的資料,從而構成一個更大的網路。
路由器有兩大典型功能
- 即資料通道功能
包括轉發決定、背板轉發以及輸出鏈路排程等,一般由特定的硬體來完成
- 控制功能
一般用軟體來實現,包括與相鄰路由器之間的資訊交換、系統配置、系統管理等
gin裡面的路由
路由是web框架的核心功能。
寫過路由的朋友最開始是不是這樣看待路由的:
- 根據路由裡的
/
把路由切分成多個字串陣列 - 然後按照相同的前子陣列把路由構造成樹的結構
當需要定址的時候,先把請求的 url
按照 /
切分,然後遍歷樹進行定址,這樣子有點像是深度優先演算法
的遞迴遍歷,從根節點開始,不停的向根的地方進行延伸,知道不能再深入為止,算是得到了一條路徑
舉個例子
定義了兩個路由 /v1/hi
,/v1/hello
那麼這就會構造出擁有三個節點的路由樹,根節點是 v1
,兩個子節點分別是 hi
hello
。
上述是一種實現路由樹的方式,這種是比較直觀,容易理解的。對 url 進行切分、比較,可是時間複雜度是 O(2n)
,那麼我們有沒有更好的辦法優化時間複雜度呢?大名鼎鼎的GIn框架有辦法,往後看
演算法是什麼?
再來提一提演算法是啥。
演算法是解決某個問題的計算方法、步驟,不僅僅是有了計算機才有演算法這個名詞/概念的,
例如我們小學學習的九九乘法表
中學學習的各種解決問題的計算方法,例如物理公式等等
現在各種吃播大秀廚藝,做法的流程和方法也是演算法的一種
- 面臨的問題是bug , 解決的方法不盡相同,步驟也大相徑庭
- 面臨豬蹄,烹飪方法各有特色
- 面臨我們生活中的難題,也許每個人都會碰到同樣的問題,可是每個人解決問題的方式方法差異也非常大,有的人處理事情非常漂亮,有的人拖拖拉拉,總留尾巴
大學裡面學過演算法這本書,演算法是計算機的靈魂,面臨問題,好的演算法能夠輕易應對且健壯性好
面臨人生難題,好的解決方式,也同樣能夠讓我們走的更遠,更確切有點來說,應該是好的思維模型。
演算法有如下五大特徵
每個事物都會有自己的特點,否則如何才能讓人記憶深刻呢
- 有限性 , 演算法得有明確限步之後會結束
- 確切性,每一個步驟都是明確的,涉及的引數也是確切的
- 輸入,演算法有零個或者多個輸入
- 輸出,演算法有零個或者多個輸出
- 可行性,演算法的每一個步驟都是可以分解出來執行的,且都可以在有限時間內完成
gin的路由演算法
那我們開始進入進入正題,gin的路由演算法,千呼萬喚始出來
gin的是路由演算法類似於一棵字首樹
只需遍歷一遍字串即可,時間複雜度為O(n)
。比上面提到的方式,在時間複雜度上來說真是大大滴優化呀
不過,僅僅是對於一次 http 請求來說,是看不出啥效果的
誒,敲黑板了,什麼叫做字首樹呢?
Trie樹,又叫字典樹、字首樹(Prefix Tree),是一種多叉樹結構
畫個圖,大概就能明白字首樹是個啥玩意了
這棵樹還和二叉樹不太一樣,它的鍵不是直接儲存在節點中,而是由節點在樹中的位置決定
一個節點的所有子孫都有相同的字首,也就是這個節點對應的字串,而根節點對應空字串。
例如上圖,我們一個一個的來定址一下,會有這樣的字串
- MAC
- TAG
- TAB
- HEX
字首樹有如下幾個特點:
- 字首樹除根節點不包含字元,其他節點都包含字元
- 每個節點的子節點包含的字串不相同
- 從根節點到某一個節點,路徑上經過的字元連線起來,為該節點對應的字串
- 每個節點的子節點通常有一個標誌位,用來標識單詞的結束
有沒有覺得這個和路由的樹一毛一樣?
gin的路由樹演算法類似於一棵字首樹. 不過並不是只有一顆樹, 而是每種方法(POST, GET ,PATCH…)都有自己的一顆樹
例如,路由的地址是
- /hi
- /hello
- /:name/:id
那麼gin對應的樹會是這個樣子的
GO中 路由對應的節點資料結構是這個樣子的
type node struct {
path string
indices string
children []*node
handlers HandlersChain
priority uint32
nType nodeType
maxParams uint8
wildChild bool
}
具體新增路由的方法,實現方法是這樣的
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)
engine.trees = append(engine.trees, methodTree{method: method, root: root})
}
root.addRoute(path, handlers)
}
仔細看,gin的實現不像一個真正的樹
因為他的children []*node所有的孩子都會放在這個陣列裡面,具體實現是,他會利用indices, priority變相的去實現一棵樹
我們來看看不同註冊路由的方式有啥不同?每一種註冊方式,最終都會反應到gin的路由樹上面
普通註冊路由
普通註冊路由的方式是 router.xxx
,可以是如下方式
- GET
- POST
- PATCH
- PUT
- …
router.POST("/hi", func(context *gin.Context) {
context.String(http.StatusOK, "hi xiaomotong")
})
也可以以組Group
的方式註冊,以分組的方式註冊路由,便於版本的維護
v1 := router.Group("v1")
{
v1.POST("hello", func(context *gin.Context) {
context.String(http.StatusOK, "v1 hello world")
})
}
在呼叫POST, GET, PATCH
等路由HTTP相關函式時, 會呼叫handle
函式
func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes {
absolutePath := group.calculateAbsolutePath(relativePath) // calculateAbsolutePath
handlers = group.combineHandlers(handlers) // combineHandlers
group.engine.addRoute(httpMethod, absolutePath, handlers)
return group.returnObj()
}
calculateAbsolutePath
和 combineHandlers
還會再次出現
呼叫組的話,看看是咋實現的
func (group *RouterGroup) Group(relativePath string, handlers ...HandlerFunc) *RouterGroup {
return &RouterGroup{
Handlers: group.combineHandlers(handlers),
basePath: group.calculateAbsolutePath(relativePath),
engine: group.engine,
}
}
同樣也會呼叫 calculateAbsolutePath
和 combineHandlers
這倆函式,我們來看看 這倆函式是幹啥的,看到函式名字,也許大概也能猜出個所以然了吧,來看看原始碼
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
}
func (group *RouterGroup) calculateAbsolutePath(relativePath string) string {
return joinPaths(group.basePath, relativePath)
}
func joinPaths(absolutePath, relativePath string) string {
if relativePath == "" {
return absolutePath
}
finalPath := path.Join(absolutePath, relativePath)
appendSlash := lastChar(relativePath) == '/' && lastChar(finalPath) != '/'
if appendSlash {
return finalPath + "/"
}
return finalPath
}
joinPaths
函式在這裡相當重要,主要是做拼接的作用
從上面來看,可以看出如下2點:
- 呼叫中介軟體, 是將某個路由的handler處理函式和中介軟體的處理函式都放在了Handlers的陣列中
- 呼叫Group, 是將路由的path上面拼上Group的值. 也就是/hi/:id, 會變成v1/hi/:id
使用中介軟體的方式註冊路由
我們也可以使用中介軟體的方式來註冊路由,例如在訪問我們的路由之前,我們需要加一個認證的中介軟體放在這裡,必須要認證通過了之後,才可以訪問路由
router.Use(Login())
func (group *RouterGroup) Use(middleware ...HandlerFunc) IRoutes {
group.Handlers = append(group.Handlers, middleware...)
return group.returnObj()
}
不管是普通的註冊,還是通過中介軟體的方式註冊,裡面都有一個關鍵的handler
handler
方法 呼叫 calculateAbsolutePath
和 combineHandlers
將路由拼接好之後,呼叫addRoute
方法,將路由預處理的結果註冊到gin Engine的trees上,來在看讀讀handler的實現
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()
}
那麼,服務端寫好路由之後,我們通過具體的路由去做http請求的時候,服務端是如何通過路由找到具體的處理函式的呢?
我們仔細追蹤原始碼, 我們可以看到如下的實現
...
// 一棵字首樹
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
// 這裡通過 path 來找到相應的 handlers 處理函式
handlers, params, tsr := root.getValue(path, c.Params, unescape)
if handlers != nil {
c.handlers = handlers
c.Params = params
// 在此處呼叫具體的 處理函式
c.Next()
c.writermem.WriteHeaderNow()
return
}
if httpMethod != "CONNECT" && path != "/" {
if tsr && engine.RedirectTrailingSlash {
redirectTrailingSlash(c)
return
}
if engine.RedirectFixedPath && redirectFixedPath(c, root, engine.RedirectFixedPath) {
return
}
}
break
}
...
func (c *Context) Next() {
c.index++
for c.index < int8(len(c.handlers)) {
c.handlers[c.index](c)
c.index++
}
}
當客戶端請求服務端的介面時, 服務端此處 handlers, params, tsr := root.getValue(path, c.Params, unescape)
, 通過 path 來找到相應的 handlers 處理函式,
將handlers
, params
複製給到服務中,通過 c.Next()
來執行具體的處理函式,此時就可以達到,客戶端請求響應的路由地址,服務端能過對響應路由做出對應的處理操作了
總結
- 簡單回顧了一下gin的特性
- 介紹了gin裡面的路由
- 分享了gin的路由演算法,以及具體的原始碼實現流程
好了,本次就到這裡,下一次 分享最常用的限流演算法以及如何在http中介軟體中加入流控,
技術是開放的,我們的心態,更應是開放的。擁抱變化,向陽而生,努力向前行。
我是小魔童哪吒,歡迎點贊關注收藏,下次見~
本作品採用《CC 協議》,轉載必須註明作者和本文連結