拆輪子系列:gin框架

樑健發表於2018-07-01

關於WEB框架

gin是go的輕量級的web框架,輕量級意味著僅僅提供web框架應有的基礎功能。我覺得看原始碼最好就是要有目標,看gin這個web框架,我的目標是:

  1. gin這個web框架是怎麼實現web框架應有的基礎功能的
  2. 程式碼上實現上有什麼值得學習的地方。

一次請求處理的大體流程

如何找到入口

要知道一次請求處理的大體流程,只要找到web框架的入口即可。先看看gin文件當中最簡單的demo。Run方法十分耀眼,點選去可以看到關鍵的http.ListenAndServe,這意味著Engine這個結構體,實現了ServeHTTP這個介面。入口就是Engine實現的ServeHTTP介面。

//我是最簡單的demo
func main() {
	r := gin.Default()
	r.GET("/ping", func(c *gin.Context) {
		c.JSON(200, gin.H{
			"message": "pong",
		})
		c.Redirect(http.StatusMovedPermanently, "https://github.com/gin-gonic/gin")

	})
	r.Run() // listen and serve on 0.0.0.0:8080
}

//我是Run方法
func (engine *Engine) Run(addr ...string) (err error) {
	defer func() { debugPrintError(err) }()

	address := resolveAddress(addr)
	debugPrint("Listening and serving HTTP on %s\n", address)
	err = http.ListenAndServe(address, engine)
	return
}
複製程式碼

ServeHTTP

大體流程就如註釋那樣,那麼的簡單。這裡值得關注的是,Context這個上下文物件是在物件池裡面取出來的,而不是每次都生成,提高效率。可以看到,真正的核心處理流程是在handleHTTPRequest方法當中。

func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {

	// 從上下文物件池中獲取一個上下文物件
	c := engine.pool.Get().(*Context)

	// 初始化上下文物件,因為從物件池取出來的資料,有髒資料,故要初始化。
	c.writermem.reset(w)
	c.Request = req
	c.reset()

	//處理web請求
	engine.handleHTTPRequest(c)

	//將Context物件扔回物件池了
	engine.pool.Put(c)
}
複製程式碼

handleHTTPRequest

下面的程式碼省略了很多和核心邏輯無關的程式碼,核心邏輯很簡單:更具請求方法和請求的URI找到處理函式們,然後呼叫。為什麼是處理函式們,而不是我們寫的處理函式?因為這裡包括了中間層的處理函式。

func (engine *Engine) handleHTTPRequest(context *Context) {
	httpMethod := context.Request.Method
	var path string
	var unescape bool

	// 省略......
	// tree是個陣列,裡面儲存著對應的請求方式的,URI與處理函式的樹。
	// 之所以用陣列是因為,在個數少的時候,陣列查詢比字典要快
	t := engine.trees
	for i, tl := 0, len(t); i < tl; i++ {
		
		if t[i].method == httpMethod {
			root := t[i].root
			
			// 找到路由對應的處理函式們
			handlers, params, tsr := root.getValue(path, context.Params, unescape)
			
			// 呼叫處理函式們
			if handlers != nil {
				context.handlers = handlers
				context.Params = params
				context.Next()
				context.writermem.WriteHeaderNow()
				return
			}
			
			// 省略......
			break
		}
	}

	// 省略......
}
複製程式碼

值得欣賞學習的地方

路由處理

關鍵需求

先拋開gin框架不說,路由處理的關鍵需求有哪些?個人認為有以下兩點

  • 高效的URI對應的處理函式的查詢
  • 靈活的路由組合

gin的處理

核心思路

  • 每一個路由對應的都有一個獨立的處理函式陣列
  • 中介軟體與處理函式是一致的
  • 利用樹提供高效的URI對應的處理函式陣列的查詢

有趣的地方

RouterGroup對路由的處理

靈活的路由組合是通過將每一個URI都應用著一個獨立的處理函式陣列來實現的。對於路由組合的操作抽象出了RouterGroup結構體來應對。它的主要作用是:

  • 將路由與相關的處理函式關聯起來
  • 提供了路由組的功能,這個是由於關聯字首的方式實現的
  • 提供了中介軟體自由組合的功能:1. 總的中介軟體 2. 路由組的中介軟體 3.處理函式的中介軟體

路由組和處理函式都可以新增中介軟體這比DJango那種只有總的中介軟體要靈活得多。

中介軟體的處理

中介軟體在請求的時候需要處理,在返回時也可能需要做處理。如下圖(圖是django的)。

中介軟體的處理
問題來了在gin中介軟體就是一個處理函式,怎麼實現返回時的處理呢。仔細觀察,上面圖的呼叫,就是後進先出,是的每錯答案就是:利用函式呼叫棧後進先出的特點,巧妙的完成中介軟體在自定義處理函式完成的後處理的操作。django它的處理方式是定義個類,請求處理前的處理的定義一個方法,請求處理後的處理定義一個方法。gin的方式更靈活,但django的方式更加清晰。

//呼叫處理函式陣列
func (c *Context) Next() {
	c.index++
	s := int8(len(c.handlers))
	for ; c.index < s; c.index++ {
		c.handlers[c.index](c)
	}
}

// 中介軟體例子
func Logger() gin.HandlerFunc {
	return func(c *gin.Context) {
		t := time.Now()

		// before request
		c.Set("example", "12345")

		c.Next()

		// 返回後的處理
		latency := time.Since(t)
		log.Print("latency: ", latency)

		status := c.Writer.Status()
		log.Println("status: ", status)
	}
}

func main() {
	r := gin.New()
	r.Use(Logger())

	r.GET("/test", func(c *gin.Context) {
		example := c.MustGet("example").(string)

		// it would print: "12345"
		log.Println("example", example)
	})

	// Listen and serve on 0.0.0.0:8080
	r.Run(":8081")
}

複製程式碼

請求內容的處理與返回內容的處理

需求

  • 獲取路徑當中的引數
  • 獲取請求引數
  • 獲取請求內容
  • 將處理好的結果返回

Gin框架的實現思路

自己包裝一層除了能提供體驗一致的處理方法之外,如果對官方實現的不爽,可以替換掉,甚至可以加一層快取處理(其實沒必要,因為正常的使用,僅僅只會處理一次就夠了)。

  • 如果官方的http庫能提供的,則在官方的http庫只上包裝一層,提供體驗一致的介面。
  • 官方http庫不能提供的,則自己實現

關鍵結構體

type Context struct {
	writermem responseWriter
	Request   *http.Request

	// 傳遞介面,使用各個處理函式,更加靈活,降低耦合
	Writer    ResponseWriter
	
	Params   Params				// 路徑當中的引數
	handlers HandlersChain		// 處理函式陣列
	index    int8				// 目前在執行著第幾個處理函式

	engine   *Engine
	Keys     map[string]interface{}  // 各個中介軟體新增的key value
	Errors   errorMsgs
	Accepted []string
}
複製程式碼

值得學習的點

在數量少的情況下用陣列查詢值,比用字典查詢值要快

在上面對Context結構體的註釋當中,可以知道Params其實是個陣列。本質上可以說是key值的對應,為啥不用字典呢,而是用陣列呢? 實際的場景,獲取路徑引數的引數個數不會很多,如果用字典效能反而不如陣列高。因為字典要找到對應的值,大體的流程:對key進行hash —> 通過某演算法找到對應偏移的位置(有好幾種演算法,有興趣的可以去查檢視) —> 取值。一套流程下來,陣列在量少的情況下,已經遍歷完了。

router.GET("user/:name/*action", func(c *gin.Context) {
		name := c.Param("name")
		action := c.Param("action")
		message := name + " is222 " + action
		c.String(http.StatusOK, message)
	})
   
func (ps Params) Get(name string) (string, bool) {
	for _, entry := range ps {
		if entry.Key == name {
			return entry.Value, true
		}
	}
	return "", false
}
複製程式碼

通過介面處理有所同,有所不同的場景

獲取請求內容

對於獲取請求內容這個需求面臨著的場景。對於go這種靜態語言來說,如果要對請求內容進行處理,就需要對內容進行反序列化到某個結構體當中,然而請求內容的形式多種多樣,例如:JSON,XML,ProtoBuf等等。因此這裡可以總結出下面的非功能性需求。

  • 不同的內容需要不同的反序列化機制
  • 允許使用者自己實現反序列化機制

共同點都是對內容做處理,不同點是對內容的處理方式不一樣,很容易讓人想到多型這概念,異種求同。多型的核心就是介面,這時候需要抽象出一個介面。

type Binding interface {
	Name() string
	Bind(*http.Request, interface{}) error
}
複製程式碼

將處理好的內容返回

請求內容多種多樣,返回的內容也是一樣的。例如:返回JSON,返回XML,返回HTML,返回302等等。這裡可以總結出以下非功能性需求。

  • 不同型別的返回內容需要不同的序列化機制
  • 允許使用者實現自己的序列化機制

和上面的一致的,因此這裡也抽象出一個介面。

type Render interface {
	Render(http.ResponseWriter) error
	WriteContentType(w http.ResponseWriter)
}
複製程式碼

介面定義好之後需要思考如何使用介面

思考如何優雅的使用這些介面

對於獲取請求內容,在模型繫結當中,有以下的場景

  • 繫結失敗是使用者自己處理還是框架統一進行處理
  • 使用者需是否需要關心請求的內容選擇不同的繫結器

在gin框架的對於這些場景給出的答案是:提供不同的方法,滿足以上的需求。這裡的關鍵點還是在於使用場景是怎樣的。

// 自動更加請求頭選擇不同的繫結器物件進行處理
func (c *Context) Bind(obj interface{}) error {
	b := binding.Default(c.Request.Method, c.ContentType())
	return c.MustBindWith(obj, b)
}

// 繫結失敗後,框架會進行統一的處理
func (c *Context) MustBindWith(obj interface{}, b binding.Binding) (err error) {
	if err = c.ShouldBindWith(obj, b); err != nil {
		c.AbortWithError(400, err).SetType(ErrorTypeBind)
	}

	return
}

// 使用者可以自行選擇繫結器,自行對出錯處理。自行選擇繫結器,這也意味著使用者可以自己實現繫結器。
// 例如:嫌棄預設的json處理是用官方的json處理包,嫌棄它慢,可以自己實現Binding介面
func (c *Context) ShouldBindWith(obj interface{}, b binding.Binding) error {
	return b.Bind(c.Request, obj)
}
複製程式碼

對於實現的結構體構造不一致的處理

將處理好的內容返回,實現的類構造引數都是不一致的。例如:對於文字的處理和對於json的處理。面對這種場景祭出的武器是:封裝多一層,用於構造出相對於的處理物件。

//對於String的處理
type String struct {
	Format string
	Data   []interface{}
}

//對於String處理封裝多的一層 
func (c *Context) String(code int, format string, values ...interface{}) {
	c.Render(code, render.String{Format: format, Data: values})
}

//對於json的處理
JSON struct {
	Data interface{}
}

//對於json的處理封裝多的一層
func (c *Context) JSON(code int, obj interface{}) {
	c.Render(code, render.JSON{Data: obj})
}

//核心的一致的處理
func (c *Context) Render(code int, r render.Render) {
	c.Status(code)

	if !bodyAllowedForStatus(code) {
		r.WriteContentType(c.Writer)
		c.Writer.WriteHeaderNow()
		return
	}

	if err := r.Render(c.Writer); err != nil {
		panic(err)
	}
}
複製程式碼

總結

這個看程式碼的過程是在有目標之後,按照官方文件的例子,一步一步的看的。然後再慢慢欣賞,這框架對於一些web框架常見的場景,它是怎麼處理。這框架的程式碼量很少,而且寫得十分的優雅,非常值得一看。

相關文章