Go框架解析-gin

http://cdn.tigerb.cn/20190705124639.jpg

前言

今天是我golang框架閱讀系列第三篇文章,今天我們主要看看gin的框架執行流程。關於golang框架生命週期源碼閱讀下面是我的計劃:

計劃 狀態
Go框架解析-beego done
Go框架解析-iris done
Go框架解析-gin done
Go框架解析-echo doing
Go框架解析-revel doing
Go框架解析-Martini doing

再完成各個golang框架生命週期的解析之後,我會計劃對這幾個框架的優略進行一個系列分析,由於業內大多都是性能分析的比較多,我可能會更側重於以下維度:

  • 框架設計
  • 路由算法

第一波我們主要把重點放在框架設計上面。


安裝

上次閱讀iris我們使用的glide安裝的,今天我們安裝gin嘗試下使用gomod,具體步驟如下。

使用go mod安裝:

// 初始化go.mod文件
go mod init gin-code-read
// 安裝gin
go get github.com/gin-gonic/gin
// 複製依賴到vendor目錄
go mod vendor

啓動一個簡單的gin http服務:

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()
}

看上面的啓動代碼是不是很熟悉,和iris很像是吧,同樣的Default方法。


gin的生命週期

看完gin框架流程我有大致如下幾個感觸:

  • gin是我目前看過的這三個go框架裏最簡潔的框架
  • gin和iris在框架設計存在風格一致的地方,例如註冊中間件、handle的執行

總之,目前就一個感受:

Gin是我認爲的一個GO框架應該有的樣子

下圖就是我對整個Gin框架生命週期的輸出,由於圖片過大存在平臺壓縮的可能,建議大家直接查看原圖鏈接。

http://cdn.tigerb.cn/20190704211526.png

訪問圖片源地址查看大圖 http://cdn.tigerb.cn/20190704...

原圖查看鏈接: http://cdn.tigerb.cn/20190704...


關鍵代碼解析

// 獲取一個gin框架實例
gin.Default()
⬇️
// 具體的Default方法
func Default() *Engine {
    // 調試模式日誌輸出 
    // 🌟很不錯的設計
    debugPrintWARNINGDefault()
    // 創建一個gin框架實例
    engine := New()
    // 是不是很眼熟 和iris裏註冊中間件的方式一致
    // 不過比iris好的是支持多參數 iris則是得調用多次
    engine.Use(Logger(), Recovery())
    return engine
}
⬇️
// 創建一個gin框架實例 具體方法
func New() *Engine {
    // 調試模式日誌輸出 
    debugPrintWARNINGNew()
    // 先插入一個小話題,可能好多人都在想爲什麼叫gin呢?
    // 哈哈,這個框架實例的結構體實際命名的Engine, 很明顯gin就是一個很個性的簡稱了,是不是真相大白了。
    // 初始化一個Engine實例
    engine := &Engine{
        // 路由組
        // 給框架實例綁定上一個路由組
        RouterGroup: RouterGroup{
            // engine.Use 註冊的中間方法到這裏
            Handlers: nil,
            basePath: "/",
            // 是否是路由根節點
            root:     true,
        },
        FuncMap:                template.FuncMap{},
        RedirectTrailingSlash:  true,
        RedirectFixedPath:      false,
        HandleMethodNotAllowed: false,
        ForwardedByClientIP:    true,
        AppEngine:              defaultAppEngine,
        UseRawPath:             false,
        UnescapePathValues:     true,
        MaxMultipartMemory:     defaultMultipartMemory,
        // 路由樹
        // 我們的路由最終註冊到了這裏
        trees:                  make(methodTrees, 0, 9),
        delims:                 render.Delims{Left: "{{", Right: "}}"},
        secureJsonPrefix:       "while(1);",
    }
    // RouterGroup綁定engine自身的實例
    // 不太明白爲何如此設計
    // 職責分明麼?
    engine.RouterGroup.engine = engine
    // 綁定從實例池獲取上下文的閉包方法
    engine.pool.New = func() interface{} {
        // 獲取一個Context實例
        return engine.allocateContext()
    }
    // 返回框架實例
    return engine
}
⬇️
// 註冊日誌&goroutin panic捕獲中間件
engine.Use(Logger(), Recovery())
⬇️
// 具體的註冊中間件的方法
func (engine *Engine) Use(middleware ...HandlerFunc) IRoutes {
    engine.RouterGroup.Use(middleware...)
    engine.rebuild404Handlers()
    engine.rebuild405Handlers()
    return engine
}

// 上面 是一個engine框架實例初始化的關鍵代碼
// 我們基本看完了
// --------------router--------------
// 接下來 開始看路由註冊部分

// 註冊GET請求路由
func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes {
    // 往路由組內 註冊GET請求路由
    return group.handle("GET", relativePath, handlers)
}
⬇️
func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes {
    absolutePath := group.calculateAbsolutePath(relativePath)
    // 把中間件的handle和該路由的handle合併
    handlers = group.combineHandlers(handlers)
    // 註冊一個GET集合的路由
    group.engine.addRoute(httpMethod, absolutePath, handlers)
    return group.returnObj()
}
⬇️
func (engine *Engine) addRoute(method, path string, handlers HandlersChain) {
    assert1(path[0] == '/', "path must begin with '/'")
    assert1(method != "", "HTTP method can not be empty")
    assert1(len(handlers) > 0, "there must be at least one handler")

    debugPrintRoute(method, path, handlers)
    // 檢查有沒有對應method集合的路由
    root := engine.trees.get(method)
    if root == nil {
        // 沒有 創建一個新的路由節點
        root = new(node)
        // 添加該method的路由tree到當前的路由到路由樹裏
        engine.trees = append(engine.trees, methodTree{method: method, root: root})
    }
    // 添加路由
    root.addRoute(path, handlers)
}
⬇️
// 很關鍵
// 路由樹節點
type node struct {
    // 路由path
    path      string
    indices   string
    // 子路由節點
    children  []*node
    // 所有的handle 構成一個鏈
    handlers  HandlersChain
    priority  uint32
    nType     nodeType
    maxParams uint8
    wildChild bool
}

// 上面 
// 我們基本看完了
// --------------http server--------------
// 接下來 開始看gin如何啓動的http server

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方法 啓動路由
    // engine實現了http.Handler接口 所以在這裏作爲參數傳參進去
    // 後面我們再看engine.ServeHTTP的具體邏輯
    err = http.ListenAndServe(address, engine)
    return
}
⬇️
// engine自身就實現了Handler接口
type Handler interface {
    ServeHTTP(ResponseWriter, *Request)
}
⬇️
// 下面就是網絡相關了
// 監聽IP+端口
ln, err := net.Listen("tcp", addr)
⬇️
// 上面執行完了監聽
// 接着就是Serve
srv.Serve(tcpKeepAliveListener{ln.(*net.TCPListener)})
⬇️
// Accept請求
rw, e := l.Accept()
⬇️
// 使用goroutine去處理一個請求
// 最終就執行的是engine的ServeHTTP方法
go c.serve(ctx)

// 上面服務已經啓動起來了
// --------------handle request--------------
// 接着我們來看看engine的ServeHTTP方法的具體內容
// engine實現http.Handler接口ServeHTTP的具體方法
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    // 獲取一個上下文實例
    // 從實例池獲取 性能高
    c := engine.pool.Get().(*Context)
    // 重置獲取到的上下文實例的http.ResponseWriter
    c.writermem.reset(w)
    // 重置獲取到的上下文實例*http.Request
    c.Request = req
    // 重置獲取到的上下文實例的其他屬性
    c.reset()

    // 實際處理請求的地方
    // 傳遞當前的上下文
    engine.handleHTTPRequest(c)

    //歸還上下文實例
    engine.pool.Put(c)
}
⬇️
// 具體執行路由的方法
engine.handleHTTPRequest(c)
⬇️
t := engine.trees
for i, tl := 0, len(t); i < tl; i++ {
    // 這裏尋找當前請求method的路由樹節點
    // 我在想這裏爲啥不用map呢?
    // 雖說也遍歷不了幾次
    if t[i].method != httpMethod {
        continue
    }
    // 找到節點
    root := t[i].root
    // 很關鍵的地方
    // 尋找當前請求的路由
    handlers, params, tsr := root.getValue(path, c.Params, unescape)
    if handlers != nil {
        // 把找到的handles賦值給上下文
        c.handlers = handlers
        // 把找到的入參賦值給上下文
        c.Params = params
        // 執行handle
        c.Next()
        // 處理響應內容
        c.writermem.WriteHeaderNow()
        return
    }
    ...
}

// 方法樹結構體
type methodTree struct {
    // HTTP Method
    method string
    // 當前HTTP Method的路由節點
    root   *node
}

// 方法樹集合
type methodTrees []methodTree
⬇️
// 執行handle
func (c *Context) Next() {
    // 上下文處理之後c.index被執爲-1
    c.index++
    for s := int8(len(c.handlers)); c.index < s; c.index++ {
        // 遍歷執行所有handle(其實就是中間件+路由handle)
        // 首先感覺這裏的設計又是似曾相識 iris不是也是這樣麼 不懂了 哈哈
        // 其次感覺這裏設計的很一般 遍歷?多無聊,這裏多麼適合「責任鏈模式」
        // 之後給大家帶來關於這個handle執行的「責任鏈模式」的設計
        c.handlers[c.index](c)
    }
}

// Context的重置方法
func (c *Context) reset() {
    c.Writer = &c.writermem
    c.Params = c.Params[0:0]
    c.handlers = nil
    // 很關鍵 注意這裏是-1哦
    c.index = -1
    c.Keys = nil
    c.Errors = c.Errors[0:0]
    c.Accepted = nil
}

結語

最後我們再簡單的回顧下上面的流程,從上圖看來,是不是相對於iris簡單了好多。

http://cdn.tigerb.cn/20190706222632.png


《Golang框架解析》系列文章鏈接如下:

clipboard.png

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章