学习笔记|Gin

本文探讨 Gin 的一些用法和源码。

1. Quickstart

1.1 Gin 是什么

一个用 Go (Golang) 编写的 HTTP Web 框架。

1.2 安装

1
go get -u github.com/gin-gonic/gin

1.3 hello word

1
2
3
4
5
6
7
8
9
10
11
12
13
package main

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

func main() {
r := gin.Default() // 实例化一个 gin 的 server 实例(结构体变量)。
r.GET("/ping", func(c *gin.Context) { // 路由 + 方法
c.JSON(200, gin.H{
"message": "pong",
})
})
r.Run(":8099") // 监听并在 0.0.0.0:8099 上启动服务
}
  • r.GET第一个参数是 路由地址,第二个参数是处理函数。
1
2
3
4
// 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)
}
  • gin.H 实际上就是键值对 map。
1
2
// H is a shortcut for map[string]interface{}
type H map[string]any

1.4 Default 和 New 初始化路由器的区别

1
2
3
router := gin.New()

router := gin.Default()

使用 Default 方法,会在 New 的基础上,增加两个中间件:Logger、Recovery。

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()) // 增加两个中间件:Logger、Recovery。
return engine
}

Logger、Recovery 作用分别为:

  • Logger:打印访问日志。
1
2
[GIN] 2022/06/17 - 18:03:53 | 404 |         646ns |       127.0.0.1 | GET      "/"
[GIN] 2022/06/17 - 18:04:02 | 200 | 1.556255ms | 127.0.0.1 | GET "/ping"
  • Recovery:如果程序中出现 panic,没加入 Recovery ,服务器会直接无响应;加入 Recovery,服务器会返回 HTTP 500 错误。

2.路由

2.1 路由分组

对于 URL 中的重复部分,可以使用 Group 进行路由分组。

例如:商品的增删改查,可以使用 “/goods” 进行分组。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func router() {
r := gin.Default()
r.GET("/goods/list", goodsList) // 每个 URL 都要添加 goods,繁杂。
r.POST("/goods/add", createGoods)
r.GET("/goods/3", goodsDetail)
r.Run()
}


func routerWithGroup() {
r := gin.Default()
routerGroup := r.Group("/goods") // 路由分组
{ // 将路由分组下的代码用括号括在一起,只是为了代码的易读性,可不加{}。
routerGroup.GET("/list", goodsList) // 访问:ip:8080/goods/list
routerGroup.POST("/add", createGoods)
routerGroup.GET("/3", goodsDetail)
}
r.Run()
}

func goodsDetail(context *gin.Context) {}
func createGoods(context *gin.Context) {}
func goodsList(context *gin.Context) {}

例如:不同版本的 API,使用 “/v1” 和 “v2” 进行分组。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func routerWithGroup() {
r := gin.Default()
v1 := r.Group("/v1") // v1 版本 API
{
v1.GET("/login", login) // 访问:ip:8080/v1/login
v1.POST("/submit", submit)
v1.GET("/read", read)
}

v2 := r.Group("/v2") // v2 版本 API
{
v2.GET("/login", login2)
v2.POST("/submit", submit2)
v2.GET("/read", read2)
}

r.Run()
}

2.2 获取 URL 参数

路由中使用 :id 来匹配参数,使用 *path 来匹配路径。

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
func main() {
r:= gin.Default()
goodsGroup := r.Group("/goods")
{
/*
URL: http://127.0.0.1:8080/goods/3
返回: {id":"3"}
*/
goodsGroup.GET("/:id", goodsDetail) // 加一个参数 id

/*
URL: http://127.0.0.1:8080/goods/3/Add
返回: {"action":"Add","id":"3"}
*/
goodsGroup.GET("/:id/:action", goodsAction) // 加两个参数 id 和 action

/*
URL: http://127.0.0.1:8080/goods/file/net/aimtao/www/home/goods/file/
返回: {"path":"/net/aimtao/www/home/goods/file/"}
*/
goodsGroup.GET("/file/*path", goodsFilePath) // : 只匹配到下一个 / 结束,* 直接匹配到最后
}
r.Run()
}

func goodsDetail(context *gin.Context) {
id := context.Param("id")
context.JSON(http.StatusOK, gin.H{
"id": id,
})
}

func goodsAction(context *gin.Context) {
id := context.Param("id")
action := context.Param("action")
context.JSON(http.StatusOK, gin.H{
"id": id,
"action": action,
})
}

func goodsFilePath(context *gin.Context) {
path := context.Param("path")
context.JSON(http.StatusOK, gin.H{
"path": path,
})
}

注意匹配冲突,举个例子:

1
2
3
// 两个路由会冲突,输入 `http://127.0.0.1:8080/goods/list`,程序无法确认选择路由。
goodsGroup.GET("/list", goodsList)
goodsGroup.GET("/:id", goodsDetail)

2.3 URL 参数的表单验证

先对结构体变量用 tag 标记,形成约束条件。将 URL 中获取的参数,用 context.ShouldBindUri 进行验证该约束条件。

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
type Person struct{
// uri:"id" 表示 Id 从 URL 参数 id 中取,参数 id 必须为 int 类型。
// binding 表示该参数的约束,required 为必选参数。
Id int `uri:"id" binding:"required"`

// uri:"name" 表示 Name 从 URL 参数 name 中取,参数 name 必须为 string 类型。
// binding 表示该参数的约束,required 为必选参数。
Name string `uri:"name" binding:"required"`
}

func main() {
r:= gin.Default()
r.GET("/:id/:name", func(context *gin.Context) {

var p Person
if err := context.ShouldBindUri(&p); err != nil { // 从 context 中获取参数,进行验证后,赋值给 p。
context.Status(http.StatusBadRequest)
return
}
fmt.Println(p)
context.JSON(http.StatusOK, gin.H{
"id": p.Id,
"name": p.Name,
})
})
r.Run()
}

context.ShouldBindUri 的函数实现:从 context 中获取参数,进行验证后,赋值给 obj。

1
2
3
4
5
6
7
8
// ShouldBindUri binds the passed struct pointer using the specified binding engine.
func (c *Context) ShouldBindUri(obj any) error {
m := make(map[string][]string)
for _, v := range c.Params {
m[v.Key] = []string{v.Value}
}
return binding.Uri.BindUri(m, obj)
}

2.4 获取 GET/POST 参数

GET 参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func main() {
r:= gin.Default()
r.GET("/person", person)
r.Run()
}

func person(context *gin.Context) {
id := context.Query("id") // 获取 GET 参数 id
name := context.DefaultQuery("name", "anonymous") // 获取 GET 参数 name,如果不存在,则使用默认值 anonymous。
context.JSON(http.StatusOK, gin.H{
"id": id,
"name": name,
})
}

/*
curl http://127.0.0.1:8080/person?id=1&name=hh
{"id":"1","name":"hh"}
*/

POST 参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func main() {
r:= gin.Default()
r.POST("/person", person)
r.Run()
}

func person(context *gin.Context) {
id := context.PostForm("id") // 获取 POST 参数 id
name := context.DefaultPostForm("name", "anonymous") // 获取 POST 参数 name,如果不存在,则使用默认值 anonymous。
context.JSON(http.StatusOK, gin.H{
"id": id,
"name": name,
})
}

/*
curl -X POST -d "id=1&name=hh" http://127.0.0.1:8080/person
{"id":"1","name":"hh"}
*/

GET 和 POST 混合参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func main() {
r:= gin.Default()
r.POST("/person", person)
r.Run()
}

func person(context *gin.Context) {
id := context.Query("id") // 获取 GET 参数
name := context.DefaultPostForm("name", "anonymous") // 获取 POST 参数
context.JSON(http.StatusOK, gin.H{
"id": id,
"name": name,
})
}
/*
curl -X POST -d "name=hh" http://127.0.0.1:8080/person?id=1
{"id":"1","name":"hh"}
*/

2.5 返回 JSON 和 ProtoBuf

除了使用 gin.H 返回 JSON,还可以返回整个 struct 作为 JSON。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func main() {
r:= gin.Default()
r.GET("/JSON", returnJSON)
r.Run()
}

func returnJSON(context *gin.Context) {
var person struct{
Name string `json:"name"`
Age int `json:"age"`
}
person.Name = "h"
person.Age = 10
context.JSON(http.StatusOK, person) // 返回整个 struct
}

/*
curl http://127.0.0.1:8080/JSON
{"name":"h","age":10}
*/

先定义 proto 文件,在生成处理 protobuf 的 bg.go 文件,其中包含 struct 的定义。初始化该 struct,并使用 context.ProtoBuf 返回即可。

注意:此处需传递实例化的 struct 地址。

为什么 context.ProtoBuf 需要传地址,而 context.JSON 不需要?

  • context.JSON 需要接收一个结构体,并将其处理为字符串即可。
  • context.ProtoBuf 需要接收一个结构体指针,用这个指针去调用 protoreflect.ProtoMessage 方法。(bp.go 中,该结构体实现了 ProtoMessage 接口。)最终转化为 protobuf 格式。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
func main() {
r:= gin.Default()
r.GET("/Protobuf", returnProtobuf)
r.Run()
}

func returnProtobuf(context *gin.Context) {
course := []string{"go","rpc", "gin"}
p := proto.Person{
Name: "hh",
Course: course,
}
context.ProtoBuf(http.StatusOK, &p) // 注意:此处需传递实例化的 struct 地址
}

2.6 使用 validator 进行表单验证

gin 使用 go-playground/validator 验证参数。gin 将其封装成两套绑定方法。

  • Must bind

    • Methods:BindBindJSONBindXMLBindQueryBindYAML
    • Behavior:这些方法底层使用 MustBindWith,如果验证错误,请求会被 c.AbortWithError(http.StatusBadRequest, err).SetType(ErrorTypeBind) 中止请求,响应状态码会设置为 400。(如果想自定义处理错误和请求响应,应使用 ShouldBind)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // MustBindWith binds the passed struct pointer using the specified binding engine.
    // It will abort the request with HTTP 400 if any error occurs.
    // See the binding package.
    func (c *Context) MustBindWith(obj any, b binding.Binding) error {
    if err := c.ShouldBindWith(obj, b); err != nil {
    c.AbortWithError(http.StatusBadRequest, err).SetType(ErrorTypeBind) //nolint: errcheck
    return err
    }
    return nil
    }
  • Should bind

    • Methods:ShouldBindShouldBindJSON`、ShouldBindXML、``ShouldBindQuery、``ShouldBindYAML`
    • Behavior:这些方法底层使用 ShouldBindWith,如果验证错误,则返回错误,由开发人员自定义处理错误和请求响应。

当调用 BindShouldBind 两种方法时,Gin 会根据求的 Method 和 Content-Type 推断使用哪种绑定器,然后使用 ShouldBindWith(obj any, b binding.Binding)BindWith(obj any, b binding.Binding)。具体如何调用及推断的逻辑如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
// ShouldBind checks the Method and Content-Type to select a binding engine automatically,
// Depending on the "Content-Type" header different bindings are used, for example:
//
// "application/json" --> JSON binding
// "application/xml" --> XML binding
//
// It parses the request's body as JSON if Content-Type == "application/json" using JSON or XML as a JSON input.
// It decodes the json payload into the struct specified as a pointer.
// Like c.Bind() but this method does not set the response status code to 400 or abort if input is not valid.
func (c *Context) ShouldBind(obj any) error {
b := binding.Default(c.Request.Method, c.ContentType()) // 推断使用哪种绑定器
return c.ShouldBindWith(obj, b)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Content-Type MIME of the most common data formats.
const (
MIMEJSON = "application/json"
//...

// Default returns the appropriate Binding instance based on the HTTP method
// and the content type.
func Default(method, contentType string) Binding {
if method == http.MethodGet { // GET 方法使用 ShouldBindForm 方法
return Form
}

switch contentType { // 根据 contentType 判断使用哪种绑定器
case MIMEJSON:
return JSON
case MIMEXML, MIMEXML2:
//...

举个例子: 对注册的表单进行验证。

  • SignUpForm 使用 tag 配置约束条件。
  • 使用 context.ShouldBind(&signUpForm) 进行表单验证。
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
import (
"fmt"
"net/http"

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

type SignUpForm struct {
Username string `json:"username" form:"username" binding:"required,max=10"` // 约束条件:必填、最大长度10
Password string `json:"password" form:"password" binding:"required,min=6"` // 约束条件:必填、最小长度6
RePassword string `json:"repassword" form:"repassword" binding:"required,eqfield=Password"` // 约束条件:必填、等于 Password 字段
Age int `json:"age" form:"age" binding:"required"` // 约束条件:必填
Email string `json:"email" form:"email" binding:"required,email"` // 约束条件:必填、检查是否是邮箱
}

func main() {
engine := gin.Default()
engine.POST("SignUp", Login)
engine.Run(":8083")
}

func Login(context *gin.Context) {
var signUpForm SignUpForm

// 表单验证,验证不通过,ShouldBind 会返回 error。
// ShouldBind 会根据请求的 Method 和 Content-Type,自动选择 JSON、XML、Query 等等,此处会调用 ShouldBindJSON
if err := context.ShouldBind(&signUpForm); err != nil {
context.JSON(http.StatusBadRequest, gin.H{
"error": err.Error(),
})
return // 验证错误记得 return,否则会在继续往下执行。
}

context.JSON(http.StatusOK, gin.H{
"msg": "signup success",
"user": signUpForm.Username,
})
}

当有不符合条件的访问时,会返回 400 及错误原因。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
## Request
curl -X "POST" "http://127.0.0.1:8083/SignUp" \
-H 'Content-Type: application/json; charset=utf-8' \
-d $'{
"username": "Tom",
"password": "123456",
"repassword": "12345", # 重复密码不一致
"age": 18,
"email": "123@123.com"

}'


# 返回错误
HTTP/1.1 400 Bad Request
Content-Length: 108
Content-Type: text/plain; charset=utf-8
Connection: close

{"error":"Key: 'SignUpForm.RePassword' Error:Field validation for 'RePassword' failed on the 'eqfield' tag"}

2.7 为 validator 配置翻译器

  1. 初始化语言环境
  2. 传入 语言环境,初始化通用翻译器
  3. 从通用翻译器中拿出中文翻译器
  4. 绑定中文验证器和翻译器
  5. 将验证器返回的 Error 转换为 validator.ValidationErrors,这样 Error 就有了 Translate 方法
  6. 使用 Translate 方法

具体流程参照代码阅读。

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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
import (
"fmt"
"net/http"

"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
"github.com/go-playground/locales/en"
"github.com/go-playground/locales/zh"
ut "github.com/go-playground/universal-translator"
"github.com/go-playground/validator/v10"
en2 "github.com/go-playground/validator/v10/translations/en"
zh2 "github.com/go-playground/validator/v10/translations/zh"
)

type SignUpForm struct {
Username string `json:"username" form:"username" binding:"required,max=10"` // 约束条件:必填、最大长度10
Password string `json:"password" form:"password" binding:"required,min=6"` // 约束条件:必填、最小长度6
RePassword string `json:"repassword" form:"repassword" binding:"required,eqfield=Password"` // 约束条件:必填、等于 Password 字段
Age int `json:"age" form:"age" binding:"required"` // 约束条件:必填
Email string `json:"email" form:"email" binding:"required,email"` // 约束条件:必填、检查是否是邮箱
}

var translator ut.Translator // 定义全局的翻译器

func main() {
engine := gin.Default()
err := InitTrans("zh") // 初始化中文翻译器
if err != nil {
panic(err)
}
engine.POST("SignUp", Login)
engine.Run(":8083")
}


func InitTrans(locale string) (err error) {
translatorZh := zh.New() // 中文环境 //【1.初始化中文翻译器】
translatorEn := en.New() // 英文环境
uni := ut.New(translatorZh, translatorEn, translatorZh) // 【2.传入语言环境,初始化通用翻译器】 // 通用翻译器保存所有的语言环境和翻译数据。第一个参数是备用的语言环境,后面是应支持的语言环境
var found bool
translator, found = uni.GetTranslator(locale) // 【3.根据 locale 拿到最终的翻译器】
if !found {
return fmt.Errorf("uni.GetTranslator(%s)", locale)
}

// 拿到 gin 的验证器
validate := binding.Validator.Engine().(*validator.Validate)

// 【4.根据 locale,绑定验证器和翻译器】
switch locale {
case "en":
err = en2.RegisterDefaultTranslations(validate, translator)
case "zh":
err = zh2.RegisterDefaultTranslations(validate, translator)
default:
err = en2.RegisterDefaultTranslations(validate, translator)
}

return err
}

func Login(context *gin.Context) {
var signUpForm SignUpForm

// 表单验证,验证不通过,ShouldBind 会返回 error。
// ShouldBind 会根据请求的 Method 和 Content-Type,自动选择 JSON、XML、Query 等等,此处会调用 ShouldBindJSON
if err := context.Bind(&signUpForm); err != nil {
errors := err.(validator.ValidationErrors) // 【5.类型转换为 validator.ValidationErrors】
context.JSON(http.StatusOK, gin.H{
"error": errors.Translate(translator), // 【6.使用 Translate 方法】
})
return
}

context.JSON(http.StatusOK, gin.H{
"msg": "signup success",
"user": signUpForm.Username,
})
}

当有不符合条件的访问时,错误原因被翻译成中文。

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
## Request
curl -X "POST" "http://127.0.0.1:8083/SignUp" \
-H 'Content-Type: application/json; charset=utf-8' \
-d $'{
"username": "Tom",
"password": "123456",
"repassword": "12345", # 重复密码不一致
"age": 18,
"email": "123.com" # 邮箱格式错误

}'


# 返回错误
HTTP/1.1 400 Bad Request
Content-Length: 108
Content-Type: text/plain; charset=utf-8
Connection: close

{
"error":{
"SignUpForm.Email":"Email必须是一个有效的邮箱",
"SignUpForm.RePassword":"RePassword必须等于Password"
}
}

2.8 更改验证错误字段格式

上述错误原因虽然是中文的,但是字段格式不是 json 格式的。期望返回的是这样的格式:

1
2
3
4
5
6
{
"error":{
"email":"email必须是一个有效的邮箱",
"repassword":"repassword必须等于password"
}
}

主要需要做两件事:

  • 验证时,字段使用 tag 配置中的 json 格式。

    在拿到 gin 的验证器时,注册一个获取 Tag Name 的方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // 拿到 gin 的验证器
    validate := binding.Validator.Engine().(*validator.Validate)
    validate.RegisterTagNameFunc(func(field reflect.StructField) string { // 注册一个获取 Tag Name 的方法
    name := strings.SplitN(field.Tag.Get("json"), ",", 2)[0] // 获取 tag 中的 json 配置。
    if name == "-" { // 如果 tag 中的 json 配置为 - 时,说明配置为 空字符串。
    return " "
    }
    return name
    })
  • 去掉 struct 名称。

    errors.Translate(translator) 返回的是 ValidationErrorsTranslations 类型,其实就是 map[string]string 类型。我们要进行字符串操作的就是这个 map 的 key。

    1
    2
    3
    4
    5
    6
    7
    8
    // 在验证请求时
    if err := context.Bind(&signUpForm); err != nil {
    errors := err.(validator.ValidationErrors)
    context.JSON(http.StatusOK, gin.H{
    "error": removeTopStruct(errors.Translate(translator)), // 处理 map 的 key。
    })
    return
    }

    如何处理?找到点的位置,进行切片操作。

    1
    2
    3
    4
    5
    6
    7
    func removeTopStruct(fields map[string]string) map[string]string {
    rsp := map[string]string{}
    for field, err := range fields {
    rsp[field[strings.Index(field, ".")+1:]] = err // 只要点后面的字符串:找到点的位置,进行切片操作
    }
    return rsp
    }

3.中间件

3.1 自定义中间件

中间件可以避免代码的侵入性。gin 的中间件使用很简便,只需提供一个 gin.HandlerFunc 类型的函数即可。

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
// 用于计时的中间件
func timing() gin.HandlerFunc {
return func(c *gin.Context) {
t := time.Now()
c.Set("number", "1233")

c.Next() // 执行真正的请求处理函数

end := time.Since(t)
fmt.Println("Duration: ", end)
fmt.Println("Writer status: ", c.Writer.Status())
}
}

func main() {
engine := gin.New() // gin.Default() 默认使用 Logger、Recovery 两个中间件
engine.Use(gin.Logger(), gin.Recovery(), timing()) // 使用计时的中间件
engine.GET("/index", index)
engine.Run(":8083")
}

func index(c *gin.Context) {
if value, exists := c.Get("number"); exists {
time.Sleep(time.Second)
c.JSON(http.StatusOK, gin.H{
"number": value,
})
}
}

3.2 中间件队列原理

如图,RouterGroup 中维护了一个队列 Handlers,并将中间件及请求处理函数依次加入其中。处理请求时,依次执行队列中的函数,并使用 index 指向正在执行的函数。

  • c.Next():表示 index ++,开始执行队列中后面的函数。
  • c.Abort():表示中止队列函数的执行。所以在中间件中,使用 return 并不能中止队列函数的执行,只能结束当前函数,index 还是会向后移动,并执行后面的函数。

我们来具体看下源码。

RouterGroup 中,维护一个函数数组 Handlers。这里的 HandlersChain 类型就是 func(*Context)

1
2
3
4
5
6
7
// routergroup.go
type RouterGroup struct {
Handlers HandlersChain
basePath string
engine *Engine
root bool
}
1
2
3
// gin.go
type HandlerFunc func(*Context)
type HandlersChain []HandlerFunc

当需要加入中间件时,gin.Use -> RouterGroup.Use,将中间件加入队列。

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

当需要加入请求处理函数时,gin.GET -> RouterGroup.handle -> RouterGroup.combineHandlers,创建一个新的队列,将原有的中间件和请求处理函数拷贝到其中。

1
2
3
4
5
6
7
8
9
10
11
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
}

4.其他

4.1 模版功能

https://pkg.go.dev/html/template

处理请求时,gin 返回模版文件+变量数据。

(1)最小实践

1
2
3
4
.
├── main.go
└── templates
└── index.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// main.go

func main() {
router := gin.Default()
router.LoadHTMLFiles("templates/index.html") // 相对路径加载 HTML 文件。

router.GET("/index", func(context *gin.Context) {
context.HTML(http.StatusOK, "index.html", gin.H{ // 将 title 传给 index.html
"title": "haha",
})
})

router.Run(":8080")
}
1
2
3
4
5
6
7
8
9
10
11
12
<!--templates/index.html-->

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Hello World</title>
</head>
<body>
<h1>{{ .title }}</h1> <!-- 使用 gin 传递过来的 title。注意格式 {{.变量名}} -->
</body>
</html>

注意相对路径问题:

  • 上述代码中,使用 "templates/index.html" 相对路径,直接在 IDE 中使用 go run 无法获取到相当路径,需要在当前路径下 go build 出可执行文件,再执行。
  • 原因:go run 执行时,所在的目录是临时目录。
1
2
3
4
5
dir, _ := filepath.Abs(filepath.Dir(os.Args[0]))
fmt.Println("dir: ", dir)

// output:
// dir: /private/var/folders/hr/2w_2q41n7mvb9jv1vrjsrzmh0000gn/T/GoLand // 临时目录

(2)同时加载多个 HTML

文件过多,可以使用 LoadHTMLGlob 加载整个目录。

1
2
3
4
5
.
├── main.go
└── templates
├── goods.html
└── index.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func main() {
router := gin.Default()
router.LoadHTMLGlob("templates/*") // 加载整个目录下所有文件
// router.LoadHTMLFiles("templates/index.html", "templates/goods.html") // 可变参数列表,一个一个加载

router.GET("/index", func(context *gin.Context) {
context.HTML(http.StatusOK, "index.html", gin.H{ // 将 title 传给 index.html
"title": "haha",
})
})

router.GET("/goods", func(context *gin.Context) {
context.HTML(http.StatusOK, "goods.html", gin.H{ // 将 title 传给 goods.html
"title": "goods",
})
})

router.Run(":8080")
}

(3)处理文件名冲突问题

1
2
3
4
5
6
7
8
9
.
├── main.go
└── templates
├── goods
│   └── list.html
├── index
│   └── index.html
└── user
└── list.html
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
// main.go

func main() {
router := gin.Default()
router.LoadHTMLGlob("templates/**/*") // 加载 templates 下的所有二级目录(只加载二级目录)

router.GET("/index", func(context *gin.Context) {
context.HTML(http.StatusOK, "index/index.html", gin.H{ // 使用 /templates/index/index.html 中 define 的值
"title": "haha",
})
})

router.GET("/user/list", func(context *gin.Context) {
context.HTML(http.StatusOK, "user/list.html", gin.H{ // 使用 /templates/user/list.html 中 define 的值
"title": "users",
})
})

router.GET("/goods/list", func(context *gin.Context) {
context.HTML(http.StatusOK, "goods/list.html", gin.H{ // 使用 /templates/goods/list.html 中 define 的值
"title": "goods",
})
})

router.Run(":8080")
}

其中有几个问题需要说明一下:

  • router.LoadHTMLGlob("templates/**/*") 表示只能加载 templates 下的二级目录的所有内容,此时 templates 目录下的 html 文件是无法被加载的。

    • 解决:index.html 文件,也放在二级目录 index 目录下。
  • goods/list.htmluser/list.html 的 文件重名,在使用 context.HTML 时,均传入的 list.html,会导致其中一个会失效。

    • 解决:在 html 文件中,使用 define 定义文件名称。gin 中使用时,使用 define 的名称。
    • 优先找 define 的名称,没有 define 定义,使用默认的 LoadHTMLGlob 来找文件。

4.2 静态文件处理

html 中需要引用大量的静态文件,使用 router.Static("/static", "./static") 指明以 /static 开头的链接,均在 ./static 目录下找。

1
2
3
4
5
6
7
.
├── main.go
├── static
│   └── css
│   └── index.css
└── templates
└── index.html
1
2
3
4
5
6
7
8
9
10
11
12
13
// main.go
func main() {
router := gin.Default()
router.LoadHTMLGlob("templates/*")
router.Static("/static", "./static") // 以 /static 开头的链接,均在 ./static 目录下找
router.GET("/index", func(context *gin.Context) {
context.HTML(http.StatusOK, "index.html", gin.H{
"title": "haha",
})
})

router.Run(":8080")
}
1
2
3
4
5
6
7
8
9
10
11
12
<!-- /templates/index.html --> 
<!DOCTYPE html>
<link rel="stylesheet" href="/static/css/index.css"> <!-- 引用静态文件-->
<html lang="en">
<head>
<meta charset="utf-8">
<title>Hello World</title>
</head>
<body>
<h1>{{ .title }}</h1> <!-- 使用 gin 传递过来的 title。注意格式 {{.变量名}} -->
</body>
</html>
1
2
3
4
/*template/static/css/index.css*/
*{
background-color: burlywood;
}

4.3 gin 的优雅退出

https://gin-gonic.com/docs/examples/graceful-restart-or-stop/

优雅退出的作用:当关闭程序时,应该做一些后续处理。比如:将没保存的数据做保存、保存日志、向注册中心告知当前服务已下线等等。

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
import (
"fmt"
"net/http"
"os"
"os/signal"
"syscall"

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

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

go func() {
router.Run(":8080") // 在 goroutine 中监听,让主进程等待 channel 信号。
}()

quit := make(chan os.Signal)
// SIGINT 是 Crtl+C; SIGTERM 是 kill 命令不加参数。(kill -9 是 SIGKILL 无法捕获)
// 当收到 SIGINT、SIGTERM 信号时,像 channel 中写数据。
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)

<-quit // channel 收到数据,说明收到结束进程的信号。

// 处理退出逻辑,此处使用 print 模拟。
fmt.Println("Closing server...")
fmt.Println("Unregistering service...")
}

学习笔记|Gin
https://www.aimtao.net/gin/
Posted on
2022-08-25
Licensed under