1.gin介绍
gin框架是golang中比较常见的web框架,截止到目前(2023-03-21),github上已经累计了67.3K的star数,这足以表明其优秀。作为一名想要知其然亦想知其所以然的程序员,希望通过学习gin框架的实现原理来提高自己的技术能力,也希望通过分享来帮助想要进行学习的同学。
2. 前置知识
其实golang本身的标准库已经足以实现简单的web服务,但是出于以下原因,使得直接使用标准库开发难以满足我们的需求:
- 标准库本身提供了比较简单的路由注册能力,只支持精确匹配,而实际开发时难免会遇到需要使用通配、路径参数的场景
 - 标准库暴露给开发者的函数参数是
(w http.ResponseWriter, req *http.Request),这就导致我们需要直接从请求中读取数据、反序列化,响应时手动序列化、设置Content-Type、写响应内容,比较麻烦 - 有时候我们希望能够在不过多地侵入业务的前提下,对请求或响应进行一些前置或后置处理。直接基于标准库开发,业务和非业务代码难免会耦合在一起
 
下面是直接使用标准库开发一个简单的获取当前时间的案例,当客户端以GET方法请求路径/time时,服务端以json方式返回当前时间,格式为: {"time": "xxx"}.
            
            
              go
              
              
            
          
          func main() {
    http.HandleFunc("/time", func(w http.ResponseWriter, r *http.Request) {
        w.WriteHeader(http.StatusOK)
        w.Header().Set("Content-Type", "application/json")
        tim := map[string]string{
            "time": time.Now().Format("2006-01-02"),
        }
        byts, err := json.Marshal(tim)
        if err != nil {
            panic(err)
        }
        w.Write(byts)
    })
    http.ListenAndServe("0.0.0.0:8080", nil)
}
        由此我们能得到基于net/http开发web服务的一般流程:
- 利用
http.HandleFunc函数注册路由,并指定处理函数,函数签名为func(w http.ResponseWriter, r *http.Request) - 在处理函数内部获取查询参数、路径参数、读取请求体并反序列化
 - 业务逻辑处理
 - 错误处理(设置错误响应状态码、错误信息等)
 - 设置响应状态码、设置响应头(如Content-Type)、处理结果序列化并写入响应体
 - 调用
http.ListenAndServer进行端口监听 
操作起来略有不便,如步骤2、4、5,对每个请求基本都是通用的,每次都写一遍很麻烦。为了解决这个问题,gin框架在标准库的基础上进行了一些封装。下面基于gin框架实现上述需求:
            
            
              go
              
              
            
          
          func main() {
    mux := gin.Default()
    mux.GET("/time", func(c *gin.Context) {
        m := map[string]string{
            "time": time.Now().Format("2006-01-02"),
        }
        c.JSON(http.StatusOK, m)
    })
    err := mux.Run("0.0.0.0:8080")
    if err != nil {
        panic(err)
    }
}
        基于gin开发的一般流程可总结为:
- 创建gin.Engine、注册middleware
 - 注册路由,编写处理函数,在函数内通过gin.Context获取参数,进行逻辑处理,通过gin.Context暴露的方法(如JSON())写回输出
 - 监听端口
 
可以看到简洁了很多,不用再关注响应内容的序列化和状态码问题了。
gin框架自身也是基于标准库net/http开发的,很多功能实现都是在标准库的基础上进行的封装,因此本文在剖析gin框架的过程中,点到为止,不会过多的对标准库的细节进行说明(后续会专门学习标准库的源码)。
3. 路由注册流程
3.1 核心数据结构
从案例中可知,使用gin开发前需要先调用gin.Default()函数,该函数返回一个*gin.Engine对象,该对象就是gin中的一个核心对象。
            
            
              scss
              
              
            
          
          func Default() *Engine {
    debugPrintWARNINGDefault()
    engine := New()
    engine.Use(Logger(), Recovery())
    return engine
}
        其实是先调用New方法创建了Engine对象,再调用Use注册middleware,这里先忽略。
(1) gin.Engine
            
            
              go
              
              
            
          
          func New() *Engine {
  ...
    engine := &Engine{
        // NOTE: 实例化RouteGroup,路由管理相关(Engine自身也是一个RouterGroup)
        RouterGroup: RouterGroup{
            Handlers: nil,
            basePath: "/",
            root:     true,
        },
        ...
        // NOTE: 负责存储路由和处理方法的映射,采用类似字典树的结构(这里构造了几棵树,每棵树对应一个http请求方法)
        trees:            make(methodTrees, 0, 9),
        ...
    }
  ...
    // NOTE: 基于sync.Pool实现的context池,能够避免context频繁销毁和重建
    engine.pool.New = func() any {
        return engine.allocateContext(engine.maxParams)
    }
    return engine
}
        该结构中包含三个核心对象:
- RouterGroup: 路由组,和路由管理相关
 - 路由树数组trees: 标准库本身的路由是不区分请求方法的,也就是说注册一个路由后,GET、POST都能匹配到该路由。这显然不是我们想要的,我们希望的是同一个路由在不同的请求方法下,由不同的逻辑进行处理。其实就是通过路由树实现的,gin的针对每个请求方法都有一棵路由树
 - context对象池: gin.Context是gin框架暴露给开发的另一个核心对象,可以通过该对象获取请求信息,业务处理的结果也是通过该对象写回客户端的。为了实现context对象的复用,gin基于sync.Pool实现了对象池
 
如果了解golang的http标准库,应该知道: http.ListenAndServe函数的第二个参数是一个接口类型,只要实现了该接口的ServeHTTP(ResponseWriter, *Request)方法,就能够对请求进行自定义处理。
            
            
              go
              
              
            
          
          type Handler interface {
    ServeHTTP(ResponseWriter, *Request)
}
        gin.Engine对象其实就是该接口的一个实现,因为它实现了该方法。至于具体处理过程,后续会详细说明。
            
            
              scss
              
              
            
          
          func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
  ...
}
        (2) 路由组RouterGroup
路由组的目的是为了实现配置的复用。
比如有一组对food的请求: /food/add、/food/query、/food/update等,我们希望在注册路由时尽量简单(不要每次都写/food),并且与food相关的请求使用一组单独的middleware(与其他对象的请求隔离开),这时候就可以使用路由组。
下面是其定义:
            
            
              go
              
              
            
          
          type RouterGroup struct {
    // 路由组处理函数链,其下路由的函数链将结合路由组和自身的函数组成最终的函数链
    Handlers HandlersChain
    // 路由组的基地址,一般是其下路由的公共地址
    basePath string
    // 路由组所属的Engine,这里构成了双向引用
    engine *Engine
    // 该路由组是否位于根节点,基于RouterGroup.Group创建路由组时此属性为false
    root bool
}
        需要注意的是gin.Engine对象本身就是一个路由组。
(3) 处理器链 HandlersChain
上述路由组对象中有一个很重要的字段,即Handlers,用于收集该路由组下注册的middleware函数。在运行时,会按顺序执行HandlersChain中的注册的函数。
            
            
              go
              
              
            
          
          type HandlerFunc func(*Context)
// HandlersChain defines a HandlerFunc slice.
// NOTE: 路由处理函数链,运行时会根据索引先后顺序依次调用
type HandlersChain []HandlerFunc
        3.2 执行流程
一般情况下使用gin框架开发时使用默认的engine即可,因为相对于直接使用gin.New()创建Engine对象,它只是多注册了两个中间件。
下面是一般流程:
- 创建并初始化Engine对象
 - 注册middleware
 - 注册路由及处理函数
 - 服务端口监听