關於WEB框架
gin是go的輕量級的web框架,輕量級意味著僅僅提供web框架應有的基礎功能。我覺得看原始碼最好就是要有目標,看gin這個web框架,我的目標是:
- gin這個web框架是怎麼實現web框架應有的基礎功能的
- 程式碼上實現上有什麼值得學習的地方。
一次請求處理的大體流程
如何找到入口
要知道一次請求處理的大體流程,只要找到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框架常見的場景,它是怎麼處理。這框架的程式碼量很少,而且寫得十分的優雅,非常值得一看。