高性能 Gin 框架原理學習教程

作者:jiayan

工作中的部分項目使用到了 gin 框架,因此從源碼層面學習一下其原理。

1. 概述

Gin 是一款高性能的 Go 語言 Web 框架,Gin 的一些特性:

2. 源碼學習

下面從一個簡單的包含基礎路由和路由組路由的 demo 開始分析:

func main() {
 // 初始化
 mux := gin.Default()

 // 設置全局通用handlers,這裏是設置了engine的匿名成員RouterGroup的Handlers成員
 mux.Handlers = []gin.HandlerFunc{
  func(c *gin.Context) {
   log.Println("log 1")
   c.Next()
  },
  func(c *gin.Context) {
   log.Println("log 2")
   c.Next()
  },
 }

 // 綁定/ping 處理函數
 mux.GET("/ping", func(c *gin.Context) {
  c.String(http.StatusOK, "ping")
 })
 mux.GET("/pong", func(c *gin.Context) {
  c.String(http.StatusOK, "pong")
 })
 mux.GET("/ping/hello", func(c *gin.Context) {
  c.String(http.StatusOK, "ping hello")
 })

 mux.GET("/about", func(c *gin.Context) {
  c.String(http.StatusOK, "about")
 })

 // system組
 system := mux.Group("system")
 // system->auth組
 systemAuth := system.Group("auth")
 {
  // 獲取管理員列表
  systemAuth.GET("/addRole", func(c *gin.Context) {
   c.String(http.StatusOK, "system/auth/addRole")
  })
  // 添加管理員
  systemAuth.GET("/removeRole", func(c *gin.Context) {
   c.String(http.StatusOK, "system/auth/removeRole")
  })
 }
 // user組
 user := mux.Group("user")
 // user->auth組
 userAuth := user.Group("auth")
 {
  // 登陸
  userAuth.GET("/login", func(c *gin.Context) {
   c.String(http.StatusOK, "user/auth/login")
  })
  // 註冊
  userAuth.GET("/register", func(c *gin.Context) {
   c.String(http.StatusOK, "user/auth/register")
  })
 }

 mux.Run("0.0.0.0:8080")
}

2.1 初始化 Gin(Default 函數)

初始化步驟主要是初始化 engine 與加載兩個默認的中間件:

func Default(opts ...OptionFunc) *Engine {
    debugPrintWARNINGDefault()
 // 初始化engine實例
    engine := New()
 // 默認加載log & recovery中間件
    engine.Use(Logger(), Recovery())
    return engine.With(opts...)
}
2.1.1 初始化 engine

engine 是 gin 中的核心對象,gin 通過 Engine 對象來定義服務路由信息、組裝插件、運行服務,是框架的核心發動機,整個 Web 服務的都是由它來驅動的 關鍵字段:

func New(opts ...OptionFunc) *Engine {
    debugPrintWARNINGNew()
    engine := &Engine{
       RouterGroup: RouterGroup{
          Handlers: nil,
      // 默認的basePath爲/,綁定路由時會用到此參數來計算絕對路徑
          basePath: "/",
          root:     true,
       },
       FuncMap:                template.FuncMap{},
       RedirectTrailingSlash:  true,
       RedirectFixedPath:      false,
       HandleMethodNotAllowed: false,
       ForwardedByClientIP:    true,
       RemoteIPHeaders:        []string{"X-Forwarded-For""X-Real-IP"},
       TrustedPlatform:        defaultPlatform,
       UseRawPath:             false,
       RemoveExtraSlash:       false,
       UnescapePathValues:     true,
       MaxMultipartMemory:     defaultMultipartMemory,
       trees:                  make(methodTrees, 0, 9),
       delims:                 render.Delims{Left: "{{", Right: "}}"},
       secureJSONPrefix:       "while(1);",
       trustedProxies:         []string{"0.0.0.0/0""::/0"},
       trustedCIDRs:           defaultTrustedCIDRs,
    }
    engine.RouterGroup.engine = engine
 // 池化gin核心context對象,有新請求來時會使用到該池
    engine.pool.New = func() any {
       return engine.allocateContext(engine.maxParams)
    }
    return engine.With(opts...)
}
2.1.2 初始化中間件
Engine.Use 函數

Engine.Use 函數用於將中間件添加到當前的路由上,位於 gin.go 中,代碼如下:

// Use attaches a global middleware to the router. ie. the middleware attached though Use() will be
// included in the handlers chain for every single request. Even 404, 405, static files...
// For example, this is the right place for a logger or error management middleware.
func (engine *Engine) Use(middleware ...HandlerFunc) IRoutes {
 engine.RouterGroup.Use(middleware...)
 engine.rebuild404Handlers()
 engine.rebuild405Handlers()
 return engine
}
RouterGroup.Use 函數

實際上,還需要進一步調用engine.RouterGroup.Use(middleware...)完成實際的中間件註冊工作,函數位於 gin.go 中,代碼如下:

// Use adds middleware to the group, see example code in GitHub.
func (group *RouterGroup) Use(middleware ...HandlerFunc) IRoutes {
 group.Handlers = append(group.Handlers, middleware...)
 return group.returnObj()
}

實際上就是把中間件 (本質是一個函數)添加到 HandlersChain 類型(實質上爲數組type HandlersChain []HandlerFunc)的 group.Handlers 中。

HandlerFunc
type HandlerFunc func(*Context)

如果需要實現一箇中間件,那麼需要實現該類型,函數參數只有 * Context。

gin.Context
  1. 貫穿一個 http 請求的所有流程,包含全部上下文信息。

  2. 提供了很多內置的數據綁定和響應形式,JSON、HTML、Protobuf 、MsgPack、Yaml 等,它會爲每一種形式都單獨定製一個渲染器

  3. engine 的 ServeHTTP 函數,在響應一個用戶的請求時,都會先從臨時對象池中取一個 context 對象。使用完之後再放回臨時對象池。爲了保證併發安全,如果在一次請求新起一個協程,那麼一定要 copy 這個 context 進行參數傳遞。

type Context struct {
    writermem responseWriter
    Request   *http.Request  // 請求對象
    Writer    ResponseWriter // 響應對象
    Params   Params  // URL 匹配參數
    handlers HandlersChain // // 請求處理鏈
    ...
}

2.2 綁定處理函數到對應的 HttpMethod 上

2.2.1 普通路由實現

調用綁定函數:

mux.GET("/ping", func(c *gin.Context) {
    c.String(http.StatusOK, "ping")
})

函數實際上走到了 engine 對象的匿名成員 RouterGroup 的 handle 函數中

// POST is a shortcut for router.Handle("POST", path, handlers). 
func (group *RouterGroup) POST(relativePath string, handlers ...HandlerFunc) IRoutes {
    return group.handle(http.MethodPost, relativePath, handlers)
}

// GET is a shortcut for router.Handle("GET", path, handlers). 
func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes {
    return group.handle(http.MethodGet, relativePath, handlers)
}

// DELETE is a shortcut for router.Handle("DELETE", path, handlers). 
func (group *RouterGroup) DELETE(relativePath string, handlers ...HandlerFunc) IRoutes {
    return group.handle(http.MethodDelete, relativePath, handlers)
}

// PATCH is a shortcut for router.Handle("PATCH", path, handlers). 
func (group *RouterGroup) PATCH(relativePath string, handlers ...HandlerFunc) IRoutes {
    return group.handle(http.MethodPatch, relativePath, handlers)
}

// PUT is a shortcut for router.Handle("PUT", path, handlers). 
func (group *RouterGroup) PUT(relativePath string, handlers ...HandlerFunc) IRoutes {
    return group.handle(http.MethodPut, relativePath, handlers)
}

綁定邏輯:

func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes {
    // 使用相對路徑與路由組basePath 計算絕對路徑
    absolutePath := group.calculateAbsolutePath(relativePath)
 // 將函數參數中的 "處理函數" handlers與本路由組已有的Handlers組合起來,作爲最終要執行的完整handlers列表
    handlers = group.combineHandlers(handlers)
 // routerGroup會存有engine對象的引用,調用engine的addRoute將絕對路徑與處理函數列表綁定起來
    group.engine.addRoute(httpMethod, absolutePath, handlers)
    return group.returnObj()
}

從源碼中 handlers = group.combineHandlers(handlers) 可以看出我們也可以給 gin 設置一些全局通用的 handlers,這些 handlers 會綁定到所有的路由方法上,如下:

// 設置全局通用handlers,這裏是設置了engine的匿名成員RouterGroup的Handlers成員
mux.Handlers = []gin.HandlerFunc{
    func(c *gin.Context) {
       log.Println("log 1")
       c.Next()
    },
    func(c *gin.Context) {
       log.Println("log 2")
       c.Next()
    },
}

addRoute 函數:

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)
    
 // 每個HTTP方法(如:GET,POST)的路由信息都各自由一個樹結構來維護,該樹結構的模型與函數實現位於gin/tree.go中,此處不再繼續展開。不同http方法的樹根節點組成了 engine.trees 這個數組
 // 從engine的路由樹數組中遍歷找到該http方法對應的路由樹的根節點
    root := engine.trees.get(method)
    if root == nil {
    // 如果根節點不存在,那麼新建根節點
       root = new(node)
       root.fullPath = "/"
    // 將根節點添加到路由樹數組中
       engine.trees = append(engine.trees, methodTree{method: method, root: root})
 }
 // 調用根節點的addRoute函數,將絕對路徑與處理函數鏈綁定起來
    root.addRoute(path, handlers)
 
 ...
}
// 路由樹數組數據結構
type methodTree struct {
    method string     root   *node 
}

type methodTrees []methodTree
2.2.2 路由組的實現
// system組
system := mux.Group("system")
// system->auth組
systemAuth := system.Group("auth")
{
 // 獲取管理員列表
 systemAuth.GET("/addRole", func(c *gin.Context) {
  c.String(http.StatusOK, "system/auth/addRole")
 })
 // 添加管理員
 systemAuth.GET("/removeRole", func(c *gin.Context) {
  c.String(http.StatusOK, "system/auth/removeRole")
 })
}
// user組
user := mux.Group("user")
// user->auth組
userAuth := user.Group("auth")
{
 // 登陸
 userAuth.GET("/login", func(c *gin.Context) {
  c.String(http.StatusOK, "user/auth/login")
 })
 // 註冊
 userAuth.GET("/register", func(c *gin.Context) {
  c.String(http.StatusOK, "user/auth/register")
 })
}

Group 函數會返回一個新的 RouterGroup 對象,每一個 RouterGroup 都會基於原有的 RouterGroup 而生成:

2.3. 啓動 Gin

2.3.1 Engine.Run 函數
// Run attaches the router to a http.Server and starts listening and serving HTTP requests. // It is a shortcut for http.ListenAndServe(addr, router) // Note: this method will block the calling goroutine indefinitely unless an error happens. 
func (engine *Engine) Run(addr ...string) (err error) {
 ...
    address := resolveAddress(addr)
    debugPrint("Listening and serving HTTP on %s\n", address)
    err = http.ListenAndServe(address, engine.Handler())
    return 
}

可以看到,最核心的監聽與服務實質上是調用 Go 語言內置庫 net/http 的http.ListenAndServe函數實現的。

2.3.2 net/http 的 ListenAndServe 函數
// ListenAndServe listens on the TCP network address addr and then calls
// Serve with handler to handle requests on incoming connections.
// Accepted connections are configured to enable TCP keep-alives.
//
// The handler is typically nil, in which case the DefaultServeMux is used.
//
// ListenAndServe always returns a non-nil error.
func ListenAndServe(addr string, handler Handler) error {
 server := &Server{Addr: addr, Handler: handler}
 return server.ListenAndServe()
}


// ListenAndServe listens on the TCP network address srv.Addr and then
// calls Serve to handle requests on incoming connections.
// Accepted connections are configured to enable TCP keep-alives.
//
// If srv.Addr is blank, ":http" is used.
//
// ListenAndServe always returns a non-nil error. After Shutdown or Close,
// the returned error is ErrServerClosed.
func (srv *Server) ListenAndServe() error {
    if srv.shuttingDown() {
       return ErrServerClosed
    }
    addr := srv.Addr
    if addr == "" {
       addr = ":http"
    }
    ln, err := net.Listen("tcp", addr)
    if err != nil {
       return err
    }
    return srv.Serve(ln)
}

ListenAndServe 函數實例化 Sever,調用其ListenAndServe函數實現監聽與服務功能。在 gin 中,Engine 對象以 Handler 接口的對象的形式被傳入給了 net/http 庫的 Server 對象,作爲後續 Serve 對象處理網絡請求時調用的函數。

2.3.3 標準庫中的 Handler 接口

net/http 的 Server 結構體類型中有一個 Handler 接口類型的 Handler。

// A Server defines parameters for running an HTTP server.
// The zero value for Server is a valid configuration.
type Server struct {
 // Addr optionally specifies the TCP address for the server to listen on,
 // in the form "host:port". If empty, ":http" (port 80) is used.
 // The service names are defined in RFC 6335 and assigned by IANA.
 // See net.Dial for details of the address format.
 Addr string

 Handler Handler // handler to invoke, http.DefaultServeMux if nil
    // ...
}


//Handler接口有且只有一個函數,任何類型,只需要實現了該ServeHTTP函數,就實現了Handler接口,就可以用作Server的Handler,供HTTP處理時調用。
type Handler interface {
    ServeHTTP(ResponseWriter, *Request)
}
2.3.4 標準庫中的 Server.Serve 函數

Server.Serve函數用於監聽、接受和處理網絡請求,代碼如下:

// Serve accepts incoming connections on the Listener l, creating a
// new service goroutine for each. The service goroutines read requests and
// then call srv.Handler to reply to them.
//
// HTTP/2 support is only enabled if the Listener returns *tls.Conn
// connections and they were configured with "h2" in the TLS
// Config.NextProtos.
//
// Serve always returns a non-nil error and closes l.
// After Shutdown or Close, the returned error is ErrServerClosed.
func (srv *Server) Serve(l net.Listener) error {
 //...
 for {
  rw, err := l.Accept()
  if err != nil {
   select {
   case <-srv.getDoneChan():
    return ErrServerClosed
   default:
   }
   if ne, ok := err.(net.Error); ok && ne.Temporary() {
    if tempDelay == 0 {
     tempDelay = 5 * time.Millisecond
    } else {
     tempDelay *= 2
    }
    if max := 1 * time.Second; tempDelay > max {
     tempDelay = max
    }
    srv.logf("http: Accept error: %v; retrying in %v", err, tempDelay)
    time.Sleep(tempDelay)
    continue
   }
   return err
  }
  connCtx := ctx
  if cc := srv.ConnContext; cc != nil {
   connCtx = cc(connCtx, rw)
   if connCtx == nil {
    panic("ConnContext returned nil")
   }
  }
  tempDelay = 0
  c := srv.newConn(rw)
  c.setState(c.rwc, StateNew, runHooks) // before Serve can return
  go c.serve(connCtx)
 }
}

Server.Serve函數的實現中,啓動了一個無條件的 for 循環持續監聽、接受和處理網絡請求,主要流程爲:

  1. 接受請求l.Accept()調用在無請求時保持阻塞,直到接收到請求時,接受請求並返回建立的連接;

  2. 處理請求:啓動一個 goroutine,使用標準庫中 conn 連接對象的 serve 函數進行處理(go c.serve(connCtx));

2.3.5 conn.serve 函數
func (c *conn) serve(ctx context.Context) {
    c.remoteAddr = c.rwc.RemoteAddr().String()
    ctx = context.WithValue(ctx, LocalAddrContextKey, c.rwc.LocalAddr()) 
    ... 
    ctx, cancelCtx := context.WithCancel(ctx)
    c.cancelCtx = cancelCtx
    defer cancelCtx() 
    c.r = &connReader{conn: c}
    c.bufr = newBufioReader(c.r)
    c.bufw = newBufioWriterSize(checkConnErrorWriter{c}, 4<<10)  
    for {
        // 讀取請求
        w, err := c.readRequest(ctx) 
        ... 
        // 根據請求路由調用處理器處理請求
        serverHandler{c.server}.ServeHTTP(w, w.req)
        w.cancelCtx()
        if c.hijacked() {
            return
        }
        w.finishRequest() 
        ...
    }
}

一個連接建立之後,該連接中所有的請求都將在這個協程中進行處理,直到連接被關閉。在 for 循環裏面會循環調用 readRequest 讀取請求進行處理。可以在第 16 行看到請求處理是通過調用 serverHandler 結構體的 ServeHTTP 函數 進行的。

// serverHandler delegates to either the server's Handler or
// DefaultServeMux and also handles "OPTIONS *" requests.
type serverHandler struct {
    srv *Server
}

func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {
    handler := sh.srv.Handler
    if handler == nil {
       handler = DefaultServeMux
    }
    if !sh.srv.DisableGeneralOptionsHandler && req.RequestURI == "*" && req.Method == "OPTIONS" {
       handler = globalOptionsHandler{}
    }

    handler.ServeHTTP(rw, req)
}

可以看到上面第八行 handler := sh.srv.Handler,在 gin 框架中,sh.srv.Handler 其實就是 engine.Handler()。

func (engine *Engine) Handler() http.Handler {
    if !engine.UseH2C {
       return engine
    }
 // 使用了標準庫的h2c(http2 client)能力,本質還是使用了engine對象自身的ServeHTTP函數
    h2s := &http2.Server{}
    return h2c.NewHandler(engine, h2s)
}

engine.Handler() 函數使用了 http2 server 的能力,實際的邏輯處理還是依賴 engine 自身的 ServeHTTP() 函數。

2.3.6 Gin 的 Engine.ServeHTTP 函數

gin 在 gin.go 中實現了ServeHTTP函數,代碼如下:

// 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)
 c.Request = req
 c.reset()

 engine.handleHTTPRequest(c)

 engine.pool.Put(c)
}

主要步驟爲:

  1. 建立連接上下文:從全局 engine 的緩存池中提取上下文對象,填入當前連接的http.ResponseWriter實例與http.Request實例;

  2. 處理連接:以上下文對象的形式將連接交給函數處理,由engine.handleHTTPRequest(c)封裝實現了;

  3. 回收連接上下文:處理完畢後,將上下文對象回收進緩存池中。

gin 中對每個連接都需要的上下文對象進行緩存化存取,通過緩存池節省高併發時上下文對象頻繁創建銷燬造成內存頻繁分配與釋放的代價。

2.3.7 Gin 的 Engine.handleHTTPRequest 函數

handleHTTPRequest函數封裝了對請求進行處理的具體過程,位於 gin/gin.go 中,代碼如下:

func (engine *Engine) handleHTTPRequest(c *Context) {
 httpMethod := c.Request.Method
 rPath := c.Request.URL.Path
 unescape := false
 if engine.UseRawPath && len(c.Request.URL.RawPath) > 0 {
  rPath = c.Request.URL.RawPath
  unescape = engine.UnescapePathValues
 }

 if engine.RemoveExtraSlash {
  rPath = cleanPath(rPath)
 }

 // Find root of the tree for the given HTTP method
 // 根據http方法找到路由樹數組中對應的路由樹根節點
 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
  // 使用請求參數和請求路徑從路由樹中找到對應的路由節點
  value := root.getValue(rPath, c.params, unescape)
  if value.params != nil {
   c.Params = *value.params
  }
  
  // 調用context的Next()函數,實際上也就是調用路由節點的handlers方法鏈
  if value.handlers != nil {
   c.handlers = value.handlers
   c.fullPath = value.fullPath
   c.Next()
   c.writermem.WriteHeaderNow()
   return
  }
  // ...
  break
 }

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

Engine.handleHTTPRequest函數的主要邏輯位於中間的 for 循環中,主要爲:

  1. 遍歷查找engine.trees以找出當前請求的 HTTP Method 對應的處理樹;

  2. 從該處理樹中,根據當前請求的路徑與參數查詢出對應的處理函數value

  3. 將查詢出的處理函數鏈(gin.HandlerChain)寫入當前連接上下文的c.handlers中;

  4. 執行c.Next(),調用 handlers 鏈上的下一個函數(中間件 / 業務處理函數),開始形成 LIFO 的函數調用棧;

  5. 待函數調用棧全部返回後,c.writermem.WriteHeaderNow()根據上下文信息,將 HTTP 狀態碼寫入響應頭。

4. Gin 路由樹

gin 的路由樹源碼上面沒有展開,實際上就是實現了 radix tree 的數據結構:

實際看下 demo 中的代碼會生成的 radix tree:

mux.GET("/ping", func(c *gin.Context) {
    c.String(http.StatusOK, "ping")
})
mux.GET("/pong", func(c *gin.Context) {
    c.String(http.StatusOK, "pong")
})
mux.GET("/ping/hello", func(c *gin.Context) {
    c.String(http.StatusOK, "ping hello")
})

mux.GET("/about", func(c *gin.Context) {
    c.String(http.StatusOK, "about")
})

實際上源碼中的基數樹還涉及可變參數路由的處理,會更復雜一些。

5. Gin 中間件實現

  1. 每個路由節點都會掛載一個函數鏈,鏈的前面部分是插件函數,後面部分是業務處理函數。

  2. 在 gin 中插件和業務處理函數形式是一樣的,都是 func(*Context)。當我們定義路由時,gin 會將插件函數和業務處理函數合併在一起形成一個鏈條結構。gin 在接收到客戶端請求時,找到相應的處理鏈,構造一個 Context 對象,再調用它的 Next() 函數就正式進入了請求處理的全流程。

  3. 慣用法: 可以通過在處理器中調用c.Next()提前進入下一個處理器,待其執行完後再返回到當前處理器,這種比較適合需要對請求做前置和後置處理的場景,如請求執行時間統計,請求的前後日誌等。

  4. 有時候我們可能會希望,某些條件觸發時直接返回,不再繼續後續的處理操作。Context 提供了Abort方法幫助我們實現這樣的目的。原理是將 Context.index 調整到一個比較大的數字,gin 中要求一個路由的全部處理器個數不超過 63,每次執行一個處理器時,會先判斷 index 是否超過了這個限制,如果超過了就不會執行。

    // Next should be used only inside middleware. // It executes the pending handlers in the chain inside the calling handler. // See example in GitHub. 
    func (c *Context) Next() {
        c.index++
        for c.index < int8(len(c.handlers)) {
           if c.handlers[c.index] == nil {
              continue        }
           c.handlers[c.index](c)
           c.index++
        }
    }
        
    func (c *Context) Abort() {
        c.index = abortIndex 
    }  
    const abortIndex int8 = math.MaxInt8 >> 1

總結

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/T-rJjiyvp5dIdnGIVY-OcA