高性能GO web 框架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

  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而生成

  • 这里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循环持续监听、接受和处理网络请求,主要流程为:

  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中,代码如下:

展开阅读全文

本文系作者在时代Java发表,未经许可,不得转载。

如有侵权,请联系nowjava@qq.com删除。

编辑于

关注时代Java

关注时代Java