輕量級 Web 框架 Gin 結構分析

Go 語言最流行了兩個輕量級 Web 框架分別是 Gin 和 Echo,這兩個框架大同小異,都是插件式輕量級框架,背後都有一個開源小生態來提供各式各樣的小插件,這兩個框架的性能也都很是好,裸測起來跑的飛快。本節咱們只講 Gin 的實現原理和使用方法,Gin 起步比 Echo 要早,市場佔有率要高一些,生態也豐富一些。node

go get -u github.com/gin-gonic/gin
複製代碼

Hello World

Gin 框架的 Hello World 只須要 10 行代碼,比大多數動態腳本語言稍微多幾行。git

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
}

複製代碼

代碼中的 gin.H 是 map[string]interface{} 的一個快捷名稱,寫起來會更加簡潔。github

type H map[string]interface{}
複製代碼

gin.Engine

Engine 是 Gin 框架最重要的數據結構,它是框架的入口。咱們經過 Engine 對象來定義服務路由信息、組裝插件、運行服務。正如 Engine 的中文意思「引擎」同樣,它就是框架的核心發動機,整個 Web 服務的都是由它來驅動的。數組

發動機屬於精密設備,構造很是複雜,不過 Engine 對象很簡單,由於引擎最重要的部分 —— 底層的 HTTP 服務器使用的是 Go 語言內置的 http server,Engine 的本質只是對內置的 HTTP 服務器的包裝,讓它使用起來更加便捷。bash

gin.Default() 函數會生成一個默認的 Engine 對象,裏面包含了 2 個默認的經常使用插件,分別是 Logger 和 Recovery,Logger 用於輸出請求日誌,Recovery 確保單個請求發生 panic 時記錄異常堆棧日誌,輸出統一的錯誤響應。服務器

func Default() *Engine {
	engine := New()
	engine.Use(Logger(), Recovery())
	return engine
}
複製代碼

路由樹

在 Gin 框架中,路由規則被分紅了最多 9 棵前綴樹,每個 HTTP Method對應一棵「前綴樹」,樹的節點按照 URL 中的 / 符號進行層級劃分,URL 支持 :name 形式的名稱匹配,還支持 *subpath 形式的路徑通配符 。網絡

// 匹配單節點 named
pattern = /book/:id
match /book/123
nomatch /book/123/10
nomatch /book/

// 匹配子節點 catchAll mode
/book/*subpath
match /book/
match /book/123
match /book/123/10
複製代碼

每一個節點都會掛接若干請求處理函數構成一個請求處理鏈 HandlersChain。當一個請求到來時,在這棵樹上找到請求 URL 對應的節點,拿到對應的請求處理鏈來執行就完成了請求的處理。

type Engine struct {
  ...
  trees methodTrees
  ...
}

type methodTrees []methodTree

type methodTree struct {
	method string
	root   *node  // 樹根
}

type node struct {
  path string // 當前節點的路徑
  ...
  handlers HandlersChain // 請求處理鏈
  ...
}

type HandlerFunc func(*Context)

type HandlersChain []HandlerFunc
複製代碼

Engine 對象包含一個 addRoute 方法用於添加 URL 請求處理器,它會將對應的路徑和處理器掛接到相應的請求樹中數據結構

func (e *Engine) addRoute(method, path string, handlers HandlersChain)
複製代碼

gin.RouterGroup

RouterGroup 是對路由樹的包裝,全部的路由規則最終都是由它來進行管理。Engine 結構體繼承了 RouterGroup ,因此 Engine 直接具有了 RouterGroup 全部的路由管理功能。這是爲何在 Hello World 的例子中,能夠直接使用 Engine 對象來定義路由規則。同時 RouteGroup 對象裏面還會包含一個 Engine 的指針,這樣 Engine 和 RouteGroup 就成了「你中有我我中有你」的關係。app

type Engine struct {
  RouterGroup
  ...
}

type RouterGroup struct {
  ...
  engine *Engine
  ...
}
複製代碼

RouterGroup 實現了 IRouter 接口,暴露了一系列路由方法,這些方法最終都是經過調用 Engine.addRoute 方法將請求處理器掛接到路由樹中。框架

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
// 匹配全部 HTTP Method
Any(string, ...HandlerFunc) IRoutes
複製代碼

RouterGroup 內部有一個前綴路徑屬性,它會將全部的子路徑都加上這個前綴再放進路由樹中。有了這個前綴路徑,就能夠實現 URL 分組功能。Engine 對象內嵌的 RouterGroup 對象的前綴路徑是 /,它表示根路徑。RouterGroup 支持分組嵌套,使用 Group 方法就可讓分組下面再掛分組,因而子子孫孫無窮盡也。

func main() {
	router := gin.Default()

	v1 := router.Group("/v1")
	{
		v1.POST("/login", loginEndpoint)
		v1.POST("/submit", submitEndpoint)
		v1.POST("/read", readEndpoint)
	}

	v2 := router.Group("/v2")
	{
		v2.POST("/login", loginEndpoint)
		v2.POST("/submit", submitEndpoint)
		v2.POST("/read", readEndpoint)
	}

	router.Run(":8080")
}
複製代碼

上面這個例子中實際上已經使用了分組嵌套,由於 Engine 對象裏面的 RouterGroup 對象就是第一層分組,也就是根分組,v1 和 v2 都是根分組的子分組。

gin.Context

這個對象裏保存了請求的上下文信息,它是全部請求處理器的入口參數。

type HandlerFunc func(*Context)

type Context struct {
  ...
  Request *http.Request // 請求對象
  Writer ResponseWriter // 響應對象
  Params Params // URL匹配參數
  ...
  Keys map[string]interface{} // 自定義上下文信息
  ...
}
複製代碼

Context 對象提供了很是豐富的方法用於獲取當前請求的上下文信息,若是你須要獲取請求中的 URL 參數、Cookie、Header 均可以經過 Context 對象來獲取。這一系列方法本質上是對 http.Request 對象的包裝。

// 獲取 URL 匹配參數  /book/:id
func (c *Context) Param(key string) string
// 獲取 URL 查詢參數 /book?id=123&page=10
func (c *Context) Query(key string) string
// 獲取 POST 表單參數
func (c *Context) PostForm(key string) string
// 獲取上傳的文件對象
func (c *Context) FormFile(name string) (*multipart.FileHeader, error)
// 獲取請求Cookie
func (c *Context) Cookie(name string) (string, error) 
...
複製代碼

Context 對象提供了不少內置的響應形式,JSON、HTML、Protobuf 、MsgPack、Yaml 等。它會爲每一種形式都單獨定製一個渲染器。一般這些內置渲染器已經足夠應付絕大多數場景,若是你以爲不夠,還能夠自定義渲染器。

func (c *Context) JSON(code int, obj interface{})
func (c *Context) Protobuf(code int, obj interface{})
func (c *Context) YAML(code int, obj interface{})
...
// 自定義渲染
func (c *Context) Render(code int, r render.Render)

// 渲染器通用接口
type Render interface {
	Render(http.ResponseWriter) error
	WriteContentType(w http.ResponseWriter)
}
複製代碼

全部的渲染器最終仍是須要調用內置的 http.ResponseWriter(Context.Writer) 將響應對象轉換成字節流寫到套接字中。

type ResponseWriter interface {
 // 容納全部的響應頭
 Header() Header
 // 寫Body
 Write([]byte) (int, error)
 // 寫Header
 WriteHeader(statusCode int)
}
複製代碼

插件與請求鏈

咱們編寫業務代碼時通常也就是一個處理函數,爲何路由節點須要掛接一個函數鏈呢?

type node struct {
  path string // 當前節點的路徑
  ...
  handlers HandlersChain // 請求處理鏈
  ...
}
type HandlerFunc func(*Context)
type HandlersChain []HandlerFunc
複製代碼

這是由於 Gin 提供了插件,只有函數鏈的尾部是業務處理,前面的部分都是插件函數。在 Gin 中插件和業務處理函數形式是同樣的,都是 func(*Context)。當咱們定義路由時,Gin 會將插件函數和業務處理函數合併在一塊兒造成一個鏈條結構。

type Context struct {
  ...
  index uint8 // 當前的業務邏輯位於函數鏈的位置
  handlers HandlersChain // 函數鏈
  ...
}

// 挨個調用鏈條中的處理函數
func (c *Context) Next() {
	c.index++
	for s := int8(len(c.handlers)); c.index < s; c.index++ {
		c.handlers[c.index](c)
	}
}
複製代碼

Gin 在接收到客戶端請求時,找到相應的處理鏈,構造一個 Context 對象,再調用它的 Next() 方法就正式進入了請求處理的全流程。

Gin 還支持 Abort() 方法中斷請求鏈的執行,它的原理是將 Context.index 調整到一個比較大的數字,這樣 Next() 方法中的調用循環就會當即結束。須要注意的 Abort() 方法並非經過 panic 的方式中斷執行流,執行 Abort() 方法以後,當前函數內後面的代碼邏輯還會繼續執行。

const abortIndex = 127
func (c *Context) Abort() {
	c.index = abortIndex
}

func SomePlugin(c *Context) {
  ...
  if condition {
    c.Abort()
    // continue executing
  }
  ...
}
複製代碼

若是在插件中顯示調用 Next() 方法,那麼它就改變了正常的順序執行流,變成了像洋蔥同樣的嵌套執行流。換個角度來理解,正常的執行流就是後續的處理器是在前一個處理器的尾部執行,而嵌套執行流是讓後續的處理器在前一個處理器進行到一半的時候執行,待後續處理器完成執行後,再回到前一個處理器繼續往下執行。

要是你學過 Python 語言,這種嵌套結構很容易讓人聯想到裝飾器 decorator。若是全部的插件都使用嵌套執行流,那麼就會變成了下面這張圖

RouterGroup 提供了 Use() 方法來註冊插件,由於 RouterGroup 是一層套一層,不一樣層級的路由可能會註冊不同的插件,最終不一樣的路由節點掛接的處理函數鏈也不盡相同。

func (group *RouterGroup) Use(middleware ...HandlerFunc) IRoutes {
	group.Handlers = append(group.Handlers, middleware...)
	return group.returnObj()
}

// 註冊 Get 請求
func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes {
	return group.handle("GET", relativePath, handlers)
}

func (g *RouterGroup) handle(method, path string, handlers HandlersChain) IRoutes {
 // 合併URL (RouterGroup有URL前綴)
	absolutePath := group.calculateAbsolutePath(relativePath)
	// 合併處理鏈條
 handlers = group.combineHandlers(handlers)
	// 註冊路由樹
 group.engine.addRoute(httpMethod, absolutePath, handlers)
	return group.returnObj()
}
複製代碼

HTTP 錯誤

當 URL 請求對應的路徑不能在路由樹裏找到時,就須要處理 404 NotFound 錯誤。當 URL 的請求路徑能夠在路由樹裏找到,可是 Method 不匹配,就須要處理 405 MethodNotAllowed 錯誤。Engine 對象爲這兩個錯誤提供了處理器註冊的入口

func (engine *Engine) NoMethod(handlers ...HandlerFunc)
func (engine *Engine) NoRoute(handlers ...HandlerFunc)
複製代碼

異常處理器和普通處理器同樣,也須要和插件函數組合在一塊兒造成一個調用鏈。若是沒有提供異常處理器,Gin 就會使用內置的簡易錯誤處理器。

注意這兩個錯誤處理器是定義在 Engine 全局對象上,而不是 RouterGroup。對於非 404 和 405 錯誤,須要用戶自定義插件來處理。對於 panic 拋出來的異常須要也須要使用插件來處理。

靜態文件服務

RouterGroup 對象裏定義了下面三個用來服務靜態文件的方法

// 服務單個靜態文件
StaticFile(relativePath, filePath string) IRoutes
// 服務靜態文件目錄
Static(relativePath, dirRoot string) IRoutes
// 服務虛擬靜態文件系統
StaticFS(relativePath string, fs http.FileSystem) IRoutes
複製代碼

它不一樣於錯誤處理器,靜態文件服務掛在 RouterGroup 上,支持嵌套。這三個方法中 StaticFS 方法比較特別,它對文件系統進行了抽象,你能夠提供一個基於網絡的靜態文件系統,也能夠提供一個基於內存的靜態文件系統。FileSystem 接口也很簡單,提供一個路徑參數返回一個實現了 File 接口的文件對象。不一樣的虛擬文件系統使用不一樣的代碼來實現 File 接口。

type FileSystem interface {
 Open(path string) (File, error)
}

type File interface {
 io.Closer
 io.Reader
 io.Seeker
 Readdir(count int) ([]os.FileInfo, error)
 Stat() (os.FileInfo, error)
}
複製代碼

靜態文件處理器和普通處理器同樣,也須要通過插件的重重過濾。

表單處理

當請求參數數量比較多時,使用 Context.Query() 和 Context.PostForm() 方法來獲取參數就會顯得比較繁瑣。Gin 框架也支持表單處理,將表單參數和結構體字段進行直接映射。

package main

import (
	"github.com/gin-gonic/gin"
)

type LoginForm struct {
	User     string `form:"user" binding:"required"`
	Password string `form:"password" binding:"required"`
}

func main() {
	router := gin.Default()
	router.POST("/login", func(c *gin.Context) {
  var form LoginForm
		if c.ShouldBind(&form) == nil {
			if form.User == "user" && form.Password == "password" {
				c.JSON(200, gin.H{"status": "you are logged in"})
			} else {
				c.JSON(401, gin.H{"status": "unauthorized"})
			}
		}
	})
	router.Run(":8080")
}
複製代碼

Context.ShouldBind 方法遇到校驗不經過時,會返回一個錯誤對象告知調用者校驗失敗的緣由。它支持多種數據綁定類型,如 XML、JSON、Query、Uri、MsgPack、Protobuf等,根據請求的 Content-Type 頭來決定使用何種數據綁定方法。

func (c *Context) ShouldBind(obj interface{}) error {
 // 獲取綁定器
	b := binding.Default(c.Request.Method, c.ContentType())
	// 執行綁定
 return c.ShouldBindWith(obj, b)
}
複製代碼

默認內置的表單校驗功能很強大,它經過結構體字段 tag 標註來選擇相應的校驗器進行校驗。Gin 還提供了註冊自定義校驗器的入口,支持用戶自定義一些通用的特殊校驗邏輯。

Context.ShouldBind 是比較柔和的校驗方法,它只負責校驗,並將校驗結果以返回值的形式傳遞給上層。Context 還有另一個比較暴力的校驗方法 Context.Bind,它和 ShouldBind 的調用形式一摸同樣,區別是當校驗錯誤發生時,它會調用 Abort() 方法中斷調用鏈的執行,向客戶端返回一個 HTTP 400 Bad Request 錯誤。

HTTPS

Gin 不支持 HTTPS,官方建議是使用 Nginx 來轉發 HTTPS 請求到 Gin。

相關文章
相關標籤/搜索