Gin Web框架记录

1.背景

Gin 是一个 golang 的 web 框架,封装比较优雅,API 友好,源码注释比较明确,具有快速灵活,容错方便等特点。它的主要作者是 ManuJavierBo-Yi Wu,2016 年发布第一个版本,目前是最受欢迎的开源 Go 框架。

根据 Round 22 results - TechEmpower Framework Benchmarks 对现存的 web 框架的排名,Gin 框架以 95900 分的成绩位列第 185 名,与之相对的,业界常用的其他 web 框架比如 Spring 仅有 24082 分,位列第 381 名,可见 Gin 的效率远高于 spring 框架。

考虑到采用 go 进行开发时,其他排名靠前的 web 框架的生态并不如 Gin 完善,因此采用 Gin 是一个不错的选择。Gin 包含的组件和支持的功能如下:

组件 功能
server 作为 server,监听端口,接受请求
router 路由和分组路由,可以把请求路由到对应的处理函数
middleware 支持中间件,对外部发过来的 http 请求经过中间件处理,再给到对应的处理函数。例如 http 请求的日志记录、请求鉴权(比如校验 token)、CORS 支持、CSRF 校验等
template engine 模板引擎,支持后端代码对 html 模板里的内容做渲染(render),返回给前端渲染好的 html
Crash-free 捕捉运行期处理 http 请求过程中的 panic 并且做 recover 操作,让服务一直可用
JSON validation 解析和验证 request 里的 JSON 内容,比如字段必填等。
Error management Gin 提供了一种简单的方式可以收集 http request 处理过程中的错误,最终中间件可以选择把这些错误写入到 log 文件、数据库或者发送到其它系统。
Middleware Extendtable 支持用户自定义中间件

2.安装与项目创建

要使用 Gin 框架上手进行后端开发,可以遵循以下步骤:

  1. 安装 Go 语言支持(Gin 目前需要 1.13 以上版本的 Go 环境)

    All releases - The Go Programming Language 下载对应系统的安装包(此处以 linux 为例),保存到服务器上,解压到/usr/local/go。

    解压后,输入 sudo nano /etc/profile 打开配置文件,在文件末尾添加路径 export PATH=$PATH:/usr/local/go/bin,保存退出后使用 source /etc/profile 使环境变量配置生效。

  2. 安装 Gin 框架

    在命令行输入 go get -u github.com/gin-gonic/gin,在全局下载安装 Gin 框架

  3. 拷贝初始模板与运行

    在命令行输入 curl https://raw.githubusercontent.com/gin-gonic/examples/master/basic/main.go > main.go 拷贝一个初始模板并使用 go run main.go 运行即可

3.技术原理

3.1 程序启动流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main

import (
"github.com/LearnGin/handler"
"github.com/LearnGin/middleware"
"github.com/gin-gonic/gin"
)
func main() {
// init gin with default configs
r := gin.Default()
// append custom middle-wares
middleware.RegisterMiddleware(r)
// register custom routers
handler.RegisterHandler(r)
// run the engine
r.Run()
}

结合上述 Gin 框架的主函数,我们可以梳理 Gin 后端程序的启动流程如下:

  1. 初始化 Gingin.Default() 执行 Gin 的初始化过程,默认的初始化包含两个中间件

    1
    2
    3
    4
    5
    6
    7
    // Default returns an Engine instance with the Logger and Recovery middleware already attached.
    func Default() *Engine {
    debugPrintWARNINGDefault()
    engine := New()
    engine.Use(Logger(), Recovery())
    return engine
    }

    1. Logger:日志中间件,将 Gin 的启动与响应日志输出到控制台;
    2. Recovery:恢复中间件,将 Gin 遇到的无法处理的请求按 HTTP 500 状态码返回。
  2. 注册中间件:本例的 middleware.RegisterMiddleware(r) 用于将项目中开发的中间件注册到 Gin Engine 上;RegisterMiddleware 的定义如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    package middleware

    import (
    "github.com/LearnGin/middleware/debug"
    "github.com/gin-gonic/gin"
    )

    func RegisterMiddleware(r *gin.Engine) {
    r.Use(debug.DebugMiddleWare())
    }

    其中,r.Use 负责将 gin.HandleFunc 类型函数注册为中间件。此处的 debug.DebugMiddleWare() 是本例开发的一个简易的自定义中间件,用于在实际的事件处理前,输出详细的请求信息;在实际的事件处理后,输出结果状态码。

  3. 注册事件处理:本例的 handler.RegisterHandler(r) 用于将项目中开发的对应于指定 URL 的事件处理函数注册到 Gin Engine 上;

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    package handler

    import (
    "github.com/LearnGin/handler/person"
    "github.com/gin-gonic/gin"
    )

    func RegisterHandler(r *gin.Engine) {
    r.Handle("GET", "/ping", PingHandler())
    r.Handle("POST", "/person/create", person.CreatePersonHandler())
    }

    gin.Engine 的 r.Handle 函数用于将事件处理函数注册到指定的 HTTP 方法 + 相对路径上。

  4. 启动 Ginr.Run() 负责启动 Gin Engine,开始监听请求并提供 HTTP 服务。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    func (engine *Engine) Run(addr ...string) (err error) {
    defer func() { debugPrintError(err) }()

    trustedCIDRs, err := engine.prepareTrustedCIDRs()
    if err != nil {
    return err
    }
    engine.trustedCIDRs = trustedCIDRs
    address := resolveAddress(addr)
    debugPrint("Listening and serving HTTP on %s\n", address)
    err = http.ListenAndServe(address, engine)
    return
    }

    可以看到,Engine.Run 函数主要是:

    1. 解析监听地址传参;
    2. 启动监听与服务。

    其中,最核心的监听与服务实质上是调用 Go 语言内置库 net/http 的 http.ListenAndServe 函数实现的。

    1
    2
    3
    4
    func ListenAndServe(addr string, handler Handler) error {
    server := &Server{Addr: addr, Handler: handler}
    return server.ListenAndServe()
    }

    该函数实例化 Sever,并调用其 ListenAndServe 函数实现监听与服务功能。

    注意:此时,输入的 Gin Engine 对象以 Handler 接口的对象的形式被传入给了 net/http 库的 Server 对象,作为后续 Serve 对象处理网络请求时调用的函数。

3.2 Gin 框架路由原理

Gin 框架使用的是定制版本的 httprouter,其路由注册和查询功能是通过维护一个基数树(Radix Tree)来实现的。基数树又称为 PAT 位树(Patricia Trie or crit bit tree),本质上是对字典树(Trie Tree)的进一步压缩。对于基数树的每个节点,如果该节点是唯一的子树的话,就和父节点合并。关于前缀树和基数树,具体可以参考这里的介绍:树- 前缀树(Trie Tree)图解基数树(RadixTree)

对于如下路由注册信息:

1
2
3
4
5
6
7
8
9
10
r := gin.Default()

r.GET("/", func1)
r.GET("/search/", func2)
r.GET("/support/", func3)
r.GET("/blog/", func4)
r.GET("/blog/:post/", func5)
r.GET("/about-us/", func6)
r.GET("/about-us/team/", func7)
r.GET("/contact/", func8)

Gin 会对 GET 方法给出一棵如下所示的路由树:

1
2
3
4
5
6
7
8
9
10
11
Priority   Path             Handle
9 \ *<1>
3 ├s nil
2 |├earch\ *<2>
1 |└upport\ *<3>
2 ├blog\ *<4>
1 | └:post nil
1 | └\ *<5>
2 ├about-us\ *<6>
1 | └team\ *<7>
1 └contact\ *<8>

其中,最右侧一列是指向路由节点对应的处理函数的指针,完整遍历该树即可输出对应于 GET 方法的路由表。类似于:post 的占位符和 s 的中间节点不存在对应处理函数。Gin 为每种请求方法单独维护路由树,并为各个数级别上的子节点分配优先级。一个节点的优先级在数值上等于其所有子节点中注册的句柄数,这样可以优先匹配被大多数路由路径包含的节点,同时使得最长的路径被优先匹配以抵消其较长的搜索时间。

Gin 维护的路由基数树数据结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// tree.go

type node struct {
// 节点路径,比如上面的s,earch,和upport
path string
// 和children字段对应, 保存的是分裂的分支的第一个字符
// 例如search和support, 那么s节点的indices对应的"eu"
// 代表有两个分支, 分支的首字母分别是e和u
indices string
// 儿子节点
children []*node
// 处理函数链条(切片)
handlers HandlersChain
// 优先级,子节点、子子节点等注册的handler数量
priority uint32
// 节点类型,包括static, root, param, catchAll
// static: 静态节点(默认),比如上面的s,earch等节点
// root: 树的根节点
// catchAll: 有*匹配的节点
// param: 参数节点
nType nodeType
// 路径上最大参数个数
maxParams uint8
// 节点是否是参数节点,比如上面的:post
wildChild bool
// 完整路径
fullPath string
}
type methodTree struct {
method string
root *node
}
type methodTrees []methodTree // slice

func (trees methodTrees) get(method string) *node { // 返回对应方法的基数树
for _, tree := range trees {
if tree.method == method {
return tree.root
}
}
return nil
}

3.3 Gin 框架中间件原理

在程序启动一节我们已经简要说明了如何向 Gin 引擎注册中间件,这里我们进一步说明中间件的注册和执行原理。

Engine 的 Use 方法调用的是 RouterGroup 的 Use 函数,另外加上了两个处理资源不存在情况的 404 和 405 错误处理中间件:

1
2
3
4
5
6
func (engine *Engine) Use(middleware ...HandlerFunc) IRoutes {
engine.RouterGroup.Use(middleware...) // 实际上还是调用的RouterGroup的Use函数
engine.rebuild404Handlers()
engine.rebuild405Handlers()
return engine
}

而在 RouterGroup 中的 Use 方法实际上是将中间件函数追加到了 group.Handlers 列表中:

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

注册路由时,Gin 会将路由处理函数和中间件全部组合成一个处理函数链(HandlersChain),这是由 HandlerFunc 组成的切片:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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()
}

const abortIndex int8 = math.MaxInt8 / 2

func (group *RouterGroup) combineHandlers(handlers HandlersChain) HandlersChain {
finalSize := len(group.Handlers) + len(handlers)
if finalSize >= int(abortIndex) { // 这里有一个最大限制
panic("too many handlers")
}
mergedHandlers := make(HandlersChain, finalSize)
copy(mergedHandlers, group.Handlers)
copy(mergedHandlers[len(group.Handlers):], handlers)
return mergedHandlers
}

// 处理函数链
type HandlersChain []HandlerFunc

中间件和路由处理函数的执行是通过 c.Next() 方法触发的,其中 c 代表 Context 对象。该方法循环遍历 HandlersChain,依次执行每个 HandlerFunc

1
2
3
4
5
6
7
func (c *Context) Next() {
c.index++
for c.index < int8(len(c.handlers)) {
c.handlers[c.index](c)
c.index++
}
}

注册到函数链条的函数可以顺序执行,也可以嵌套执行。如下图所示:

image

在执行一个调用时,通过在该调用内部使用 c.Next 方法可以跳跃至后一个函数调用中;两个函数调用可以通过 c.Set 和 c.Get 两种方法传递数据,如下图所示:

image

4.示例

4.1 使用自定义中间件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
func Logger() gin.HandlerFunc {
return func(c *gin.Context) {
t := time.Now() // 记录当前时间

// 设置示例变量
c.Set("example", "12345")

// 在请求之前
c.Next()

// 在请求之后
latency := time.Since(t) // 计算请求处理时间
log.Print(latency) // 打印请求处理时间

// 获取发送的状态
status := c.Writer.Status()
log.Println(status) // 打印状态码
}
}

func main() {
r := gin.New()
r.Use(Logger()) // 使用自定义的日志中间件

r.GET("/test", func(c *gin.Context) {
example := c.MustGet("example").(string)

// 输出:"12345"
log.Println(example) // 打印示例变量
})

// 监听并在 0.0.0.0:8080 上提供服务
r.Run(":8080")
}

4.2 Restful后端服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
package main

import (
"net/http"

"github.com/gin-gonic/gin"
)

// album 代表唱片专辑的数据。
type album struct {
ID string `json:"id"`
Title string `json:"title"`
Artist string `json:"artist"`
Price float64 `json:"price"`
}

// albums 切片用于存储唱片专辑的数据。
var albums = []album{
{ID: "1", Title: "Blue Train", Artist: "John Coltrane", Price: 56.99},
{ID: "2", Title: "Jeru", Artist: "Gerry Mulligan", Price: 17.99},
{ID: "3", Title: "Sarah Vaughan and Clifford Brown", Artist: "Sarah Vaughan", Price: 39.99},
}

func main() {
router := gin.Default()
router.GET("/albums", getAlbums)
router.GET("/albums/:id", getAlbumByID)
router.POST("/albums", postAlbums)

router.Run("localhost:8080")
}

// getAlbums 以 JSON 格式回复所有唱片专辑的列表。
func getAlbums(c *gin.Context) {
c.IndentedJSON(http.StatusOK, albums)
}

// postAlbums 从请求的 JSON 数据中添加一个新的专辑。
func postAlbums(c *gin.Context) {
var newAlbum album

// 使用 BindJSON 方法将接收到的 JSON 数据绑定到 newAlbum 变量。
if err := c.BindJSON(&newAlbum); err != nil {
return
}

// 将新的专辑添加到切片中。
albums = append(albums, newAlbum)
c.IndentedJSON(http.StatusCreated, newAlbum)
}

// getAlbumByID 根据客户端发送的 id 参数,定位 ID 值与之匹配的唱片专辑,然后将其作为响应返回。
func getAlbumByID(c *gin.Context) {
id := c.Param("id")

// 遍历唱片专辑列表,寻找 ID 值与参数匹配的专辑。
for _, a := range albums {
if a.ID == id {
c.IndentedJSON(http.StatusOK, a)
return
}
}
c.IndentedJSON(http.StatusNotFound, gin.H{"message": "album not found"})
}