gin框架简介

1.gin介绍

gin框架是golang中比较常见的web框架,截止到目前(2023-03-21),github上已经累计了67.3K的star数,这足以表明其优秀。作为一名想要知其然亦想知其所以然的程序员,希望通过学习gin框架的实现原理来提高自己的技术能力,也希望通过分享来帮助想要进行学习的同学。

2. 前置知识

其实golang本身的标准库已经足以实现简单的web服务,但是出于以下原因,使得直接使用标准库开发难以满足我们的需求:

  1. 标准库本身提供了比较简单的路由注册能力,只支持精确匹配,而实际开发时难免会遇到需要使用通配、路径参数的场景
  2. 标准库暴露给开发者的函数参数是(w http.ResponseWriter, req *http.Request),这就导致我们需要直接从请求中读取数据、反序列化,响应时手动序列化、设置Content-Type、写响应内容,比较麻烦
  3. 有时候我们希望能够在不过多地侵入业务的前提下,对请求或响应进行一些前置或后置处理。直接基于标准库开发,业务和非业务代码难免会耦合在一起

下面是直接使用标准库开发一个简单的获取当前时间的案例,当客户端以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服务的一般流程:

  1. 利用http.HandleFunc函数注册路由,并指定处理函数,函数签名为func(w http.ResponseWriter, r *http.Request)
  2. 在处理函数内部获取查询参数、路径参数、读取请求体并反序列化
  3. 业务逻辑处理
  4. 错误处理(设置错误响应状态码、错误信息等)
  5. 设置响应状态码、设置响应头(如Content-Type)、处理结果序列化并写入响应体
  6. 调用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开发的一般流程可总结为:

  1. 创建gin.Engine、注册middleware
  2. 注册路由,编写处理函数,在函数内通过gin.Context获取参数,进行逻辑处理,通过gin.Context暴露的方法(如JSON())写回输出
  3. 监听端口

可以看到简洁了很多,不用再关注响应内容的序列化和状态码问题了。

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对象,它只是多注册了两个中间件。

下面是一般流程:

  1. 创建并初始化Engine对象
  2. 注册middleware
  3. 注册路由及处理函数
  4. 服务端口监听
相关推荐
千慌百风定乾坤1 天前
Go 语言入门指南:基础语法和常用特性解析(下) | 豆包MarsCode AI刷题
青训营笔记
FOFO1 天前
青训营笔记 | HTML语义化的案例分析: 粗略地手绘分析juejin.cn首页 | 豆包MarsCode AI 刷题
青训营笔记
滑滑滑3 天前
后端实践-优化一个已有的 Go 程序提高其性能 | 豆包MarsCode AI刷题
青训营笔记
柠檬柠檬3 天前
Go 语言入门指南:基础语法和常用特性解析 | 豆包MarsCode AI刷题
青训营笔记
用户967136399653 天前
计算最小步长丨豆包MarsCodeAI刷题
青训营笔记
用户52975799354724 天前
字节跳动青训营刷题笔记2| 豆包MarsCode AI刷题
青训营笔记
clearcold4 天前
浅谈对LangChain中Model I/O的见解 | 豆包MarsCode AI刷题
青训营笔记
夭要7夜宵4 天前
【字节青训营】 Go 进阶语言:并发概述、Goroutine、Channel、协程池 | 豆包MarsCode AI刷题
青训营笔记
用户336901104445 天前
数字分组求和题解 | 豆包MarsCode AI刷题
青训营笔记
dnxb1235 天前
GO语言工程实践课后作业:实现思路、代码以及路径记录 | 豆包MarsCode AI刷题
青训营笔记