gin01:初探gin的启动

4 阅读7分钟

之前探究了go原生net/http的启动过程,现在使用gin框架来启动一个后端接口。

func main() {
    router := gin.Default()
    router.GET("/ping", func(c *gin.Context) {
       c.JSON(http.StatusOK, gin.H{
          "message": "pong",
       })
    })
    router.Run()
}

gin框架中,Engine是整个Web应用的核心,它集成了路由中间件管理HTTP 服务运行模板渲染错误处理等所有核心功能。所有请求的入口出口都由它来调度。
相对于直接操作底层的net/httpEngine有以下的好处:

  1. 封装与简化Engine封装了net/http中复杂的ServeMuxHandler逻辑。它提供了更直观、更强大的路由定义方式(如路由分组、参数路由)和中间件机制,极大地提升了开发效率。
  2. 高性能路由ginEngine内部使用了一个名为 httprouter的定制化高性能路由库。它使用基数树(Radix Tree) 来存储和匹配路由,使得路由查找速度极快,且不受注册路由数量多少的影响。
  3. 中间件流水线Engine提供了统一的中间件注册和管理能力。中间件可以全局作用于所有路由,也可以作用于特定路由组,这种链式处理机制是构建现代 Web 应用(如认证、日志、跨域处理)的基石。

Engine的创建

gin.Default()的源码如下:

func Default(opts ...OptionFunc) *Engine {
    debugPrintWARNINGDefault()
    engine := New()
    engine.Use(Logger(), Recovery())
    return engine.With(opts...)
}

其通过gin内部的一个名为New的函数创建一个Engine对象,并使用Use方法增加两个中间件Logger()以及Recovery(),最后使用OptionFunc来修改engine的参数。

New()

New()函数的源码如下:

func New(opts ...OptionFunc) *Engine {
    debugPrintWARNINGNew()
    //创建一个Engine
    engine := &Engine{
       RouterGroup: RouterGroup{
          Handlers: nil,
          //设置路由的根路径
          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,
       UseEscapedPath:         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.engine = engine
    engine.pool.New = func() any {
       return engine.allocateContext(engine.maxParams)
    }
    return engine.With(opts...)
}

这份源码engine.engine = engineengine.engineEngine结构体中嵌入字段RouterGroup字段,这使得RouterGroup能够反过来访问到Engine
为什么要这么做?
首先,gin.Engine结构体内嵌了gin.RouterGroup。这意味着Engine是一个RouterGroup,继承了其所有方法(如 GET, POST, Use等)。
其次,gin的核心路由树(engine.trees)是存放在 Engine层级,而不是 RouterGroup层级的。但POST这类添加处理逻辑的方法是在RouterGroup上定义的。
这就产生了一个问题:当一个 RouterGroup(包括顶层的 Engine自己,以及通过 Group()创建的子分组)调用 GET(“/path”, handler)时,这个方法最终需要去修改 Engine.trees。然而,一个普通的 RouterGroup实例并不知道它所属于的那个顶层的 Engine是谁。 通过engine.engine = engine就可以解决这个问题。

使用Use()增加中间件

Use()的源码如下:

func (engine *Engine) Use(middleware ...HandlerFunc) IRoutes {
    engine.RouterGroup.Use(middleware...)
    engine.rebuild404Handlers()
    engine.rebuild405Handlers()
    return engine
}

本质上是给engine中的RouterGroup增加中间件,其源码如下:

func (group *RouterGroup) Use(middleware ...HandlerFunc) IRoutes {
    group.Handlers = append(group.Handlers, middleware...)
    return group.returnObj()
}

使用OptionFunc修改Engine

先查看什么是OptionFunc:

type OptionFunc func(*Engine)

OptionFunc的本质是一个接收Engine的指针,对Engine进行修改的函数
engine.With()中,源码如下:

func (engine *Engine) With(opts ...OptionFunc) *Engine {
    for _, opt := range opts {
       opt(engine)
    }

    return engine
}

With()方法的作用是遍历这些OptionFunc,并利用这些OptionFunc来修改engine。 现在有一个问题:既然New()函数的最后也使用了With()方法,为什么不将Default()修改为以下的内容?

func Default(opts ...OptionFunc) *Engine {
    debugPrintWARNINGDefault()
    engine := New(opts...)
    engine.Use(Logger(), Recovery())
    return engine
}

原因是要保证用户自己的配置大于代码约定,比如用户写了一个中间件清空的OptionFunc

func main() {
    clearMiddlerFunc := func(engine *gin.Engine) {
       engine.Handlers = make(gin.HandlersChain, 0)
    }
    router := gin.Default(clearMiddlerFunc)

    router.With(func(engine *gin.Engine) {
       engine.RedirectTrailingSlash = false
    })
    router.GET("/ping", func(c *gin.Context) {
       c.JSON(http.StatusOK, gin.H{
          "message": "pong",
       })
    })
    router.Run()
}

如果是先清空中间件,再先配置Logger以及Recovery,这就与用户实际的需求不相符。

路径和处理逻辑的绑定

在这一部分中,我们讨论在router.GET()的内部发生了什么。 源码如下:

func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes {
    return group.handle(http.MethodGet, relativePath, handlers)
}

可以看出,它和其他的方法POST,PATCH等底层都是group.handle()

func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes {
    absolutePath := group.calculateAbsolutePath(relativePath)
    handlers = group.combineHandlers(handlers)
    group.engine.addRoute(httpMethod, absolutePath, handlers)
    return group.returnObj()
}

在其中,首先计算请求的绝对路径,其等于group的根路径+传入的相对路径。

func (group *RouterGroup) calculateAbsolutePath(relativePath string) string {
    return joinPaths(group.basePath, relativePath)
}

其次,获取该请求的逻辑处理链,其等于group中的HandlersChain+传入的逻辑处理链。

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
}

然后,将请求方法类型请求的绝对路径请求的逻辑处理链作为参数添加到Engine的路由,最后返回一个returnedObj()的返回值。

func (group *RouterGroup) returnObj() IRoutes {
    if group.root {
       return group.engine
    }
    return group
}

此处的逻辑是:如果当前组是根组,返回最上层的Engine,如果是子组,返回子组本身。
为什么要加以判断呢?
我的理解是,在根组的情况下,一般直接使用Engine,而不是Engine.RouterGroup,为此保证调用者和实际返回类型一致,才做如此判断。而子组本身就是一个RouterGroup,因此直接返回。

路由添加

Engine中最重要的便是路由的添加,下面将深入这一块的代码:

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)

    root := engine.trees.get(method)
    if root == nil {
       root = new(node)
       root.fullPath = "/"
       engine.trees = append(engine.trees, methodTree{method: method, root: root})
    }
    root.addRoute(path, handlers)

    if paramsCount := countParams(path); paramsCount > engine.maxParams {
       engine.maxParams = paramsCount
    }

    if sectionsCount := countSections(path); sectionsCount > engine.maxSections {
       engine.maxSections = sectionsCount
    }
}

首要便是三个判断,要求:

  1. 路径要求以/为起点
  2. 请求方法不能为空
  3. 逻辑处理链的长度大于0

然后根据请求方法取出对应树的根节点

func (trees methodTrees) get(method string) *node {
    for _, tree := range trees {
       if tree.method == method {
          return tree.root
       }
    }
    return nil
}

engine.trees是一个[]methodTree,它存储了每一种请求方法的路由树

type methodTree struct {
    method string
    root   *node
}

如果当前的请求方法没有对应的methodTree,则创建新的树根节点,并将这棵新树添加到 engine.trees 中。 再调用root.addRoute()将路径和逻辑处理链加入路由。
最后记录最大路径参数数量最大路径段数避免在每次HTTP请求处理中重复分配内存,实现零内存分配的高性能路由匹配

Engine的运行

以下是Run()的源码:

func (engine *Engine) Run(addr ...string) (err error) {
    defer func() { debugPrintError(err) }()

    if engine.isUnsafeTrustedProxies() {
       debugPrint("[WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.\n" +
          "Please check https://github.com/gin-gonic/gin/blob/master/docs/doc.md#dont-trust-all-proxies for details.")
    }
    engine.updateRouteTrees()
    address := resolveAddress(addr)
    debugPrint("Listening and serving HTTP on %s\n", address)
    server := &http.Server{ // #nosec G112
       Addr:    address,
       Handler: engine.Handler(),
    }
    err = server.ListenAndServe()
    return
}

首先会解析地址

  1. 如果没有传入地址,将获取名为PORT的环境变量,如果该变量不存在,返回8080,如果存在,返回该变量。
  2. 如果传入地址数为1,返回该地址
  3. 如果传入地址数过多,报错
func resolveAddress(addr []string) string {
    switch len(addr) {
    case 0:
       if port := os.Getenv("PORT"); port != "" {
          debugPrint("Environment variable PORT="%s"", port)
          return ":" + port
       }
       debugPrint("Environment variable PORT is undefined. Using port :8080 by default")
       return ":8080"
    case 1:
       return addr[0]
    default:
       panic("too many parameters")
    }
}

其次,创建一个http服务器,传入地址以及Handler,并运行这个服务器。

func (engine *Engine) Run(addr ...string) (err error) {
    defer func() { debugPrintError(err) }()

    if engine.isUnsafeTrustedProxies() {
       debugPrint("[WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.\n" +
          "Please check https://github.com/gin-gonic/gin/blob/master/docs/doc.md#dont-trust-all-proxies for details.")
    }
    engine.updateRouteTrees()
    address := resolveAddress(addr)
    debugPrint("Listening and serving HTTP on %s\n", address)
    server := &http.Server{ // #nosec G112
       Addr:    address,
       Handler: engine.Handler(),
    }
    err = server.ListenAndServe()
    return
}

Handler的传入

Handlernet/http包中的一个接口

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

而传给http服务器的Handlerengine.Handler()的返回值

func (engine *Engine) Handler() http.Handler {
    if !engine.UseH2C {
       return engine
    }

    h2s := &http2.Server{}
    return h2c.NewHandler(engine, h2s)
}

不论是什么情况,Engine都直接作为Hanlder被返回或者使用,因为Engine实现了Handler接口。

一次请求的过程

前面的过程与原生http都一样,直到这个调用

serverHandler{c.server}.ServeHTTP(w, w.req)
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)
}

Engine顶替了默认的DefaultServeMux,来处理请求。

func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    engine.routeTreesUpdated.Do(func() {
       engine.updateRouteTrees()
    })

    c := engine.pool.Get().(*Context)
    c.writermem.reset(w)
    c.Request = req
    c.reset()

    engine.handleHTTPRequest(c)

    engine.pool.Put(c)
}

如果是第一次请求,将通过 sync.OnceDo方法,将路由树中的字符替换。
然后从池中取出一个ginContext,将http.ResponseWriter传入。
最后使用handleHTTPRequest处理请求:

  1. 先通过请求方法获取到对应的路由树
  2. 再通过路由树获取到对应的路由节点
  3. 如果对应的处理逻辑链存在,将其赋给Context,并调用c.Next()
func (c *Context) Next() {
    c.index++
    for c.index < safeInt8(len(c.handlers)) {
       if c.handlers[c.index] != nil {
          c.handlers[c.index](c)
       }
       c.index++
    }
}

在其中不断调用处理逻辑处理请求。