gin 源碼閱讀(一)-- 啓動

文章首發於同名公衆號,歡迎關注~
由於 gin 的安裝教程已經處處都有了,因此這裏省略如何安裝, 建議直接去 github 官方地址的 README 中瀏覽安裝步驟,順便了解 gin 框架的功能。 https://github.com/gin-gonic/gin

最簡單的代碼

package main

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

func main() {
    r := gin.Default()
    
    r.Run() // 監聽並在 0.0.0.0:8080 上啓動服務
}

上面就是使用了 gin 框架的最簡單的 demo,接下來經過這個 demo 去一步步閱讀分析 gin 是如何啓動的。git

gin.Run()

這裏是服務器啓動的地方,進去看看這個函數有什麼奧祕:github

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
}

除去第一行和第四行打印,是否是有點像 go 原生的 http 庫?來看看 http 庫的 web 服務簡單 demoweb

package main

import (
    "log"
    "net/http"
)

func main() {
    http.HandleFunc("/", indexHandler)

    log.Fatal(http.ListenAndServe(":8080", nil))
}

能夠看到 gin 框架啓動的時候,也是基於 http.ListenAndServe(addr string, handler Handler) 這個方法的,與原生的 http 庫的區別就是,ListenAndServe(addr string, handler Handler) 這個方法的第二個參數,傳入的是 engine,咱們繼續來看看 ListenAndServe 方法的具體代碼:服務器

func ListenAndServe(addr string, handler Handler) error {
    server := &Server{Addr: addr, Handler: handler}
    return server.ListenAndServe()
}

server.ListenAndServe() 已是服務監聽並啓動的方法,因此關鍵點在 Handler 這裏:框架

// A Handler responds to an HTTP request.
// ...
type Handler interface {
    ServeHTTP(ResponseWriter, *Request)
}

這是 http 包裏的源碼註釋,其實寫得很詳細了,註釋不少,我直接省略了,感興趣的能夠去閱讀一下,不出意外,這個 Handler 是一個接口,從 gin.engine 的傳入已經能夠看出來了吧?註釋的意思是這個接口是處理程序響應HTTP請求,能夠理解爲,實現了這個接口,就能夠接收全部的請求並進行處理。函數

所以 gin 框架的入口就是從這裏開始,gin engine 實現了 ServeHTTP,而後接管全部的請求走 gin 框架的處理方式。細心的讀者應該能發現,原生的 http web 服務傳入了 nil,事實上是 http 庫也有一個默認的請求處理器,感興趣的讀者能夠去仔細閱讀研究一下 go 官方團隊的請求處理器實現哦~學習

那咱們繼續來看下 gin 框架是怎麼實現這個 ServeHTTP 方法的:測試

// gin.go
// ServeHTTP conforms to the http.Handler interface.
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    c := engine.pool.Get().(*Context) // 從池裏獲取資源處理請求
    c.writermem.reset(w) // 重置資源的 ResponseWriter
    c.Request = req // 賦值請求給 context
    c.reset()

    engine.handleHTTPRequest(c) // 將資源信息給到 handler 進一步處理

    engine.pool.Put(c) // 請求處理結束,將資源放回池子
}

能夠看到實現並不難,代碼的含義我已經寫上了註釋,這裏的重點是 Context,但不是這一篇文章的重點,我放在這一系列的後面進行講解。url

咱們只須要知道,gin 也是實現了 Handler 接口,因此能夠將請求按 gin 的處理方式進行處理,也就是咱們能夠使用 gin 來作 web 服務框架的起點。spa

gin.Default()

接下來進去 Default 函數去看具體實現,代碼以下:

// gin.go
// Default returns an Engine instance with the Logger and Recovery middleware already attached.
func Default() *Engine {
    debugPrintWARNINGDefault() // 忽略
    engine := New()
    engine.Use(Logger(), Recovery()) // 暫時忽略
    return engine
}

其實單單看函數名,已經知道這是構造默認的 gin engine,能夠看到 engine 是經過 New() 方法獲得的,咱們選忽略第一行和第三行。

// gin.go
func New() *Engine {
    debugPrintWARNINGNew() // 忽略
    engine := &Engine{
        RouterGroup: RouterGroup{ // 路由組,後面再分析
            Handlers: nil,
            basePath: "/",
            root:     true,
        },
        FuncMap:                template.FuncMap{},
        RedirectTrailingSlash:  true,
        RedirectFixedPath:      false,
        HandleMethodNotAllowed: false,
        ForwardedByClientIP:    true,
        AppEngine:              defaultAppEngine, // bool,是否爲默認處理 engine
        UseRawPath:             false,
        UnescapePathValues:     true,
        MaxMultipartMemory:     defaultMultipartMemory,
        trees:                  make(methodTrees, 0, 9),
        delims:                 render.Delims{Left: "{{", Right: "}}"},
        secureJsonPrefix:       "while(1);",
    }
    engine.RouterGroup.engine = engine  // 重點,將 engine 從新賦值給路由組對象中的 engine
    engine.pool.New = func() interface{} { // 重點,資源池
        return engine.allocateContext()
    }
    return engine
}

到這裏,咱們不用看 engine 結構的具體定義,也已經看出來比較多的信息了,主要用路由組和資源池,這兩個均可以分別展開寫一篇文章,因爲篇幅有限,這裏就先不介紹了。

值得注意的是這裏面有兩個地方很巧妙:

engine.RouterGroup.engine = engine

很明顯,路由組 RouterGroup 中還有個 engine 的指針對象,爲何要這麼設計呢?讀者們能夠思考一下。

engine.pool.New = func() interface{} { // 對象池
        return engine.allocateContext()
    }

看下 engine.allocateContext()方法:

func (engine *Engine) allocateContext() *Context {
    return &Context{engine: engine}
}

能夠看到 engine 中包含了 pool 對象池,這個對象池是對 gin.Context 的重用,進一步減小開銷,關於 sync.pool 對象池我就不在這裏細說了, 後續再更新關於 sync.pool 的文章。

engine

最後再來看看 engine 的結構:

type Engine struct {
    // 路由組
    RouterGroup

    // 若是true,當前路由匹配失敗但將路徑最後的 / 去掉時匹配成功時自動匹配後者
    // 好比:請求是 /foo/ 但沒有命中,而存在 /foo,
    // 對get method請求,客戶端會被301重定向到 /foo
    // 對於其餘method請求,客戶端會被307重定向到 /foo
    RedirectTrailingSlash bool

    // 若是true,在沒有處理者被註冊來處理當前請求時router將嘗試修復當前請求路徑
    // 邏輯爲:
    // - 移除前面的 ../ 或者 //
    // - 對新的路徑進行大小寫不敏感的查詢
    // 若是找到了處理者,請求會被301或307重定向
    // 好比: /FOO 和 /..//FOO 會被重定向到 /foo
    // RedirectTrailingSlash 參數和這個參數獨立
    RedirectFixedPath bool

    // 若是true,當路由沒有被命中時,去檢查是否有其餘method命中
    //  若是命中,響應405 (Method Not Allowed)
    //  若是沒有命中,請求將由 NotFound handler 來處理
    HandleMethodNotAllowed bool
    ForwardedByClientIP    bool

    // #726 #755 If enabled, it will thrust some headers starting with
    // 'X-AppEngine...' for better integration with that PaaS.
    AppEngine bool

    // 若是true, url.RawPath 會被用來查找參數
    UseRawPath bool

    // 若是true, path value 會被保留
    // 若是 UseRawPath是false(默認),UnescapePathValues爲true
    // url.Path會被保留並使用
    UnescapePathValues bool

    // Value of 'maxMemory' param that is given to http.Request's ParseMultipartForm
    // method call.
    MaxMultipartMemory int64

    delims           render.Delims
    secureJsonPrefix string
    HTMLRender       render.HTMLRender
    FuncMap          template.FuncMap
    allNoRoute       HandlersChain
    allNoMethod      HandlersChain
    noRoute          HandlersChain
    noMethod         HandlersChain
    pool             sync.Pool
    
    //每一個http method對應一棵樹
    trees            methodTrees
}

上面提到過 ServeHTTP 這個方法,其中engine.handleHTTPRequest(c)這行代碼就是具體的處理操做。

能夠看到 engine 的結構中有這麼一個字段 RedirectTrailingSlash ,在 Default() 初始化方法中爲 true,我對此比較感興趣,你們也能夠根據註釋的意思來測試一下,最終會走到下面的代碼中:

func (engine *Engine) handleHTTPRequest(c *Context) {
    ……
    if httpMethod != "CONNECT" && rPath != "/" {
        if value.tsr && engine.RedirectTrailingSlash {
            // 這裏就是嘗試糾正請求路徑的函數
            redirectTrailingSlash(c)
            return
        }
        if engine.RedirectFixedPath && redirectFixedPath(c, root, engine.RedirectFixedPath) {
            return
        }
    }
    ……
}

總結一下

image-20200322204206968.png

上圖就是本文的核心了,能夠結合圖來理解一下 gin 啓動的過程及設計,下一篇會將 gin 的路由,敬請期待~

本系列 「拆輪子系列:gin 框架」 的第一篇就到這裏了,這麼通讀下來,發現 gin 框架的設計和實現真的太棒了,簡潔清晰,又不失巧妙,很適合你們也去閱讀學習一下,牆裂推薦!!!

相關文章
相關標籤/搜索