Go Gin原始碼學習(一)

panlei914發表於2019-05-08

Gin的基本使用

Gin是一個比較輕量級的http框架,主要是提供了幾個便於使用的功能:

  • 簡單的中介軟體註冊,可以很方便的實現通用中介軟體的使用註冊
  • 提供了比較方便和全面的路由註冊,方便的實現RESTful介面的實現
  • 提供了便捷的獲取引數的方法,包括get、post兵可以可以把資料直接轉換成物件
  • 對路由的分組,Gin可以對一組路由做統一的中介軟體註冊等操作
  • 可以手機所有錯誤,統一在統一的地方寫日誌

效能方面:

  • 是路由的基礎資料格式為基數樹沒有使用反射,所以效能方面也是比較低消耗記憶體低
  • 上下文context使用了物件池,fasthttp中也同樣使用了sync.pool

使用方便也是比較簡單的,下面有一個很簡單的例子

package main

import (
    "fmt"
    "github.com/gin-gonic/gin"
    "test/gin/middleware/model"
)

func main() {
    //建立router
    router := gin.Default()
    //建立組
    group := router.Group("/api")
    //為組加中介軟體
    group.Use(func(context *gin.Context) {
        fmt.Println("api group url:", context.Request.URL.String())
    })
    //為組加路由方法
    group.GET("/test", func(context *gin.Context) {
        context.JSON(200, model.Message{Message:"ok"})
    })
    //執行
    router.Run(":3333")
}

例子中是一個最簡單的Gin框架的應用。建立了一個engine,建立了組並且為組新增了中介軟體之後在這個group下的路由方法都將使用這個中介軟體,方便對api最系統的管理對不同的api做不同的處理。 在Terminal中訪問可以看到下面的結果

curl http://localhost:3333/api/test
{"Message":"ok"}panleiMacBook-Pro:test 

主要流程的原始碼

首先我們先看例子中的gin.Default() 返回的engine物件,這是Gin的主要物件。只對最主要的屬性加了註釋

type Engine struct {
    //路由組
    RouterGroup
    RedirectTrailingSlash bool
    RedirectFixedPath bool
    HandleMethodNotAllowed bool
    ForwardedByClientIP    bool
    AppEngine bool
    UseRawPath bool
    UnescapePathValues bool
    MaxMultipartMemory int64
    delims           render.Delims
    secureJsonPrefix string
    HTMLRender       render.HTMLRender
    FuncMap          template.FuncMap
    allNoRoute       HandlersChain
    allNoMethod      HandlersChain
    noRoute          HandlersChain
    noMethod         HandlersChain
    // 物件池 用來建立上下文context
    pool             sync.Pool
    //記錄路由方法的 比如GET POST 都會是陣列中的一個 每個方法對應一個基數樹的一個root的node
    trees            methodTrees
}

然後我們來看Default方法,其實很簡單就是建立一個engine物件並且新增預設的兩個中介軟體,一個是做log顯示,顯示每次請求可以再console中看到。每次建立engine物件的時候回預設的新增一個routergroup地址為預設的"/" 程式碼如下:

func Default() *Engine {
    debugPrintWARNINGDefault()
    engine := New()
    engine.Use(Logger(), Recovery())
    return engine
}
//new方法中預設的 新增了routergroup路由 “/”
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,
        UseRawPath:             false,
        UnescapePathValues:     true,
        MaxMultipartMemory:     defaultMultipartMemory,
        trees:                  make(methodTrees, 0, 9),
        delims:                 render.Delims{Left: "{{", Right: "}}"},
        secureJsonPrefix:       "while(1);",
    }
    engine.RouterGroup.engine = engine
    engine.pool.New = func() interface{} {
        return engine.allocateContext()
    }
    return engine
}

在看中介軟體之前還有一個重要的物件context 上下文物件 這個物件中存放了 engine指標、請求的request物件、返回的responsewriter物件還有一些引數等物件,這個context將在請求一開始就被建立一直貫穿整個執行過程,包括中介軟體,路由等。最後的返回值可以再responseWriter寫,最終就會返回給客戶端。

type Context struct {
    writermem responseWriter
    Request   *http.Request
    Writer    ResponseWriter

    Params   Params
    handlers HandlersChain
    index    int8

    engine *Engine

    // Keys is a key/value pair exclusively for the context of each request.
    Keys map[string]interface{}

    // Errors is a list of errors attached to all the handlers/middlewares who used this context.
    Errors errorMsgs

    // Accepted defines a list of manually accepted formats for content negotiation.
    Accepted []string
}

接下來看得是新增中介軟體use方法

func (engine *Engine) Use(middleware ...HandlerFunc) IRoutes {
    //呼叫routegroup的use方法
    engine.RouterGroup.Use(middleware...)
    engine.rebuild404Handlers()
    engine.rebuild405Handlers()
    return engine
}

func (group *RouterGroup) Use(middleware ...HandlerFunc) IRoutes {
    //為group的handlers新增中介軟體 
    group.Handlers = append(group.Handlers, middleware...)
    return group.returnObj()
}

新增為中介軟體之後就是新增正常路由,Gin中提供了GET、POST、DELETE更各種方法,我們就只看get方法其餘的都是相同的處理方式。總結下來就是把group和傳入的handler合併,並且計算出路徑存入到tree中等客戶端的呼叫。

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 {
    //計算路徑地址,比如group地址是 router.Group("/api")
    //結果為/api/test/ 就是最終計算出來的結果 使用path.join 方法拼接 其中加了一些判斷
    absolutePath := group.calculateAbsolutePath(relativePath)
    //把group中的handler和傳入的handler合併 
    handlers = group.combineHandlers(handlers)
    //把方法 路徑 和處理方法作為node 加入到基數樹種,基數樹在下次單獨學習分析
    group.engine.addRoute(httpMethod, absolutePath, handlers)
    return group.returnObj()
}

func (group *RouterGroup) combineHandlers(handlers HandlersChain) HandlersChain {
    finalSize := len(group.Handlers) + len(handlers)
    if finalSize >= int(abortIndex) {
        panic("too many handlers")
    }
    mergedHandlers := make(HandlersChain, finalSize)
    copy(mergedHandlers, group.Handlers)
    copy(mergedHandlers[len(group.Handlers):], handlers)
    return mergedHandlers
}

最後我們需要看run方法,之前的都是準備工作或者說是設定路由中介軟體。等run方法執行的時候則是服務真正啟動起來。 程式碼很簡單,幾乎不需要註釋是呼叫gohttp包的ListenAndServe方法把engine傳入然後http包中會有一個for邏輯不停的監聽這個埠號的所有請求。

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
}

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

那麼客戶端最終請求之後會走到那些程式碼,又是怎麼找到路由並且呼叫一個個中介軟體的呢?其實engine是繼承了Handler 這個介面(可以看下面程式碼),我們知道如果不適用Gin框架直接使用http包我們所有的路由就是直接繼承這個介面所以對這個介面我們是很熟悉的。 下面的serveHTTP 就是Gin的方法 最主要流程就是從tree中獲取到路由,然後依次執行handler最終處理完成返回給客戶端。

type Handler interface {
    ServeHTTP(ResponseWriter, *Request)
}

func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    //從物件池中獲取context物件,這就是最初我們看到的一種優化效能的一種方式
    c := engine.pool.Get().(*Context)
    //重置writer中的一些值
    c.writermem.reset(w)
    把request放到context中
    c.Request = req
    c.reset()
    //呼叫處理方法
    engine.handleHTTPRequest(c)
    //處理完成把context物件放回到物件池
    engine.pool.Put(c)
}

func (engine *Engine) handleHTTPRequest(c *Context) {
    //獲取請求方法和路徑
    httpMethod := c.Request.Method
    path := c.Request.URL.Path
    unescape := false
    if engine.UseRawPath && len(c.Request.URL.RawPath) > 0 {
        path = c.Request.URL.RawPath
        unescape = engine.UnescapePathValues
    }

    //根據基數樹的特性尋找方法發 並呼叫next方法 依次執行handler
    t := engine.trees
    for i, tl := 0, len(t); i < tl; i++ {
        if t[i].method != httpMethod {
            continue
        }
        root := t[i].root
        // Find route in tree
        handlers, params, tsr := root.getValue(path, c.Params, unescape)
        if handlers != nil {
            c.handlers = handlers
            c.Params = params
            c.Next()
            c.writermem.WriteHeaderNow()
            return
        }
        if httpMethod != "CONNECT" && path != "/" {
            if tsr && engine.RedirectTrailingSlash {
                redirectTrailingSlash(c)
                return
            }
            if engine.RedirectFixedPath && redirectFixedPath(c, root, engine.RedirectFixedPath) {
                return
            }
        }
        break
    }

    if engine.HandleMethodNotAllowed {
        for _, tree := range engine.trees {
            if tree.method == httpMethod {
                continue
            }
            if handlers, _, _ := tree.root.getValue(path, nil, unescape); handlers != nil {
                c.handlers = engine.allNoMethod
                serveError(c, http.StatusMethodNotAllowed, default405Body)
                return
            }
        }
    }
    c.handlers = engine.allNoRoute
    serveError(c, http.StatusNotFound, default404Body)
}

總結

從上面的流程我們可以看到,其實Gin框架就是對go http包的一次封裝,加入了group和tree。讓我們可以更簡單方便的使用http包。其實Gin還有很多其他功能的原始碼包括引數分析 json解析等等,這次學習的只是Gin的主要流程。Gin中tree是一個亮點,是的它在查詢路由效能方面有很大的優勢,下一篇文章會主要學習tree(基數樹)。

相關文章