Gin 是一個用 Go(Golang)寫的 HTTP Web 框架。它具有類似 Martini 的 API,但效能比 Martini 快 40 倍。如果您需要精彩的表演,那就給自己點杜松子酒吧。
Gin 官網介紹自己是一個具有「高效能」和「良好生產力」的 Web 框架。它還提到了另外兩個庫。第一個是Martini,它也是一個Web框架,並且有一個酒的名字。 Gin 表示它使用其 API,但速度快了 40 倍。使用httprouter是它比Martini快40倍的重要原因。
官網的「Features」中列出了八個關鍵功能,稍後我們將逐步看到這些功能的實現。
讓我們來看看官方文件中給出的最小範例。
package main import "github.com/gin-gonic/gin" func main() { 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://localhost:8080/ping,就會得到「乒乓」聲。
這個例子非常簡單。它可以分為三個步驟:
從上面小例子中的GET方法我們可以看出,在Gin中,HTTP方法的處理方法需要使用對應的同名函數來註冊。
HTTP 方法有九種,最常用的四種是 GET、POST、PUT 和 DELETE,分別對應查詢、插入、更新和刪除四種功能。需要注意的是,Gin還提供了Any接口,可以直接將所有HTTP方法處理方法綁定到一個位址。
傳回的結果一般包含兩部分或三部分。代碼和訊息始終存在,資料通常用於表示附加資料。如果沒有額外的資料要返回,則可以省略。在範例中,200 是 code 欄位的值,「pong」是 message 欄位的值。
在上面的範例中,gin.Default() 用來建立引擎。然而,這個函數是 New 的包裝。其實Engine就是透過New介面創建的。
package main import "github.com/gin-gonic/gin" func main() { 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 }
暫時簡單看一下建立過程,不要注意Engine結構體中各個成員變數的意義。可以看到,New除了建立並初始化一個Engine類型的引擎變數外,還將engine.pool.New設定為一個呼叫engine.allocateContext()的匿名函數。這個函數的作用後面會講。
Engine 中有一個嵌入的 struct RouterGroup。 Engine的HTTP方法相關介面皆繼承自RouterGroup。官網提到的功能點中的「路由分組」是透過RouterGroup結構體實現的。
func New() *Engine { debugPrintWARNINGNew() engine := &Engine{ RouterGroup: RouterGroup{ //... Initialize the fields of RouterGroup }, //... Initialize the remaining fields } engine.RouterGroup.engine = engine // Save the pointer of the engine in RouterGroup engine.pool.New = func() any { return engine.allocateContext() } return engine }
每個 RouterGroup 都與一個基本路徑 basePath 相關聯。 Engine 中嵌入的 RouterGroup 的基本路徑是「/」。
還有一組處理函數Handlers。所有與該群組關聯的路徑下的請求都會額外執行該群組的處理函數,主要用於中間件呼叫。 Engine建立時Handlers為nil,可以透過Use方法匯入一組函數。我們稍後會看到這個用法。
type RouterGroup struct { Handlers HandlersChain // Processing functions of the group itself basePath string // Associated base path engine *Engine // Save the associated engine object root bool // root flag, only the one created by default in Engine is true }
RouterGroup的handle方法是註冊所有HTTP方法回呼函數的最終入口。初始範例中呼叫的 GET 方法和其他與 HTTP 方法相關的方法只是對 handle 方法的包裝。
handle方法會根據RouterGroup的basePath和相對路徑參數計算出絕對路徑,同時呼叫combineHandlers方法得到最終的handlers陣列。這些結果會作為參數傳遞給Engine的addRoute方法來註冊處理函數。
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() }
combineHandlers方法的作用是建立一個切片mergedHandlers,然後將RouterGroup本身的Handler複製到其中,然後將參數的handler複製到其中,最後傳回mergedHandlers。也就是說,當使用handle註冊任何方法時,實際的結果都包含了RouterGroup本身的Handler。
在官網提到的「快速」特徵點中,提到網路請求的路由是基於基數樹(Radix Tree)實現的。這部分並不是Gin實現的,而是由一開始介紹Gin時提到的httprouter實現的。 Gin使用httprouter來實現這部分功能。基數樹的實現這裡暫時不講。我們現在只關注它的用法。也許稍後我們會單獨寫一篇文章來介紹基數樹的實作。
在引擎中,有一個 trees 變量,它是 methodTree 結構的切片。正是這個變數保存了對所有基數樹的引用。
package main import "github.com/gin-gonic/gin" func main() { 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 方法維護一個基數樹。這棵樹的根節點和方法的名稱一起保存在一個methodTree變數中,所有methodTree變數都在樹中。
func New() *Engine { debugPrintWARNINGNew() engine := &Engine{ RouterGroup: RouterGroup{ //... Initialize the fields of RouterGroup }, //... Initialize the remaining fields } engine.RouterGroup.engine = engine // Save the pointer of the engine in RouterGroup engine.pool.New = func() any { return engine.allocateContext() } return engine }
可以看到,在Engine的addRoute方法中,首先會使用trees的get方法來取得此方法對應的radix樹的根節點。如果沒有取得到基數樹的根節點,則表示先前沒有為該方法註冊過任何方法,將會建立一個樹節點作為樹的根節點,並加入到樹中。
取得根節點後,使用根節點的addRoute方法註冊一組針對路徑path的處理函數handler。這一步驟是為路徑和處理程序建立一個節點並將其儲存在基數樹中。如果你嘗試註冊一個已經註冊的地址,addRoute會直接拋出一個panic錯誤。
在處理HTTP請求時,需要透過路徑找到對應節點的值。根節點有一個getValue方法負責處理查詢作業。我們在談論 Gin 處理 HTTP 請求時會提到這一點。
RouterGroup的Use方法可以匯入一組中間件處理函數。官網提到的功能點中的「中間件支援」是透過Use方法實現的。
在最初的範例中,在建立Engine結構體變數時,沒有使用New,而是使用了Default。讓我們看看 Default 額外做了什麼。
package main import "github.com/gin-gonic/gin" func main() { 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 }
可以看出,這是一個非常簡單的函數。除了呼叫New建立Engine物件外,只呼叫Use導入Logger和Recovery兩個中間件函數的回傳值。 Logger的傳回值是用來記錄日誌的函數,Recovery的傳回值是用來處理panic的函數。我們暫時跳過這個,稍後再看這兩個函數。
雖然Engine內嵌了RouterGroup,也實作了Use方法,但只是呼叫了RouterGroup的Use方法以及一些輔助操作。
func New() *Engine { debugPrintWARNINGNew() engine := &Engine{ RouterGroup: RouterGroup{ //... Initialize the fields of RouterGroup }, //... Initialize the remaining fields } engine.RouterGroup.engine = engine // Save the pointer of the engine in RouterGroup engine.pool.New = func() any { return engine.allocateContext() } return engine }
可見RouterGroup的使用方法也非常簡單。它只是透過append將參數的中間件處理功能添加到自己的Handler中。
在這個小例子中,最後一步就是不帶參數呼叫 Engine 的 Run 方法。呼叫後,整個框架開始運行,用瀏覽器存取註冊地址即可正確觸發回調。
type RouterGroup struct { Handlers HandlersChain // Processing functions of the group itself basePath string // Associated base path engine *Engine // Save the associated engine object root bool // root flag, only the one created by default in Engine is true }
Run方法只做兩件事:解析位址和啟動服務。這裡地址其實只要傳一個字串就可以了,但為了達到能傳能不能傳的效果,使用了一個可變參數。 resolveAddress方法處理addr不同情況的結果。
啟動服務使用標準庫的net/http套件中的ListenAndServe方法。此方法接受一個監聽位址和一個Handler介面的變數。 Handler介面的定義非常簡單,只有一個ServeHTTP方法。
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() }
因為Engine實作了ServeHTTP,所以Engine本身將會被傳遞到這裡的ListenAndServe方法。當監聽埠有新的連接時,ListenAndServe會負責接受並建立連接,當連接上有資料時,會呼叫handler的ServeHTTP方法進行處理。
Engine的ServeHTTP是處理訊息的回呼函數。讓我們來看看它的內容。
func (group *RouterGroup) combineHandlers(handlers HandlersChain) HandlersChain { finalSize := len(group.Handlers) + len(handlers) assert1(finalSize < int(abortIndex), "too many handlers") mergedHandlers := make(HandlersChain, finalSize) copy(mergedHandlers, group.Handlers) copy(mergedHandlers[len(group.Handlers):], handlers) return mergedHandlers }
回呼函數有兩個參數。第一個是w,用於接收請求回覆。將回覆資料寫入w。另一個是req,保存本次請求的資料。後續處理所需的所有資料都可以從req讀取
ServeHTTP 方法做了四件事。首先從pool池取得一個Context,然後將Context與回呼函數的參數綁定,然後以Context為參數呼叫handleHTTPRequest方法來處理這次網路請求,最後將Context放回池中。
我們首先只看handleHTTPRequest方法的核心部分。
package main import "github.com/gin-gonic/gin" func main() { 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 }
handleHTTPRequest方法主要做了兩件事。首先根據請求的位址從基數樹中取得先前註冊的方法。這裡,handlers會被指派到Context中進行本次處理,然後呼叫Context的Next函數來執行handlers中的方法。最後將本次請求的回傳資料寫入Context的responseWriter類型物件中。
處理 HTTP 請求時,所有與上下文相關的資料都在 Context 變數中。作者也在Context結構體的註釋中寫到“Context is the most important part of gin”,可見其重要性。
上面講Engine的ServeHTTP方法時可以看出,Context並不是直接建立的,而是透過Engine的pool變數的Get方法取得的。取出後,使用前重置其狀態,使用後放回池中。
Engine 的池變數的類型為sync.Pool。目前只知道它是Go官方提供的支援並發使用的物件池。您可以透過 Get 方法從池中取得對象,也可以使用 Put 方法將物件放入池中。當池為空並且使用Get方法時,它會透過自己的New方法建立物件並傳回。
這個New方法是在Engine的New方法中定義的。我們再看一下Engine的New方法。
func New() *Engine { debugPrintWARNINGNew() engine := &Engine{ RouterGroup: RouterGroup{ //... Initialize the fields of RouterGroup }, //... Initialize the remaining fields } engine.RouterGroup.engine = engine // Save the pointer of the engine in RouterGroup engine.pool.New = func() any { return engine.allocateContext() } return engine }
從程式碼可以看出Context的建立方法是Engine的allocateContext方法。 allocateContext 方法並沒有什麼神秘之處。它只是對切片長度進行兩步預分配,然後創建物件並返回它。
type RouterGroup struct { Handlers HandlersChain // Processing functions of the group itself basePath string // Associated base path engine *Engine // Save the associated engine object root bool // root flag, only the one created by default in Engine is true }
上面提到的 Context 的 Next 方法將執行處理程序中的所有方法。讓我們來看看它的實現。
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() }
雖然handlers是一個切片,但是Next方法並不是簡單地實現為handlers的遍歷,而是引入了一個處理進度記錄索引,該索引初始化為0,在方法開始時遞增,在方法結束後再次遞增執行完成。
Next的設計和它的用法有很大關係,主要是為了配合一些中間件功能。例如,當某個handler執行過程中觸發panic時,可以使用中間件中的recover捕獲錯誤,然後再次呼叫Next繼續執行後續的handler,而不會因為該問題影響整個handlers數組一名處理程序。
在Gin中,如果某個請求的處理函數觸發了panic,整個框架並不會直接崩潰。相反,將拋出錯誤訊息,並將繼續提供服務。這有點類似Lua框架通常使用xpcall來執行訊息處理函數。這個操作就是官方文件中提到的「Crash-free」特性點。
上述提到,使用 gin.Default 建立 Engine 時,會執行 Engine 的 Use 方法來匯入兩個函數。其中之一是 Recovery 函數的傳回值,它是其他函數的包裝。最終呼叫的函數是CustomRecoveryWithWriter。我們來看看這個函數的實作。
package main import "github.com/gin-gonic/gin" func main() { 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 }
這裡我們不關注錯誤處理的細節,而只看看它做了什麼。該函數傳回一個匿名函數。在這個匿名函數中,使用defer註冊了另一個匿名函數。在這個內部匿名函式中,使用recover來捕捉panic,然後進行錯誤處理。處理完成後,呼叫Context的Next方法,這樣Context原本依序執行的處理程序就可以繼續執行。
最後跟大家介紹一下部署Gin服務最好的平台:Leapcell。
在文件中探索更多內容!
Leapcell Twitter:https://x.com/LeapcellHQ
以上是深入探討 Gin:Golang 的領先框架的詳細內容。更多資訊請關注PHP中文網其他相關文章!