高性能 Gin 框架原理學習教程
作者 | jiayan
工作中的部分項目使用到了gin框架,因此從源碼層面學習一下其原理。
一、概述
Gin是一款高性能的Go語言Web框架,Gin的一些特性:
- 快速 基于 Radix 樹的路由,小內(nèi)存占用,沒有反射,可預測的 API 性能。
- 支持中間件 傳入的 HTTP 請求可以由一系列中間件和最終操作來處理,例如:Logger,Authorization,GZIP,最終操作 DB。
- 路由組 組織路由組非常方便,同時這些組可以無限制地嵌套而不會降低性能。
- 易用性 gin封裝了標準庫的底層能力。標準庫暴露給開發(fā)者的函數(shù)參數(shù)是(w http.ResponseWriter, req *http.Request),易用性低,需要直接從請求中讀取數(shù)據(jù)、反序列化,響應時手動序列化、設置Content-Type、寫響應內(nèi)容,返回碼等。使用gin可以幫我們更專注于業(yè)務處理。
二、源碼學習
下面從一個簡單的包含基礎路由和路由組路由的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 處理函數(shù)
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")
}
1. 初始化Gin(Default函數(shù))
初始化步驟主要是初始化engine與加載兩個默認的中間件:
func Default(opts ...OptionFunc) *Engine {
debugPrintWARNINGDefault()
// 初始化engine實例
engine := New()
// 默認加載log & recovery中間件
engine.Use(Logger(), Recovery())
return engine.With(opts...)
}
(1) 初始化engine
engine是gin中的核心對象,gin通過 Engine 對象來定義服務路由信息、組裝插件、運行服務,是框架的核心發(fā)動機,整個 Web 服務的都是由它來驅(qū)動的 關鍵字段:
- RouterGroup: RouterGroup 是對路由樹的包裝,所有的路由規(guī)則最終都是由它來進行管理。RouteGroup 對象里面還會包含一個 Engine 的指針,可以調(diào)用engine的addRoute函數(shù)。
- trees: 基于前綴樹實現(xiàn)。每個節(jié)點都會掛接若干請求處理函數(shù)構成一個請求處理鏈 HandlersChain。當一個請求到來時,在這棵樹上找到請求 URL 對應的節(jié)點,拿到對應的請求處理鏈來執(zhí)行就完成了請求的處理。
- addRoute: 用于添加 URL 請求處理器,它會將對應的路徑和處理器掛接到相應的請求樹中。
- RouterGroup: 內(nèi)部有一個前綴路徑屬性(basePath),它會將所有的子路徑都加上這個前綴再放進路由樹中。有了這個前綴路徑,就可以實現(xiàn) URL 分組功能。
func New(opts ...OptionFunc) *Engine {
debugPrintWARNINGNew()
engine := &Engine{
RouterGroup: RouterGroup{
Handlers: nil,
// 默認的basePath為/,綁定路由時會用到此參數(shù)來計算絕對路徑
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) 初始化中間件
① Engine.Use函數(shù)
Engine.Use函數(shù)用于將中間件添加到當前的路由上,位于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函數(shù)
實際上,還需要進一步調(diào)用engine.RouterGroup.Use(middleware...)完成實際的中間件注冊工作,函數(shù)位于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()
}
實際上就是把中間件(本質(zhì)是一個函數(shù))添加到HandlersChain類型(實質(zhì)上為數(shù)組type HandlersChain []HandlerFunc)的group.Handlers中。
③ HandlerFunc
type HandlerFunc func(*Context)
如果需要實現(xiàn)一個中間件,那么需要實現(xiàn)該類型,函數(shù)參數(shù)只有*Context。
④ gin.Context
貫穿一個 http 請求的所有流程,包含全部上下文信息。
提供了很多內(nèi)置的數(shù)據(jù)綁定和響應形式,JSON、HTML、Protobuf 、MsgPack、Yaml 等,它會為每一種形式都單獨定制一個渲染器
engine的 ServeHTTP 函數(shù),在響應一個用戶的請求時,都會先從臨時對象池中取一個context對象。使用完之后再放回臨時對象池。為了保證并發(fā)安全,如果在一次請求新起一個協(xié)程,那么一定要copy這個context進行參數(shù)傳遞。
type Context struct {
writermem responseWriter
Request *http.Request // 請求對象
Writer ResponseWriter // 響應對象
Params Params // URL 匹配參數(shù)
handlers HandlersChain // // 請求處理鏈
...
}
2. 綁定處理函數(shù)到對應的HttpMethod上
(1) 普通路由實現(xiàn)
調(diào)用綁定函數(shù):
mux.GET("/ping", func(c *gin.Context) {
c.String(http.StatusOK, "ping")
})
函數(shù)實際上走到了engine對象的匿名成員RouterGroup的handle函數(shù)中
// 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)
// 將函數(shù)參數(shù)中的 "處理函數(shù)" handlers與本路由組已有的Handlers組合起來,作為最終要執(zhí)行的完整handlers列表
handlers = group.combineHandlers(handlers)
// routerGroup會存有engine對象的引用,調(diào)用engine的addRoute將絕對路徑與處理函數(shù)列表綁定起來
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函數(shù):
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)的路由信息都各自由一個樹結(jié)構來維護,該樹結(jié)構的模型與函數(shù)實現(xiàn)位于gin/tree.go中,此處不再繼續(xù)展開。不同http方法的樹根節(jié)點組成了 engine.trees 這個數(shù)組
// 從engine的路由樹數(shù)組中遍歷找到該http方法對應的路由樹的根節(jié)點
root := engine.trees.get(method)
if root == nil {
// 如果根節(jié)點不存在,那么新建根節(jié)點
root = new(node)
root.fullPath = "/"
// 將根節(jié)點添加到路由樹數(shù)組中
engine.trees = append(engine.trees, methodTree{method: method, root: root})
}
// 調(diào)用根節(jié)點的addRoute函數(shù),將絕對路徑與處理函數(shù)鏈綁定起來
root.addRoute(path, handlers)
...
}
// 路由樹數(shù)組數(shù)據(jù)結(jié)構
type methodTree struct {
method string root *node
}
type methodTrees []methodTree
(2) 路由組的實現(xiàn)
// 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函數(shù)會返回一個新的RouterGroup對象,每一個RouterGroup都會基于原有的RouterGroup而生成:
- 這里demo中生成的systemGroup,會基于根routerGroup(basePath:"/")而生成,那么systemGroup的basePath為 joinPaths("/" + "system")
- 基于systemGroup生成的systemAuthGroup,basePath為sysetmGroup的basePath: "/system" 與函數(shù)參數(shù)中"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與傳入?yún)?shù)的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對象,調(diào)用Group()生成新RouterGroup后,再調(diào)用GET, POST..綁定路由時,依舊會使用全局engine對象的handle方法,最終會走到:
group.engine.addRoute(httpMethod, absolutePath, handlers)
根據(jù)http方法找到對應的路由樹根節(jié)點,然后再更新路由樹。
3. 啟動Gin
(1) Engine.Run函數(shù)
// 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
}
可以看到,最核心的監(jiān)聽與服務實質(zhì)上是調(diào)用Go語言內(nèi)置庫net/http的http.ListenAndServe函數(shù)實現(xiàn)的。
(2) net/http的ListenAndServe函數(shù)
// 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函數(shù)實例化Sever,調(diào)用其ListenAndServe函數(shù)實現(xiàn)監(jiān)聽與服務功能。在gin中,Engine對象以Handler接口的對象的形式被傳入給了net/http庫的Server對象,作為后續(xù)Serve對象處理網(wǎng)絡請求時調(diào)用的函數(shù)。
(3) 標準庫中的Handler接口
net/http的Server結(jié)構體類型中有一個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接口有且只有一個函數(shù),任何類型,只需要實現(xiàn)了該ServeHTTP函數(shù),就實現(xiàn)了Handler接口,就可以用作Server的Handler,供HTTP處理時調(diào)用。
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
(4) 標準庫中的Server.Serve函數(shù)
Server.Serve函數(shù)用于監(jiān)聽、接受和處理網(wǎng)絡請求,代碼如下:
// 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函數(shù)的實現(xiàn)中,啟動了一個無條件的for循環(huán)持續(xù)監(jiān)聽、接受和處理網(wǎng)絡請求,主要流程為:
- 接受請求:l.Accept()調(diào)用在無請求時保持阻塞,直到接收到請求時,接受請求并返回建立的連接;
- 處理請求:啟動一個goroutine,使用標準庫中conn連接對象的serve函數(shù)進行處理(go c.serve(connCtx));
(5) conn.serve函數(shù)
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)
...
// 根據(jù)請求路由調(diào)用處理器處理請求
serverHandler{c.server}.ServeHTTP(w, w.req)
w.cancelCtx()
if c.hijacked() {
return
}
w.finishRequest()
...
}
}
一個連接建立之后,該連接中所有的請求都將在這個協(xié)程中進行處理,直到連接被關閉。在 for 循環(huán)里面會循環(huán)調(diào)用 readRequest 讀取請求進行處理??梢栽诘?6行看到請求處理是通過調(diào)用 serverHandler結(jié)構體的ServeHTTP函數(shù)進行的。
// 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)能力,本質(zhì)還是使用了engine對象自身的ServeHTTP函數(shù)
h2s := &http2.Server{}
return h2c.NewHandler(engine, h2s)
}
engine.Handler()函數(shù)使用了http2 server的能力,實際的邏輯處理還是依賴engine自身的ServeHTTP()函數(shù)。
(6) Gin的Engine.ServeHTTP函數(shù)
gin在gin.go中實現(xiàn)了ServeHTTP函數(shù),代碼如下:
// 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實例;
- 處理連接:以上下文對象的形式將連接交給函數(shù)處理,由engine.handleHTTPRequest(c)封裝實現(xiàn)了;
- 回收連接上下文:處理完畢后,將上下文對象回收進緩存池中。
gin中對每個連接都需要的上下文對象進行緩存化存取,通過緩存池節(jié)省高并發(fā)時上下文對象頻繁創(chuàng)建銷毀造成內(nèi)存頻繁分配與釋放的代價。
(7) Gin的Engine.handleHTTPRequest函數(shù)
handleHTTPRequest函數(shù)封裝了對請求進行處理的具體過程,位于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
// 根據(jù)http方法找到路由樹數(shù)組中對應的路由樹根節(jié)點
t := engine.trees
for i, tl := 0, len(t); i < tl; i++ {
if t[i].method != httpMethod {
continue
}
// 找到了根節(jié)點
root := t[i].root
// Find route in tree
// 使用請求參數(shù)和請求路徑從路由樹中找到對應的路由節(jié)點
value := root.getValue(rPath, c.params, unescape)
if value.params != nil {
c.Params = *value.params
}
// 調(diào)用context的Next()函數(shù),實際上也就是調(diào)用路由節(jié)點的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函數(shù)的主要邏輯位于中間的for循環(huán)中,主要為:
- 遍歷查找engine.trees以找出當前請求的HTTP Method對應的處理樹;
- 從該處理樹中,根據(jù)當前請求的路徑與參數(shù)查詢出對應的處理函數(shù)value;
- 將查詢出的處理函數(shù)鏈(gin.HandlerChain)寫入當前連接上下文的c.handlers中;
- 執(zhí)行c.Next(),調(diào)用handlers鏈上的下一個函數(shù)(中間件/業(yè)務處理函數(shù)),開始形成LIFO的函數(shù)調(diào)用棧;
- 待函數(shù)調(diào)用棧全部返回后,c.writermem.WriteHeaderNow()根據(jù)上下文信息,將HTTP狀態(tài)碼寫入響應頭。
四、Gin 路由樹
gin的路由樹源碼上面沒有展開,實際上就是實現(xiàn)了radix tree的數(shù)據(jù)結(jié)構:
- trie tree: 前綴樹,一顆多叉樹,用于字符串搜索,每個樹節(jié)點存儲一個字符,從根節(jié)點到任意一個葉子結(jié)點串起來就是一個字符串。
- radix tree: 基數(shù)樹(壓縮前綴樹),對空間進一步壓縮,從上往下提取公共前綴,非公共部分存到子節(jié)點,這樣既節(jié)省了空間,同時也提高了查詢效率(左邊字符串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")
})
實際上源碼中的基數(shù)樹還涉及可變參數(shù)路由的處理,會更復雜一些。
五、Gin 中間件實現(xiàn)
每個路由節(jié)點都會掛載一個函數(shù)鏈,鏈的前面部分是插件函數(shù),后面部分是業(yè)務處理函數(shù)。
在 gin 中插件和業(yè)務處理函數(shù)形式是一樣的,都是 func(*Context)。當我們定義路由時,gin 會將插件函數(shù)和業(yè)務處理函數(shù)合并在一起形成一個鏈條結(jié)構。gin 在接收到客戶端請求時,找到相應的處理鏈,構造一個 Context 對象,再調(diào)用它的 Next() 函數(shù)就正式進入了請求處理的全流程。
慣用法: 可以通過在處理器中調(diào)用c.Next()提前進入下一個處理器,待其執(zhí)行完后再返回到當前處理器,這種比較適合需要對請求做前置和后置處理的場景,如請求執(zhí)行時間統(tǒng)計,請求的前后日志等。
有時候我們可能會希望,某些條件觸發(fā)時直接返回,不再繼續(xù)后續(xù)的處理操作。Context提供了Abort方法幫助我們實現(xiàn)這樣的目的。原理是將 Context.index 調(diào)整到一個比較大的數(shù)字,gin中要求一個路由的全部處理器個數(shù)不超過63,每次執(zhí)行一個處理器時,會先判斷index是否超過了這個限制,如果超過了就不會執(zhí)行。
// 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
總結(jié)
從源碼中可以看到gin與fasthttp庫基于自身的http庫來實現(xiàn)網(wǎng)絡層不同,gin是基于標準http庫的能力來構建的自己的網(wǎng)絡層的,所以性能應該是與原生http庫接近的。
- gin通過實現(xiàn)Go語言提供的接口快捷地接入Go的內(nèi)置庫功能,使得上層應用與底層實現(xiàn)之間互不依賴。
- gin在性能上針對HTTP Web框架常見的高并發(fā)問題進行了優(yōu)化,例如:通過上下文對象的緩存池節(jié)省連接高并發(fā)時內(nèi)存頻繁申請與釋放的代價
- gin的壓縮前綴樹數(shù)據(jù)結(jié)構設計,不同于標準庫中基于map的路由,實現(xiàn)了高效的路由查找(匹配時間復雜度是O(k)),另外可以支持模糊匹配、參數(shù)匹配等功能,具有更高的靈活性和可擴展性。
- gin采用中間件(Middleware)來處理請求和響應,可以實現(xiàn)各種功能,例如日志記錄、權限驗證、請求限流等。