高性能 Gin 框架原理學習教程
作者:jiayan
工作中的部分項目使用到了 gin 框架,因此從源碼層面學習一下其原理。
1. 概述
Gin 是一款高性能的 Go 語言 Web 框架,Gin 的一些特性:
-
快速 基於 Radix 樹的路由,小內存佔用,沒有反射,可預測的 API 性能。
-
支持中間件 傳入的 HTTP 請求可以由一系列中間件和最終操作來處理,例如:Logger,Authorization,GZIP,最終操作 DB。
-
路由組 組織路由組非常方便,同時這些組可以無限制地嵌套而不會降低性能。
-
易用性 gin 封裝了標準庫的底層能力。標準庫暴露給開發者的函數參數是
(w http.ResponseWriter, req *http.Request)
,易用性低,需要直接從請求中讀取數據、反序列化,響應時手動序列化、設置 Content-Type、寫響應內容,返回碼等。使用 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 服務的都是由它來驅動的 關鍵字段:
-
RouterGroup: RouterGroup 是對路由樹的包裝,所有的路由規則最終都是由它來進行管理。RouteGroup 對象裏面還會包含一個 Engine 的指針,可以調用 engine 的 addRoute 函數。
-
trees: 基於前綴樹實現。每個節點都會掛接若干請求處理函數構成一個請求處理鏈 HandlersChain。當一個請求到來時,在這棵樹上找到請求 URL 對應的節點,拿到對應的請求處理鏈來執行就完成了請求的處理。
-
addRoute: 用於添加 URL 請求處理器,它會將對應的路徑和處理器掛接到相應的請求樹中。
-
RouterGroup: 內部有一個前綴路徑屬性 (basePath),它會將所有的子路徑都加上這個前綴再放進路由樹中。有了這個前綴路徑,就可以實現 URL 分組功能。
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
-
貫穿一個 http 請求的所有流程,包含全部上下文信息。
-
提供了很多內置的數據綁定和響應形式,JSON、HTML、Protobuf 、MsgPack、Yaml 等,它會爲每一種形式都單獨定製一個渲染器
-
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 而生成:
-
這裏 demo 中生成的 systemGroup,會基於根 routerGroup(basePath:"/")而生成,那麼 systemGroup 的 basePath 爲
joinPaths("/" + "system")
-
基於 systemGroup 生成的 systemAuthGroup,basePath 爲 sysetmGroup 的 basePath: "/system" 與函數參數中 "auth" 組成
join("/system", "auth")
,// Group creates a new router group. You should add all the routes that have common middlewares or the same path prefix. // For example, all the routes that use a common middleware for authorization could be grouped. func (group *RouterGroup) Group(relativePath string, handlers ...HandlerFunc) *RouterGroup { return &RouterGroup{ // 新routerGroup的handlers由原routerGroup的handlers和本次傳入的handlers組成 Handlers: group.combineHandlers(handlers), // 新routerGroup的basePath由原routerGroup的basePath + relativePath 計算出來 basePath: group.calculateAbsolutePath(relativePath), // 新routerGroup依舊持久全局engine對象 engine: group.engine, } } // 合併handlers,將group的handlers與傳入參數的handlers合併 func (group *RouterGroup) combineHandlers(handlers HandlersChain) HandlersChain { finalSize := len(group.Handlers) + len(handlers) assert1(finalSize < int(abortIndex), "too many handlers") mergedHandlers := make(HandlersChain, finalSize) copy(mergedHandlers, group.Handlers) copy(mergedHandlers[len(group.Handlers):], handlers) return mergedHandlers } // 拼裝routerGroup的basePath與傳入的相對路徑relativePath func (group *RouterGroup) calculateAbsolutePath(relativePath string) string { return joinPaths(group.basePath, relativePath) }
需要注意的一點是,每個 routerGroup 都持有全局 engine 對象,調用 Group() 生成新 RouterGroup 後,再調用 GET, POST.. 綁定路由時,依舊會使用全局 engine 對象的 handle 方法,最終會走到:
group.engine.addRoute(httpMethod, absolutePath, handlers)
根據 http 方法找到對應的路由樹根節點,然後再更新路由樹。
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 循環持續監聽、接受和處理網絡請求,主要流程爲:
-
接受請求:
l.Accept()
調用在無請求時保持阻塞,直到接收到請求時,接受請求並返回建立的連接; -
處理請求:啓動一個 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)
}
主要步驟爲:
-
建立連接上下文:從全局 engine 的緩存池中提取上下文對象,填入當前連接的
http.ResponseWriter
實例與http.Request
實例; -
處理連接:以上下文對象的形式將連接交給函數處理,由
engine.handleHTTPRequest(c)
封裝實現了; -
回收連接上下文:處理完畢後,將上下文對象回收進緩存池中。
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 循環中,主要爲:
-
遍歷查找
engine.trees
以找出當前請求的 HTTP Method 對應的處理樹; -
從該處理樹中,根據當前請求的路徑與參數查詢出對應的處理函數
value
; -
將查詢出的處理函數鏈(
gin.HandlerChain
)寫入當前連接上下文的c.handlers
中; -
執行
c.Next()
,調用 handlers 鏈上的下一個函數(中間件 / 業務處理函數),開始形成 LIFO 的函數調用棧; -
待函數調用棧全部返回後,
c.writermem.WriteHeaderNow()
根據上下文信息,將 HTTP 狀態碼寫入響應頭。
4. Gin 路由樹
gin 的路由樹源碼上面沒有展開,實際上就是實現了 radix tree 的數據結構:
-
trie tree: 前綴樹,一顆多叉樹,用於字符串搜索,每個樹節點存儲一個字符,從根節點到任意一個葉子結點串起來就是一個字符串。
-
radix tree: 基數樹 (壓縮前綴樹),對空間進一步壓縮,從上往下提取公共前綴,非公共部分存到子節點,這樣既節省了空間,同時也提高了查詢效率 (左邊字符串 sleep 查詢需要 5 步, 右邊只需要 3 步)。
實際看下 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 中間件實現
-
每個路由節點都會掛載一個函數鏈,鏈的前面部分是插件函數,後面部分是業務處理函數。
-
在 gin 中插件和業務處理函數形式是一樣的,都是
func(*Context)
。當我們定義路由時,gin 會將插件函數和業務處理函數合併在一起形成一個鏈條結構。gin 在接收到客戶端請求時,找到相應的處理鏈,構造一個 Context 對象,再調用它的 Next() 函數就正式進入了請求處理的全流程。 -
慣用法: 可以通過在處理器中調用
c.Next()
提前進入下一個處理器,待其執行完後再返回到當前處理器,這種比較適合需要對請求做前置和後置處理的場景,如請求執行時間統計,請求的前後日誌等。 -
有時候我們可能會希望,某些條件觸發時直接返回,不再繼續後續的處理操作。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
總結
-
從源碼中可以看到 gin 與 fasthttp 庫基於自身的 http 庫來實現網絡層不同,gin 是基於標準 http 庫的能力來構建的自己的網絡層的,所以性能應該是與原生 http 庫接近的。
-
gin 通過實現 Go 語言提供的接口快捷地接入 Go 的內置庫功能,使得上層應用與底層實現之間互不依賴。
-
gin 在性能上針對 HTTP Web 框架常見的高併發問題進行了優化,例如:通過上下文對象的緩存池節省連接高併發時內存頻繁申請與釋放的代價
-
gin 的壓縮前綴樹數據結構設計,不同於標準庫中基於 map 的路由,實現了高效的路由查找(匹配時間複雜度是 O(k)),另外可以支持模糊匹配、參數匹配等功能,具有更高的靈活性和可擴展性。
-
gin 採用中間件(Middleware)來處理請求和響應,可以實現各種功能,例如日誌記錄、權限驗證、請求限流等。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/T-rJjiyvp5dIdnGIVY-OcA