Quickstart

环境

vscode,go1.14.3,gin1.4.0

1[root@go ~]# cat /etc/profile|tail -5
2# go enviroment
3export GOROOT=/usr/local/go
4export GOPATH=~/golib:~/goproject
5export GOBIN=~/gobin
6export PATH=$PATH:$GOROOT/bin:$GOBIN

1. Gin 是什么

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

1.1. 安装

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

1.2. hello word

 1package main
 2
 3import "github.com/gin-gonic/gin"
 4
 5func main() {
 6    r := gin.Default()  // 实例化一个 gin 的 server 实例(结构体变量)。
 7    r.GET("/ping", func(c *gin.Context) {  // 路由 + 方法
 8        c.JSON(200, gin.H{
 9            "message": "pong",
10        })
11    })
12    r.Run(":8099") // 监听并在 0.0.0.0:8099 上启动服务
13}

r.GET第一个参数是 路由地址,第二个参数是处理函数。

1// GET is a shortcut for router.Handle("GET", path, handlers).
2func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes {
3    return group.handle(http.MethodGet, relativePath, handlers)
4}

gin.H 实际上就是键值对 map。

1// H is a shortcut for map[string]interface{}
2type H map[string]any

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

1router := gin.New()
2
3router := gin.Default()

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

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

Logger、Recovery 作用分别为:

  • Logger:打印访问日志。
1[GIN] 2022/06/17 - 18:03:53 | 404 |         646ns |       127.0.0.1 | GET      "/"
2[GIN] 2022/06/17 - 18:04:02 | 200 |    1.556255ms |       127.0.0.1 | GET      "/ping"

2. 路由

2.1. 路由分组

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

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

 1func router() {
 2    r := gin.Default()
 3    r.GET("/goods/list", goodsList)   // 每个 URL 都要添加 goods,繁杂。
 4    r.POST("/goods/add", createGoods)
 5    r.GET("/goods/3", goodsDetail)
 6    r.Run()
 7}
 8
 9
10func routerWithGroup() {
11    r := gin.Default()
12    routerGroup := r.Group("/goods")   // 路由分组
13    {  // 将路由分组下的代码用括号括在一起,只是为了代码的易读性,可不加{}。
14        routerGroup.GET("/list", goodsList)  // 访问:ip:8080/goods/list
15        routerGroup.POST("/add", createGoods)
16        routerGroup.GET("/3", goodsDetail)
17    }
18    r.Run()
19}
20
21func goodsDetail(context *gin.Context) {}
22func createGoods(context *gin.Context) {}
23func goodsList(context *gin.Context) {}

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

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

2.2. 获取 URL 参数

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

 1func main() {
 2    r:= gin.Default()
 3    goodsGroup := r.Group("/goods")
 4    {
 5        /*
 6           URL:  http://127.0.0.1:8080/goods/3
 7           返回:  {id":"3"}
 8        */
 9        goodsGroup.GET("/:id", goodsDetail)  // 加一个参数 id
10
11        /*
12           URL:  http://127.0.0.1:8080/goods/3/Add
13           返回:  {"action":"Add","id":"3"}
14        */
15        goodsGroup.GET("/:id/:action", goodsAction)  // 加两个参数 id 和 action
16
17        /*
18           URL:  http://127.0.0.1:8080/goods/file/net/aimtao/www/home/goods/file/
19           返回:  {"path":"/net/aimtao/www/home/goods/file/"}
20        */
21        goodsGroup.GET("/file/*path", goodsFilePath)  // : 只匹配到下一个 / 结束,* 直接匹配到最后
22    }
23    r.Run()
24}
25
26func goodsDetail(context *gin.Context) {
27    id := context.Param("id")
28    context.JSON(http.StatusOK, gin.H{
29        "id": id,
30    })
31}
32
33func goodsAction(context *gin.Context) {
34    id := context.Param("id")
35    action := context.Param("action")
36    context.JSON(http.StatusOK, gin.H{
37        "id": id,
38        "action": action,
39    })
40}
41
42func goodsFilePath(context *gin.Context) {
43    path := context.Param("path")
44    context.JSON(http.StatusOK, gin.H{
45        "path": path,
46    })
47}

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

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

2.3. URL 参数的表单验证

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

 1type Person struct{
 2    // uri:"id" 表示 Id 从 URL 参数 id 中取,参数 id 必须为 int 类型。
 3    // binding 表示该参数的约束,required 为必选参数。
 4    Id int `uri:"id" binding:"required"`
 5  
 6    // uri:"name" 表示 Name 从 URL 参数 name 中取,参数 name 必须为 string 类型。
 7    // binding 表示该参数的约束,required 为必选参数。
 8    Name string `uri:"name" binding:"required"`
 9}
10
11func main() {
12    r:= gin.Default()
13    r.GET("/:id/:name", func(context *gin.Context) {
14
15        var p Person
16        if err := context.ShouldBindUri(&p); err != nil {  // 从 context 中获取参数,进行验证后,赋值给 p。
17            context.Status(http.StatusBadRequest)
18            return
19        }
20        fmt.Println(p)
21        context.JSON(http.StatusOK, gin.H{
22            "id": p.Id,
23            "name": p.Name,
24        })
25    })
26    r.Run()
27}

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

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

2.4. 获取 GET/POST 参数

GET 参数。

 1func main() {
 2    r:= gin.Default()
 3    r.GET("/person", person)
 4    r.Run()
 5}
 6
 7func person(context *gin.Context) {
 8    id := context.Query("id")  // 获取 GET 参数 id
 9    name := context.DefaultQuery("name", "anonymous")  // 获取 GET 参数 name,如果不存在,则使用默认值 anonymous。
10    context.JSON(http.StatusOK, gin.H{
11        "id": id,
12        "name": name,
13    })
14}
15
16/*
17    curl http://127.0.0.1:8080/person?id=1&name=hh
18    {"id":"1","name":"hh"}
19*/

POST 参数。

 1func main() {
 2    r:= gin.Default()
 3    r.POST("/person", person)
 4    r.Run()
 5}
 6
 7func person(context *gin.Context) {
 8    id := context.PostForm("id")  // 获取 POST 参数 id
 9    name := context.DefaultPostForm("name", "anonymous")  // 获取 POST 参数 name,如果不存在,则使用默认值 anonymous。
10    context.JSON(http.StatusOK, gin.H{
11        "id": id,
12        "name": name,
13    })
14}
15
16/*
17    curl -X POST -d "id=1&name=hh" http://127.0.0.1:8080/person
18    {"id":"1","name":"hh"}
19*/

GET 和 POST 混合参数。

 1func main() {
 2    r:= gin.Default()
 3    r.POST("/person", person)
 4    r.Run()
 5}
 6
 7func person(context *gin.Context) {
 8    id := context.Query("id")  // 获取 GET 参数
 9    name := context.DefaultPostForm("name", "anonymous")  // 获取 POST 参数
10    context.JSON(http.StatusOK, gin.H{
11        "id": id,
12        "name": name,
13    })
14}
15/*
16    curl -X POST -d "name=hh" http://127.0.0.1:8080/person?id=1
17    {"id":"1","name":"hh"}
18*/

2.5. 返回 JSON 和 ProtoBuf

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

 1func main() {
 2    r:= gin.Default()
 3    r.GET("/JSON", returnJSON)
 4    r.Run()
 5}
 6
 7func returnJSON(context *gin.Context) {
 8    var person struct{
 9        Name string `json:"name"`
10        Age int `json:"age"`
11    }
12    person.Name = "h"
13    person.Age  = 10
14    context.JSON(http.StatusOK, person)  // 返回整个 struct
15}
16
17/*
18    curl http://127.0.0.1:8080/JSON
19    {"name":"h","age":10}
20*/

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

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

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

  • context.JSON 需要接收一个结构体,并将其处理为字符串即可。
  • context.ProtoBuf 需要接收一个结构体指针,用这个指针去调用 protoreflect.ProtoMessage 方法。(bp.go 中,该结构体实现了 ProtoMessage 接口。)最终转化为 protobuf 格式。
 1func main() {
 2    r:= gin.Default()
 3    r.GET("/Protobuf", returnProtobuf)
 4    r.Run()
 5}
 6
 7func returnProtobuf(context *gin.Context) {
 8    course := []string{"go","rpc", "gin"}
 9    p := proto.Person{
10        Name: "hh",
11        Course: course,
12    }
13    context.ProtoBuf(http.StatusOK, &p)  // 注意:此处需传递实例化的 struct 地址
14}

2.6. 使用 validator 进行表单验证

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

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

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

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

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

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

  • SignUpForm 使用 tag 配置约束条件。
  • 使用 context.ShouldBind(&signUpForm) 进行表单验证。
 1import (
 2    "fmt"
 3    "net/http"
 4  
 5    "github.com/gin-gonic/gin"
 6)
 7
 8type SignUpForm struct {
 9    Username string `json:"username" form:"username" binding:"required,max=10"`  // 约束条件:必填、最大长度10
10    Password string `json:"password" form:"password" binding:"required,min=6"`  // 约束条件:必填、最小长度6
11    RePassword string `json:"repassword" form:"repassword" binding:"required,eqfield=Password"` // 约束条件:必填、等于 Password 字段
12    Age int `json:"age" form:"age" binding:"required"`  // 约束条件:必填
13    Email string `json:"email" form:"email" binding:"required,email"` // 约束条件:必填、检查是否是邮箱
14}
15
16func main() {
17    engine := gin.Default()
18    engine.POST("SignUp", Login)
19    engine.Run(":8083")
20}
21
22func Login(context *gin.Context) {
23    var signUpForm SignUpForm
24
25    // 表单验证,验证不通过,ShouldBind 会返回 error。
26    // ShouldBind 会根据请求的 Method 和 Content-Type,自动选择 JSON、XML、Query 等等,此处会调用 ShouldBindJSON
27    if err := context.ShouldBind(&signUpForm); err != nil {
28        context.JSON(http.StatusBadRequest, gin.H{
29            "error": err.Error(),
30        })
31        return  // 验证错误记得 return,否则会在继续往下执行。
32    }
33
34    context.JSON(http.StatusOK, gin.H{
35        "msg": "signup success",
36        "user": signUpForm.Username,
37    })
38}

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

 1## Request
 2curl -X "POST" "http://127.0.0.1:8083/SignUp" \
 3     -H 'Content-Type: application/json; charset=utf-8' \
 4     -d $'{
 5  "username": "Tom",
 6  "password": "123456",
 7   "repassword": "12345",   # 重复密码不一致
 8  "age": 18,
 9  "email": "123@123.com"
10
11}'
12
13
14# 返回错误
15HTTP/1.1 400 Bad Request
16Content-Length: 108
17Content-Type: text/plain; charset=utf-8
18Connection: close
19
20{"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 方法

具体流程参照代码阅读。

 1import (
 2    "fmt"
 3    "net/http"
 4  
 5    "github.com/gin-gonic/gin"
 6    "github.com/gin-gonic/gin/binding"
 7    "github.com/go-playground/locales/en"
 8    "github.com/go-playground/locales/zh"
 9    ut "github.com/go-playground/universal-translator"
10    "github.com/go-playground/validator/v10"
11    en2 "github.com/go-playground/validator/v10/translations/en"
12    zh2 "github.com/go-playground/validator/v10/translations/zh"
13)
14
15type SignUpForm struct {
16    Username string `json:"username" form:"username" binding:"required,max=10"`  // 约束条件:必填、最大长度10
17    Password string `json:"password" form:"password" binding:"required,min=6"`  // 约束条件:必填、最小长度6
18    RePassword string `json:"repassword" form:"repassword" binding:"required,eqfield=Password"` // 约束条件:必填、等于 Password 字段
19    Age int `json:"age" form:"age" binding:"required"`  // 约束条件:必填
20    Email string `json:"email" form:"email" binding:"required,email"` // 约束条件:必填、检查是否是邮箱
21}
22
23var translator ut.Translator  // 定义全局的翻译器
24
25func main() {
26    engine := gin.Default()
27    err := InitTrans("zh")  // 初始化中文翻译器
28    if err != nil {
29        panic(err)
30    }
31    engine.POST("SignUp", Login)
32    engine.Run(":8083")
33}
34
35
36func InitTrans(locale string) (err error) {
37    translatorZh := zh.New()  // 中文环境   //【1.初始化中文翻译器】
38    translatorEn := en.New()  // 英文环境
39    uni := ut.New(translatorZh, translatorEn, translatorZh)  // 【2.传入语言环境,初始化通用翻译器】  // 通用翻译器保存所有的语言环境和翻译数据。第一个参数是备用的语言环境,后面是应支持的语言环境
40    var found bool
41    translator, found = uni.GetTranslator(locale)  // 【3.根据 locale 拿到最终的翻译器】
42    if !found {
43        return fmt.Errorf("uni.GetTranslator(%s)", locale)
44    }
45
46    // 拿到 gin 的验证器
47    validate := binding.Validator.Engine().(*validator.Validate)
48
49    // 【4.根据 locale,绑定验证器和翻译器】
50    switch locale {
51    case "en":
52        err = en2.RegisterDefaultTranslations(validate, translator)
53    case "zh":
54        err = zh2.RegisterDefaultTranslations(validate, translator)
55    default:
56        err = en2.RegisterDefaultTranslations(validate, translator)
57    }
58
59    return err
60}
61
62func Login(context *gin.Context) {
63    var signUpForm SignUpForm
64
65    // 表单验证,验证不通过,ShouldBind 会返回 error。
66    // ShouldBind 会根据请求的 Method 和 Content-Type,自动选择 JSON、XML、Query 等等,此处会调用 ShouldBindJSON
67    if err := context.Bind(&signUpForm); err != nil {
68        errors := err.(validator.ValidationErrors)  // 【5.类型转换为 validator.ValidationErrors】
69        context.JSON(http.StatusOK, gin.H{
70            "error": errors.Translate(translator),  // 【6.使用 Translate 方法】
71        })
72        return
73    }
74
75    context.JSON(http.StatusOK, gin.H{
76        "msg": "signup success",
77        "user": signUpForm.Username,
78    })
79}

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

 1## Request
 2curl -X "POST" "http://127.0.0.1:8083/SignUp" \
 3     -H 'Content-Type: application/json; charset=utf-8' \
 4     -d $'{
 5  "username": "Tom",
 6  "password": "123456",
 7   "repassword": "12345",   # 重复密码不一致
 8  "age": 18,
 9  "email": "123.com"  # 邮箱格式错误 
10
11}'
12
13
14# 返回错误
15HTTP/1.1 400 Bad Request
16Content-Length: 108
17Content-Type: text/plain; charset=utf-8
18Connection: close
19
20{
21    "error":{
22        "SignUpForm.Email":"Email必须是一个有效的邮箱",
23        "SignUpForm.RePassword":"RePassword必须等于Password"
24     }
25 }

2.8. 更改验证错误字段格式

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

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

主要需要做两件事:

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

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

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

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

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

3. 中间件

3.1. 自定义中间件

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

 1// 用于计时的中间件
 2func timing() gin.HandlerFunc {
 3    return func(c *gin.Context) {
 4        t := time.Now()
 5        c.Set("number", "1233")
 6
 7        c.Next() // 执行真正的请求处理函数
 8
 9        end := time.Since(t)
10        fmt.Println("Duration: ", end)
11        fmt.Println("Writer status: ", c.Writer.Status())
12    }
13}
14
15func main() {
16    engine := gin.New() // gin.Default() 默认使用 Logger、Recovery 两个中间件
17    engine.Use(gin.Logger(), gin.Recovery(), timing()) // 使用计时的中间件
18    engine.GET("/index", index)
19    engine.Run(":8083")
20}
21
22func index(c *gin.Context) {
23    if value, exists := c.Get("number"); exists {
24        time.Sleep(time.Second)
25        c.JSON(http.StatusOK, gin.H{
26            "number": value,
27        })
28    }
29}

3.2. 中间件队列原理

Alt text

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

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

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

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

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

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

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

 1func (group *RouterGroup) combineHandlers(handlers HandlersChain) HandlersChain {
 2  
 3    finalSize := len(group.Handlers) + len(handlers) // 计算新的队列长度
 4    assert1(finalSize < int(abortIndex), "too many handlers")
 5  
 6    mergedHandlers := make(HandlersChain, finalSize) // 创建新队列
 7  
 8    copy(mergedHandlers, group.Handlers)  // 向新队列中增加原来的中间件
 9    copy(mergedHandlers[len(group.Handlers):], handlers)  // 向新队列的尾部,增加请求处理函数
10    return mergedHandlers
11}

4. 其他

4.1. 模版功能

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

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

(1)最小实践

1.
2├── main.go
3└── templates
4    └── index.html
 1// main.go
 2
 3func main() {
 4    router := gin.Default()
 5    router.LoadHTMLFiles("templates/index.html") // 相对路径加载 HTML 文件。
 6  
 7    router.GET("/index", func(context *gin.Context) {
 8        context.HTML(http.StatusOK, "index.html", gin.H{ // 将 title 传给 index.html
 9            "title": "haha",
10        })
11    })
12  
13    router.Run(":8080")
14}
 1<!--templates/index.html-->
 2
 3<!DOCTYPE html>
 4<html lang="en">
 5<head>
 6    <meta charset="utf-8">
 7    <title>Hello World</title>
 8</head>
 9<body>
10    <h1>{{ .title }}</h1>  <!-- 使用 gin 传递过来的 title。注意格式 {{.变量名}} -->
11</body>
12</html>

注意相对路径问题:

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

(2)同时加载多个 HTML

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

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

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

1.
2├── main.go
3└── templates
4    ├── goods
5    │   └── list.html
6    ├── index
7    │   └── index.html
8    └── user
9        └── list.html
 1// main.go
 2
 3func main() {
 4    router := gin.Default()
 5    router.LoadHTMLGlob("templates/**/*") // 加载 templates 下的所有二级目录(只加载二级目录)
 6
 7    router.GET("/index", func(context *gin.Context) {
 8        context.HTML(http.StatusOK, "index/index.html", gin.H{ // 使用 /templates/index/index.html 中 define 的值
 9            "title": "haha",
10        })
11    })
12
13    router.GET("/user/list", func(context *gin.Context) {
14        context.HTML(http.StatusOK, "user/list.html", gin.H{ // 使用 /templates/user/list.html 中 define 的值
15            "title": "users",
16        })
17    })
18
19    router.GET("/goods/list", func(context *gin.Context) {
20        context.HTML(http.StatusOK, "goods/list.html", gin.H{ // 使用 /templates/goods/list.html 中 define 的值
21            "title": "goods",
22        })
23    })
24
25    router.Run(":8080")
26}

goods/list.html

 1{{define "goods/list.html" }}   <!--定义文件名称-->
 2<!DOCTYPE html>
 3<html lang="en">
 4<head>
 5    <meta charset="UTF-8">
 6    <title>goods</title>
 7</head>
 8<body>
 9    <h1>{{ .title }}</h1>
10</body>
11</html>
12{{end}}

index/index.html

 1{{define "index/index.html" }}
 2<!--定义文件名称-->
 3<!DOCTYPE html>
 4<html lang="en">
 5<head>
 6    <meta charset="utf-8">
 7    <title>Hello World</title>
 8</head>
 9<body>
10<h1>{{ .title }}</h1>   
11    <!-- 使用 gin 传递过来的 title。注意格式 {{.变量名}} -->
12</body>
13</html>
14{{end}}

user/list.html

 1{{define "user/list.html" }}   <!--定义文件名称-->
 2<!DOCTYPE html>
 3<html lang="en">
 4<head>
 5    <meta charset="utf-8">
 6    <title>Hello World</title>
 7</head>
 8<body>
 9    <h1>{{ .title }}</h1>   <!-- 使用 gin 传递过来的 title。注意格式 {{.变量名}} -->
10</body>
11</html>
12{{end}}

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

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

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

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

4.2. 静态文件处理

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

1.
2├── main.go
3├── static
4│   └── css
5│       └── index.css
6└── templates
7    └── index.html
 1// main.go
 2func main() {
 3    router := gin.Default()
 4    router.LoadHTMLGlob("templates/*")
 5    router.Static("/static", "./static") // 以 /static 开头的链接,均在 ./static 目录下找
 6    router.GET("/index", func(context *gin.Context) {
 7        context.HTML(http.StatusOK, "index.html", gin.H{
 8            "title": "haha",
 9        })
10    })
11
12    router.Run(":8080")
13}
 1<!-- /templates/index.html --> 
 2<!DOCTYPE html>
 3<link rel="stylesheet" href="/static/css/index.css">  <!-- 引用静态文件-->
 4<html lang="en">
 5<head>
 6    <meta charset="utf-8">
 7    <title>Hello World</title>
 8</head>
 9<body>
10<h1>{{ .title }}</h1>   <!-- 使用 gin 传递过来的 title。注意格式 {{.变量名}} -->
11</body>
12</html>
1/*template/static/css/index.css*/
2*{
3    background-color: burlywood;
4}

4.3. gin 的优雅退出

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

Alt text

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

 1import (
 2    "fmt"
 3    "net/http"
 4    "os"
 5    "os/signal"
 6    "syscall"
 7
 8    "github.com/gin-gonic/gin"
 9)
10
11func main() {
12    router := gin.Default()
13    router.GET("/", func(c *gin.Context) {
14        c.JSON(http.StatusOK, gin.H{
15            "message": "Hello World!",
16        })
17    })
18
19    go func() {
20        router.Run(":8080") // 在 goroutine 中监听,让主进程等待 channel 信号。
21    }()
22
23    quit := make(chan os.Signal)
24    // SIGINT 是 Crtl+C; SIGTERM 是 kill 命令不加参数。(kill -9 是 SIGKILL 无法捕获)
25    // 当收到 SIGINT、SIGTERM 信号时,像 channel 中写数据。
26    signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
27
28    <-quit // channel 收到数据,说明收到结束进程的信号。
29
30    // 处理退出逻辑,此处使用 print 模拟。
31    fmt.Println("Closing server...")
32    fmt.Println("Unregistering service...")
33}

4.4. 自动证书配置

指无需配置任何证书,即可自动下载证书,过期自动续约的功能

 1package mainimport "github.com/gin-gonic/gin"/*
 2    * 废弃
 3    engine := gin.Default()
 4    //证书位置使用的是相对路径
 5    _ = http.ListenAndServeTLS(":443", "cert/www.domain.cn.crt","cert/www.domain.cn.key", engine)
 6    */
 7func main() {
 8    r := gin.Default()
 9    r.GET("/test", func(c *gin.Context) {
10        c.String(200, "golang")
11        }
12    )
13    autotls.Run(r, "xzeu.com")
14}