Golang 框架 gin 運行源碼分析
【導讀】gin 框架是 go 語言 web 框架中非常流行的框架之一,本文詳細解讀了 gin 框架的優點與其實現。
解析 gin 框架中數據流轉原理
分析流程
我們已經瞭解了 http 包中 server 的啓動方法, 現在我們分析 gin 處理流程
gin 框架號稱 路由提速了 40 倍, 所以,到底他是哪裏快呢?
我們還是看下他啓動服務時做哪些事情
package main
import "github.com/gin-gonic/gin"
func main() {
#生成gin實例
r := gin.Default()
#註冊路由關係,定義具體的路由處理方法
r.GET("/ping", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "pong",
})
})
#啓動服務
r.Run() // listen and serve on 0.0.0.0:8080
}
和我們剛纔看到的http_server
非常像, 但是路由管理器這塊比較強大
較高級的語法糖, 可以直接指定 HTTP 方法名稱, 內部封裝 json 響應實現。
我們繼續從啓動方法r.Run()
入手, 看下他的內部實現邏輯
#gin.go
func (engine *Engine) Run(addr ...string) (err error) {
defer func() { debugPrintError(err) }()
#解析地址
address := resolveAddress(addr)
debugPrint("Listening and serving HTTP on %s\n", address)
#調用http包的ListenAndServe方法
err = http.ListenAndServe(address, engine)
return
}
我們可以發現, 這裏仍然使用的是 http 包的ListenAndServe
方法, 和我們上一章調用類似,主要區別是其第二個參數, 這裏是 gin 框架生成 gin 實例。
並且我們知道, 第二個參數的主要意義是, 關聯路由管器對象, 即 gin 框架承接了 url 和處理函數的路由管理。其他 http 請求這一套, 繼續複用原生包的 http 接口
我們截取下 http 包的server.go
的代碼
func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {
#獲取路由管理對象, 這裏即是我們調用`ListenAndServe`的第二個參數
handler := sh.srv.Handler
#如果沒有設置,則使用默認的defaultServerMux
if handler == nil {
handler = DefaultServeMux
}
if req.RequestURI == "*" && req.Method == "OPTIONS" {
handler = globalOptionsHandler{}
}
#進入路由管理對象的ServerHttp方法中,進行路由分發
handler.ServeHTTP(rw, req)
}
從handler.ServerHTTP
方法開始,gin 框架開始接管 http 請求
因此我們看下 gin.go 中的 ServerHTTP 方法
// ServeHTTP conforms to the http.Handler interface.
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
#爲減少gc重複回收, 這裏使用sync.pool管理自定義Context對象
c := engine.pool.Get().(*Context)
c.writermem.reset(w)
c.Request = req
c.reset()
#分發處理請求
engine.handleHTTPRequest(c)
#將Context對象放回
engine.pool.Put(c)
}
我們看下上面代碼, 主要做以下事情
-
爲減少 gc 重複回收, 這裏使用
sync.pool
管理自定義 Context 對象 -
將請求 reqeust 數據 copy 到 Context 對象中, 通過 Context 進行管理
-
調用
engine.handleHTTPRequest
進行路由分發 -
在這裏引入自定義的 Context 對象, 其主要是用來管理數據流轉過程時的,上下文數據, 比如 response, request, 請求參數 params, 路徑 fullpath, 查詢緩存, 錯誤管理, 主要的目的是: 避免重複複製數據。
-
保證數據的一致性。這是 gin 最重要的數據結構體
我們着重看下 engine.handleHTTPRequest
分發內部邏輯
func (engine *Engine) handleHTTPRequest(c *Context) {
httpMethod := c.Request.Method
rPath := c.Request.URL.Path
unescape := false
if engine.UseRawPath && len(c.Request.URL.RawPath) > 0 {
rPath = c.Request.URL.RawPath
unescape = engine.UnescapePathValues
}
#獲取請求路徑
rPath = cleanPath(rPath)
// 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
#根據路徑,請求參數,找到對應的 路由處理函數
value := root.getValue(rPath, c.Params, unescape)
if value.handlers != nil {
#更新Context對象屬性,將路由地址管理的多個路由函數都交給Context管理
c.handlers = value.handlers
c.Params = value.params
c.fullPath = value.fullPath
#遞歸的執行關聯的handler方法
c.Next()
c.writermem.WriteHeaderNow()
return
}
if httpMethod != "CONNECT" && rPath != "/" {
if value.tsr && engine.RedirectTrailingSlash {
redirectTrailingSlash(c)
return
}
if engine.RedirectFixedPath && redirectFixedPath(c, root, engine.RedirectFixedPath) {
return
}
}
break
}
if engine.HandleMethodNotAllowed {
for _, tree := range engine.trees {
if tree.method == httpMethod {
continue
}
if value := tree.root.getValue(rPath, nil, unescape); value.handlers != nil {
c.handlers = engine.allNoMethod
serveError(c, http.StatusMethodNotAllowed, default405Body)
return
}
}
}
c.handlers = engine.allNoRoute
serveError(c, http.StatusNotFound, default404Body)
}
上述代碼完成完整路由,進行數據的最終響應, 我們把核心代碼摘出來。
#根據路徑,請求參數,找到對應的 路由處理函數, 和我們上一章節實現類似
value := root.getValue(rPath, c.Params, unescape)
#遞歸的執行關聯的handler方法
c.Next()
我們先看 root.getValue
方法, 其主要進行 路由操作,這裏是採用的基樹Radix_tree
算法,實現較複雜,不再這裏詳細展開了。
我們再看下c.Next()
方法,這個方法的核心,主要是方便接入中間件 (Middleware),使得代碼模塊化操作。
我們看下 Next 的具體實現
func (c *Context) Next() {
c.index++
for c.index < int8(len(c.handlers)) {
#執行關聯的中間件方法或者 實際路由處理函數
c.handlers[c.index](c)
c.index++
}
}
描述相對抽象, 我們新建一個 demo 例子
import (
"fmt"
"github.com/gin-gonic/gin"
"log"
"time"
)
func Logger() gin.HandlerFunc {
return func(c *gin.Context) {
t := time.Now()
//請求處理前 Set example variable
c.Set("example", "12345")
//請求下一個中間件
c.Next()
//請求處理之後
latency := time.Since(t)
log.Print(latency)
// access the status we are sending
status := c.Writer.Status()
log.Println(status)
}
}
func main() {
r := gin.New()
//引入Logger()中間件
r.Use(Logger())
r.GET("/test", func(c *gin.Context) {
example := c.MustGet("example").(string)
//打印關聯的全部處理路由函數
fmt.Println(c.HandlerNames())
// it would print: "12345"
log.Println(example)
})
// Listen and serve on 0.0.0.0:8080
r.Run(":8080")
}
上面例子中, 我們引入一個 Logger 中間件, 在路由函數中打印全部路由函數名稱,輸出如下
[main.Logger.func1 main.main.func1]
即能看到,這裏將我們新加入的 Logger 中間件,轉換成了句柄函數,即 / testURI 對應的路由函數有兩個, 這兩個會按照先後順序, 依次執行。
即先執行 main.Logger.func1
, 後執行 main.main.func1
, 結合我們上面的 Next 方法實現, 我們就能清楚的知道,其調用關係
實現了 Next 方法的僞代碼,加深理解:
進入Next
index++= 0
調用handler方法,即Logger方法(如果index<總handler數)
進入Logger方法
//logger業務邏輯
進入Next方法
//index++ = 1
這裏還可以繼續加入其他中間件
//進入fun1方法
//從fun1方法離開
//index++ = 2
離開Next方法
//logger業務邏輯處理
離開Logger方法
index++ = 3
離開Next方法
邏輯處理完畢
這裏比較重要的概念是, 處理函數有先後執行關係, 並且處理函數可以通過調用 Abort 方法, 提前返回,不用遞歸調用到實際處理函數。
這些中間件,可以方便的使我們的業務代碼接入權限校驗 auth,日誌管理等其他功能模塊。
總結
核心亮點:路由管理對象的實現 + 中間件的實現原理
轉自:litonglitong
1905.github.io/golang%E5%BC%80%E5%8F%91/2019/08/14/golang-gin-webserver/
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/D6Ls3fUZnmimg8EgBDuXcQ