Featured image of post Go 萌新的初学之路 V

Go 萌新的初学之路 V

Go学习日记(五):Gin

本文属于 Golang 学习日记 系列:
  1. Go 萌新的初学之路
  2. Go 萌新的初学之路 II
  3. Go 萌新的初学之路 III
  4. Go 萌新的初学之路 IV
  5. Go 萌新的初学之路 V (本文)

Gin 的简单入门:基于 Gin 框架的登录 / 注册表单验证实例,Gin 中间件的原理分析,Gin 返回 html,静态文件的挂载和 Gin 优雅的退出。

介绍

Gin 是一个用 Go 编写的 HTTP Web 框架。 它具有类似 Martini 的 API,但性能比 Martini 快 40 倍。它运行速度快,具有分组的路由器,良好的崩溃捕获和错误处理,非常好的支持中间件和json。如果你需要极好的性能,使用 Gin 吧。

用以下命令安装:

1D:\user\go>go get -u github.com/gin-gonic/gin
2
3go: module github.com/gin-gonic/gin: Get "https://proxy.golang.org/github.com/gin-gonic/gin/@v/list": dial tcp 142.250.69.209:443: connectex: A connection attempt failed because the connected party did not properly respond after a period of time, or established connection failed because connected host has failed to respond.

但是我在安装的时候碰到了上面这个报错。我的解决办法:

1go env -w GOPROXY=https://goproxy.cn/

快速入门

 1package main
 2
 3import (
 4    "net/http"
 5
 6    "github.com/gin-gonic/gin"
 7)
 8
 9//handle方法
10func Pong(c *gin.Context) {
11    c.JSON(http.StatusOK, gin.H{
12        "name":   "ice_moss",
13        "age":    18,
14        "school": "家里蹲大学",
15    })
16}
17
18func main() {
19    //初始化一个gin的server对象
20    //Default实例化对象具有日志和返回状态功能
21    r := gin.Default()
22  //注册路由,并编写处理方法
23    r.GET("/ping", Pong)
24    //监听端口:默认端口listen and serve on 0.0.0.0:8080
25    r.Run(":8083")
26}
  • c.JSON()c.JSON() 方法用于发送 JSON 格式的响应:

    • 第一个参数 http.StatusOK 表示返回 200 状态码
    • 第二个参数 gin.H{}map[string]interface{} 的简写,用于构造 JSON 对象
    • 响应包含三个字段:name、age 和 school
  • gin.Default() 初始化带默认中间件的 Gin 引擎,包括日志记录和错误恢复功能

  • r.GET("/ping", Pong) 注册一个 GET 类型的路由,当访问 “/ping” 路径时,调用 Pong 函数处理请求

  • r.Run(":8083") 启动 HTTP 服务器并监听 8083 端口,默认情况下 Gin 会监听 0.0.0.0:8080

这是一个简单的 RESTful API 示例,当用户向 http://localhost:8083/ping 发送 GET 请求时,服务器会返回一个包含 name、age 和 school 信息的 JSON 响应。这种模式是构建 Web API 的基础,可以轻松扩展为更复杂的应用程序。

Gin 的 GET 和 POST 方法

 1package main
 2
 3import (
 4    "net/http"
 5
 6    "github.com/gin-gonic/gin"
 7)
 8
 9func GinGet(c *gin.Context) {
10    c.JSON(http.StatusOK, map[string]interface{}{
11        "name": "ice_moss",
12    })
13}
14
15func GinPost(c *gin.Context) {
16    c.JSON(http.StatusOK, gin.H{
17        "token": "您好",
18    })
19}
20func main() {
21    router := gin.Default()
22    router.GET("/GinGet", GinGet)
23    router.POST("/GinPost", GinPost)
24    router.Run(":8083")
25}

我们看到 GinGet 和 GinPost 这两个方法中的 c.JSON () 第二个参数不一样,原因:gin.H{} 本质就是一个 map[string]interface{}

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

然后我们就可以在浏览器中访问:localhost:8083/GinGet

这里需要注意我们不能直接在浏览器中访问:localhost:8083/GinPost,因为他用的是 POST 方法。可以使用 postman 来发送 POST 请求:

路由分组

Gin 为我们做了很好的路由分组,这样我们可以方便,对路由进行管理:

 1package main
 2
 3import (
 4    "net/http"
 5
 6    "github.com/gin-gonic/gin"
 7)
 8
 9func ProductLists(c *gin.Context) {
10    c.JSON(http.StatusOK, gin.H{
11        "矿泉水":  [5]string{"娃哈哈", "2元", "500"},
12        "功能饮料": [3]string{"红牛", "6元", "200"},
13    })
14}
15
16func Prouduct1(c *gin.Context) {
17    req := c.Param("haha")
18
19    c.JSON(http.StatusOK, gin.H{
20        "矿泉水":   [5]string{"娃哈哈矿泉水", "2元", "500"},
21        "token": req,
22    })
23}
24
25func CreateProduct(c *gin.Context) {}
26
27//路由分组
28func main() {
29    router := gin.Default()
30
31    //未使用路由分组
32    //获取商品列表
33    //router.GET("/ProductList", ProductLists)
34    //获取某一个具体商品信息
35    //router.GET("/ProductList/1", Prouduct1)
36    //添加商品
37    //router.POST("ProductList/Add", CreateProduct)
38
39    //路由分组
40    ProductList := router.Group("/Produc")
41    {
42        ProductList.GET("/list", ProductLists)
43        ProductList.GET("/1", Prouduct1)
44        ProductList.POST("/Add", CreateProduct)
45    }
46    router.Run(":8083")
47}
  1. ProductLists 函数:
1func ProductLists(c *gin.Context) {
2    c.JSON(http.StatusOK, gin.H{
3        "矿泉水":  [5]string{"娃哈哈", "2元", "500"},
4        "功能饮料": [3]string{"红牛", "6元", "200"},
5    })
6}

返回了一个JSON格式的商品列表,包含矿泉水和功能饮料的信息。

  1. Prouduct1 函数:
1func Prouduct1(c *gin.Context) {
2    req := c.Param("haha")
3    c.JSON(http.StatusOK, gin.H{
4        "矿泉水":   [5]string{"娃哈哈矿泉水", "2元", "500"},
5        "token": req,
6    })
7}

通过c.Param()获取URL中名为"haha"的路径参数,并返回特定商品信息和该参数值。(参考下一节)

  1. CreateProduct 函数: 一个空函数,预留用于创建商品的功能实现。

使用路由分组后,所有路由都共享"/Produc"前缀,最终生成的API路径为:

  • GET /Produc/list - 调用ProductLists处理函数
  • GET /Produc/1 - 调用Prouduct1处理函数
  • POST /Produc/Add - 调用CreateProduct处理函数

URL 值的提取

很多时候我们需要对 URL 中数据的提取,或者动态的 URL,我们不可能将 URL 写成固定的。

 1package main
 2
 3import (
 4    "net/http"
 5
 6    "github.com/gin-gonic/gin"
 7)
 8
 9func ProductLists(c *gin.Context) {
10    c.JSON(http.StatusOK, gin.H{
11        "矿泉水":  [5]string{"娃哈哈矿泉水", "2元", "500"},
12        "功能饮料": [3]string{"红牛", "6元", "200"},
13    })
14}
15
16func Prouduct1(c *gin.Context) {
17    //获取url中的参数
18    id := c.Param("id")
19    action := c.Param("action")
20    c.JSON(http.StatusOK, gin.H{
21        "矿泉水":    [5]string{"娃哈哈矿泉水", "2元", "500"},
22        "id":     id,
23        "action": action,
24    })
25}
26
27func CreateProduct(c *gin.Context) {}
28
29//url取值
30func main() {
31    router := gin.Default()
32    //路由分组
33    ProductList := router.Group("/Product")
34    {
35        ProductList.GET("", ProductLists)
36        //使用"/:id"动态匹配参数
37        ProductList.GET("/:id/:action", Prouduct1)
38        ProductList.POST("", CreateProduct)
39    }
40    router.Run(":8083")
41}
访问 localhost:8083/Product/01/product1
{"action":"product1","id":"01","矿泉水":["娃哈哈矿泉水","2元","500","",""]}
访问 localhost:8083/Product/100/product2000
{"action":"product2000","id":"100","矿泉水":["娃哈哈矿泉水","2元","500","",""]}

关键部分是路由注册,使用了 冒号语法 ( :parameter ) 来定义动态路由参数:

1ProductList.GET("/:id/:action", Prouduct1)
  • :id - 匹配 URL 中的第一个路径段
  • :action - 匹配 URL 中的第二个路径段

任何形如 /Product/任意值/任意值 的请求都会被路由到 Prouduct1 处理函数。而在 Prouduct1 处理函数中,使用 c.Param() 方法提取路由参数值:

1id := c.Param("id")
2action := c.Param("action")

这种方式可以获取 URL 路径中对应位置的实际值,并将其赋给变量以便在处理函数中使用。

其他 URL 参数提取方式

除了路径参数外,Gin 还支持其他方式获取 URL 数据:

  1. 查询参数:使用 c.Query() 获取 URL 查询字符串中的参数。例如: /products?category=electronics&sort=price
  2. 表单数据:使用 c.PostForm() 获取表单提交的数据

结构体声明并做约束

 1package main
 2
 3import (
 4    "net/http"
 5
 6    "github.com/gin-gonic/gin"
 7)
 8
 9//结构体声明,并做一些约束
10type Porsen struct {
11    ID   int    `uri:"id" binding:"required"`    //uri指在client中的名字为id,binding:"required指必填
12    Name string `uri:"name" binding:"required"`  //同理
13}
14
15//url参数获取
16func main() {
17    router := gin.Default()
18    router.GET("/:name/:id", func(c *gin.Context) {
19        //使用porsen对数据进行解组
20        var porsen Porsen
21        if err := c.ShouldBindUri(&porsen); err != nil {
22            c.Status(404)
23            return
24        }
25        c.JSON(http.StatusOK, gin.H{
26            "name": porsen.Name,
27            "id":   porsen.ID,
28        })
29    })
30    router.Run(":8083")
31}
访问 localhost:8083/test100/2000
{"id":2000,"name":"test100"}
但是访问 localhost:8083/100/test2000
1找不到 localhost 的网页找不到与以下网址对应的网页:http://localhost:8083/100/test2000
2HTTP ERROR 404

这和我们约束条件一致。

结构体声明与标签

1type Porsen struct {
2    ID   int    `uri:"id" binding:"required"`
3    Name string `uri:"name" binding:"required"`
4}

这里,结构体定义了两个重要的标签注解:

  • uri:"id"uri:"name" 指定这些字段对应哪些 URL 参数
  • binding:"required" 表示这些字段在请求中必须存在

路由处理器设置

1router.GET("/:name/:id", func(c *gin.Context) {
2    var porsen Porsen
3    if err := c.ShouldBindUri(&porsen); err != nil {
4        c.Status(404)
5        return
6    }
7    // 返回 JSON 响应
8})
  • 路由模式是 /:name/:id,所以 URL 路径的第一部分映射到 name,第二部分映射到 id
  • c.ShouldBindUri(&porsen) 尝试解析 URL 参数并将它们绑定到结构体字段

为什么一个 URL 可以工作而另一个失败

可工作的 URL:localhost:8083/test100/2000

在这个 URL 中,“test100” 被绑定到 Name 字段(字符串类型),“2000” 被绑定到 ID 字段(整数类型),两个值都符合它们预期的类型因此绑定成功。

失败的 URL:localhost:8083/100/test2000

“100” 被绑定到 Name 字段(字符串),但是 “test2000” 被绑定到 ID 字段(整数),因为 “test2000” 无法转换为整数所以绑定失败。

错误处理

当绑定失败时,代码返回 404 状态码:

1if err := c.ShouldBindUri(&porsen); err != nil {
2    c.Status(404)
3    return
4}
  1. c.ShouldBindUri(&porsen):尝试从请求的 URI 中解析参数,并将它们绑定到 porsen 结构体中。它会根据结构体中定义的标签(uri:"id", uri:"name")来匹配 URL 参数,并进行类型验证。
  2. if err := ... ; err != nil:这是 Go 中常见的写法,将 ShouldBindUri 的返回值赋给 err,然后检查它是否为 nil(即是否有错误)。
  3. c.Status(404):如果存在错误(例如,参数无法正确绑定),此代码将响应的 HTTP 状态码设置为 404(资源未找到)。
  4. return:提前退出处理函数,防止执行后续代码(也就不会返回正常的 JSON 响应)。

Gin 还提供了 BindUri 方法,它在绑定失败时会自动中止请求并返回 400 状态码。

URL 参数的提取

GET 请求参数获取

URL 参数的提取是 GET 方法常用的方法,URL 中有需要的参数,例如我们访问这个百度图片:

? 以后的都是参数,用 & 分隔:?ct=201326592 &tn=baiduimage &word=%E5...

 1package main
 2
 3import (
 4    "net/http"
 5
 6    "github.com/gin-gonic/gin"
 7)
 8
 9func Welcom(c *gin.Context) {
10    //DefaultQuery根据字段名获取client请求的数据,client未提供数据则可以设置默认值
11    first_name := c.DefaultQuery("first_name", "未知")
12    last_mame := c.DefaultQuery("last_name", "未知")
13    c.JSON(http.StatusOK, gin.H{
14        "firstname": first_name,
15        "lastname":  last_mame,
16        "work":      [...]string{"公司:Tencent", "职位:Go开发工程师", "工资:20000"},
17    })
18}
19
20//url参数获取
21func main() {
22    //实例化server对象
23    router := gin.Default()
24    router.GET("/welcom", Welcom)
25    router.Run(":8083")
26}
访问 localhost:8083/welcom?first_name=moss&last_name=ice
{"firstname":"moss","lastname":"ice","work":["公司:Tencent","职位:Go开发工程师","工资:20000"]}

这样我们的后台就拿到了 client 提供的参数并做业务处理。

POST 表单提交及其数据获取

在很多时候我们都需要使用 post 方法来传输数据,例如用户登录/注册都需要提交表单等。下面我们来看看简单的表单提交:

 1package main
 2
 3import (
 4    "net/http"
 5
 6    "github.com/gin-gonic/gin"
 7)
 8
 9func Postform(c *gin.Context) {
10    UserName := c.DefaultPostForm("username", "unkown")
11    PassWord := c.DefaultPostForm("password", "unkown")
12    if UserName == "[email protected]" && PassWord == "123456" {
13        c.JSON(http.StatusOK, gin.H{
14            "name":        "ice_moss",
15            "username":    UserName,
16            "tokenstatus": "认证通过",
17        })
18    } else {
19        c.JSON(http.StatusInternalServerError, gin.H{
20            "tokenstatus": "认证未通过",
21        })
22    }
23}
24
25//url参数获取
26func main() {
27    //实例化server对象
28    router := gin.Default()
29    router.POST("/Postform", Postform)
30    router.Run(":8083")
31}

由于是 post 请求我们使用 postman 提交表单:localhost:8083/Postform

记住是 post 请求。后台输出:
1username ice_moss password 18dfdf
2[GIN] 2022/06/23 - 19:24:24 | 500 |     108.029µs |             ::1 | POST     "/Postform"

表单提交处理函数解释:

 1func Postform(c *gin.Context) {
 2    UserName := c.DefaultPostForm("username", "unkown")
 3    PassWord := c.DefaultPostForm("password", "unkown")
 4    if UserName == "[email protected]" && PassWord == "123456" {
 5        c.JSON(http.StatusOK, gin.H{
 6            "name":        "ice_moss",
 7            "username":    UserName,
 8            "tokenstatus": "认证通过",
 9        })
10    } else {
11        c.JSON(http.StatusInternalServerError, gin.H{
12            "tokenstatus": "认证未通过",
13        })
14    }
15}
表单数据获取
  • c.DefaultPostForm("username", "unkown") 从 POST 表单数据中获取"username"字段的值。如果该字段不存在,则返回默认值"unkown"。
  • c.DefaultPostForm("password", "unkown") 同样方式获取密码字段,有默认值作为备选。
认证逻辑
函数检查用户名是否为 [email protected] 且密码是否为 123456。如果凭证匹配,返回 200 OK 状态码和包含用户信息的 JSON;如果凭证不匹配,返回 500 Internal Server Error 状态码。

1func main() {
2    //实例化server对象
3    router := gin.Default()
4    router.POST("/Postform", Postform)
5    router.Run(":8083")
6}
  1. 创建一个默认的 Gin 路由器( gin.Default() ),它预先配置了 Logger 和 Recovery 中间件;
  2. 在"/Postform"路径注册一个 POST 路由,由 Postform 函数处理;
  3. 在 8083 端口启动 HTTP 服务器。

GET 和 POST 混合使用

 1package main
 2
 3import (
 4    "net/http"
 5
 6    "github.com/gin-gonic/gin"
 7)
 8
 9func GetPost(c *gin.Context) {
10    id := c.Query("id")
11    page := c.DefaultQuery("page", "未知的")
12    name := c.DefaultPostForm("name", "未知的")
13    password := c.PostForm("password")
14    c.JSON(http.StatusOK, gin.H{
15        "id":       id,
16        "page":     page,
17        "name":     name,
18        "password": password,
19    })
20}
21
22//url参数获取
23func main() {
24    //实例化server对象
25    router := gin.Default()
26
27    //GET和POST混合使用
28    router.POST("/Post", GetPost)
29    router.Run(":8083")
30}

因为是 post 方法使用,访问:localhost:8083/Post?id=1&page=2


 1func GetPost(c *gin.Context) {
 2    id := c.Query("id")
 3    page := c.DefaultQuery("page", "未知的")
 4    name := c.DefaultPostForm("name", "未知的")
 5    password := c.PostForm("password")
 6    c.JSON(http.StatusOK, gin.H{
 7        "id":       id,
 8        "page":     page,
 9        "name":     name,
10        "password": password,
11    })
12}
这个函数演示了四种不同的参数获取方法
  1. 获取 GET 参数:
  • c.Query("id"): 获取 URL 中的 id 参数,如果不存在则返回空字符串
  • c.DefaultQuery("page", "未知的"): 获取 URL 中的 page 参数,如果不存在则返回默认值"未知的"
  1. 获取 POST 表单数据:
  • c.DefaultPostForm("name", "未知的"): 获取 POST 表单中的 name 字段,如果不存在则返回默认值"未知的"
  • c.PostForm("password"): 获取 POST 表单中的 password 字段,如果不存在则返回空字符串
  1. 返回响应:
  • 使用 c.JSON() 将所有参数组合成 JSON 格式返回给客户端

1func main() {
2    //实例化server对象
3    router := gin.Default()
4
5    //GET和POST混合使用
6    router.POST("/Post", GetPost)
7    router.Run(":8083")
8}
主函数的作用
  1. 创建一个 Gin 默认路由器实例
  2. 注册 POST 路由 “/Post”,由 GetPost 函数处理
  3. 启动 HTTP 服务器并监听 8083 端口

数据格式 JSON 和 ProtoBuf

我们知道前后端数据交互大多数都是以 json 的格式,Go 也同样满足。我们知道 GRPC 的数据交互是以 ProtoBuf 格式的,这里我们用到了 proto 文件夹下的代码,结构如下:

1proto
2├── user.pb.go
3└── user.proto

可参考 proto 相关代码: GoWeb框架Gin学习总结proto文件 | Go 技术论坛,把对应的 proto 文件夹放在 Go/src 根目录下。

下面我们来看看 Go 是如何处理 json 的,如何处理 ProtoBuf 的。

 1package main
 2
 3import (
 4    "net/http"
 5
 6    "StudyGin/HolleGin/ch07/proto"
 7
 8    "github.com/gin-gonic/gin"
 9)
10
11func moreJSON(c *gin.Context) {
12    var msg struct {
13        Nmae    string `json:"UserName"`
14        Message string
15        Number  int
16    }
17    msg.Nmae = "ice_moss"
18    msg.Message = "This is a test of JSOM"
19    msg.Number = 101
20
21    c.JSON(http.StatusOK, msg)
22}
23
24//使用ProtoBuf
25func returnProto(c *gin.Context) {
26    course := []string{"python", "golang", "java", "c++"}
27    user := &proto.Teacher{
28        Name:   "ice_moss",
29        Course: course,
30    }
31    //返回protobuf
32    c.ProtoBuf(http.StatusOK, user)
33}
34
35//使用结构体和JSON对结构体字段进行标签,使用protobuf返回值
36func main() {
37    router := gin.Default()
38    router.GET("/moreJSON", moreJSON)
39    router.GET("/someProtoBuf", returnProto)
40    router.Run(":8083")
41}
访问 localhost:8083/moreJSON
{"UserName":"ice_moss","Message":"This is a test of JSOM","Number":101}

当访问 http://localhost:8083/someProtoBuf 的时候,会返回 someProtoBuf 数据的下载文件,当然我们可以使用 GRPC 中的方法将数据接收解析出来。

主要区别解释
  1. 数据格式
  • JSON: 一种基于文本、人类可读的格式,广泛用于网络 API 和前后端通信。
  • ProtoBuf: 由 Google 开发的二进制序列化格式,更加紧凑高效。
  1. 可读性与效率对比
  • JSON: 当访问 /moreJSON 时,得到的是人类可读的响应:{"UserName":"ice_moss","Message":"This is a test of JSOM","Number":101}
  • ProtoBuf: 当访问 /someProtoBuf 时,得到的是二进制数据,会被下载而不是直接显示,因为它是设计给程序解析的,而非直接阅读。
  1. 工作原理
  • JSON: Go 通过结构体标签如 json:"UserName" 内置支持 JSON 序列化,允许自定义字段名。
  • ProtoBuf: 需要在 .proto 文件中定义数据结构,然后编译成特定语言的代码(此例中是 user.pb.go 文件)。

Gin 解析特殊字符

我们很多时候需要处理特殊的字符,比如:JSON 会将特殊的 HTML 字符替换为对应的 unicode 字符,比如 < 替换为 \u003c,如果想原样输出 html,则使用 PureJSON。

 1package main
 2
 3import (
 4    "net/http"
 5
 6    "github.com/gin-gonic/gin"
 7)
 8
 9//通常情况下,JSON会将特殊的HTML字符替换为对应的unicode字符,比如<替换为\u003c,如果想原样输出html,则使用PureJSON
10func main() {
11    router := gin.Default()
12    router.GET("/json", func(c *gin.Context) {
13        c.JSON(http.StatusOK, gin.H{
14            "html": "<b>您好,世界!</b>",
15        })
16    })
17
18    router.GET("/pureJSON", func(c *gin.Context) {
19        c.PureJSON(http.StatusOK, gin.H{
20            "html": "<div><b>您好,世界!</b></div>",
21        })
22    })
23    router.Run(":8083")
24}
访问 localhost:8083/json
{"html":"\u003cb\u003e您好,世界!\u003c/b\u003e"}
访问 localhost:8083/pureJSON
{"html":"<div><b>您好,世界!</b></div>"}

Gin 翻译器的实现

在这段代码中,我们是将注册代码实现翻译功能:在 Go web 应用程序中使用 Gin 框架结合 go-playground/validator 包实现表单验证,并提供验证错误消息的国际化(i18n)支持。

  1package main
  2
  3import (
  4    "fmt"
  5    "net/http"
  6    "reflect"
  7    "strings"
  8
  9    "github.com/gin-gonic/gin/binding"
 10    "github.com/go-playground/locales/en"
 11    "github.com/go-playground/locales/zh"
 12    ut "github.com/go-playground/universal-translator"
 13    "github.com/go-playground/validator/v10"
 14    enTranslations "github.com/go-playground/validator/v10/translations/en"
 15    zhTranslations "github.com/go-playground/validator/v10/translations/zh"
 16
 17    "github.com/gin-gonic/gin"
 18)
 19
 20// 定义一个全局翻译器T
 21var trans ut.Translator
 22
 23//Login登录业务,字段添加tag约束条件
 24type Login struct {
 25    User     string `json:"user" binding:"required"`     //必填
 26    Password string `json:"password" binding:"required"` //必填
 27}
 28
 29//SignUp注册业务,字段添加tag约束条件
 30type SignUp struct {
 31    Age        int    `json:"age" binding:"gte=18"`                            //gte大于等于
 32    Name       string `json:"name" binding:"required"`                         //必填
 33    Email      string `json:"email" binding:"required,email"`                  //必填邮件
 34    Password   string `json:"password" binding:"required"`                     //必填
 35    RePassword string `json:"re_password" binding:"required,eqfield=Password"` //RePassword和Password值一致
 36}
 37
 38//RemoveTopStruct去除以"."及其左部分内容
 39func RemoveTopStruct(fields map[string]string) map[string]string {
 40    res := map[string]string{}
 41    for field, value := range fields {
 42        res[field[strings.Index(field, ".")+1:]] = value
 43    }
 44    return res
 45}
 46
 47// InitTrans 初始化翻译器
 48func InitTrans(locale string) (err error) {
 49    // 修改gin框架中的Validator引擎属性,实现自定制
 50    if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
 51        //注册一个获取json的自定义方法
 52        v.RegisterTagNameFunc(func(field reflect.StructField) string {
 53            name := strings.SplitN(field.Tag.Get("json"), ",", 2)[0]
 54            if name == "-" {
 55                return ""
 56            }
 57            return name
 58        })
 59        zhT := zh.New() // 中文翻译器
 60        enT := en.New() // 英文翻译器
 61
 62        // 第一个参数是备用(fallback)的语言环境
 63        // 后面的参数是应该支持的语言环境(支持多个)
 64        // uni := ut.New(zhT, zhT) 也是可以的
 65        uni := ut.New(enT, zhT, enT)
 66
 67        // locale 通常取决于 http 请求头的 'Accept-Language'
 68        var ok bool
 69        // 也可以使用 uni.FindTranslator(...) 传入多个locale进行查找
 70        trans, ok = uni.GetTranslator(locale)
 71        if !ok {
 72            return fmt.Errorf("uni.GetTranslator(%s) failed", locale)
 73        }
 74
 75        // 注册翻译器
 76        switch locale {
 77        case "en":
 78            err = enTranslations.RegisterDefaultTranslations(v, trans)
 79        case "zh":
 80            err = zhTranslations.RegisterDefaultTranslations(v, trans)
 81        default:
 82            err = enTranslations.RegisterDefaultTranslations(v, trans)
 83        }
 84        return
 85    }
 86    return
 87}
 88
 89func main() {
 90    res := map[string]string{
 91        "ice_moss.habbit": "打球",
 92        "ice_moss.from":   "贵州 中国",
 93    }
 94    fmt.Println(RemoveTopStruct(res))
 95
 96    //初始化翻译器, 翻译器代码看不懂不要紧,我们只需知道这样使用就行
 97    if err := InitTrans("zh"); err != nil {
 98        fmt.Println("初始化翻译器失败", err)
 99        return
100    }
101
102    router := gin.Default()
103    router.POST("/loginJSON", func(c *gin.Context) {
104        var login Login
105        if err := c.ShouldBind(&login); err != nil {
106            fmt.Println(err.Error())
107            errs, ok := err.(validator.ValidationErrors)
108            if !ok {
109                c.JSON(http.StatusOK, gin.H{
110                    "msg": err.Error(),
111                })
112            }
113            c.JSON(http.StatusInternalServerError, gin.H{
114                "error": errs.Translate(trans),
115            })
116            return
117        }
118        c.JSON(http.StatusOK, gin.H{
119            "msg": "验证通过",
120        })
121    })
122
123    router.POST("/signupJSON", func(c *gin.Context) {
124        var signup SignUp
125         //ShouldBind()对数据进行绑定,解组
126        if err := c.ShouldBind(&signup); err != nil {
127            fmt.Println(err.Error())
128            //获取validator.ValidationErrors类型的error
129            errs, ok := err.(validator.ValidationErrors)
130            if !ok {
131                c.JSON(http.StatusOK, gin.H{
132                    "msg": err.Error(),
133                })
134            }
135            //validator.ValidationErrors类型错误则进行翻译
136            c.JSON(http.StatusInternalServerError, gin.H{
137                "error": RemoveTopStruct(errs.Translate(trans)),
138            })
139            return
140        }
141
142        c.JSON(http.StatusOK, gin.H{
143            "msg": "注册成功",
144        })
145    })
146    router.Run(":8083")
147}

我们 POST 访问:localhost:8083/signupJSON

如果参数不满足 tag 中的条件,则会返回如下结果
当我们输入如下满足 tag 中的条件
1{
2    "age":20,
3    "name":"ifantic",
4    "email":"[email protected]",
5    "password":"123456",
6    "re_password":"123456"
7}

结构体标签验证

代码定义了两个带有验证规则的结构体,使用结构体标签设置规则:
 1type Login struct {
 2    User     string `json:"user" binding:"required"`     //必填
 3    Password string `json:"password" binding:"required"` //必填
 4}
 5
 6type SignUp struct {
 7    Age        int    `json:"age" binding:"gte=18"`                            //gte大于等于
 8    Name       string `json:"name" binding:"required"`                         //必填
 9    Email      string `json:"email" binding:"required,email"`                  //必填邮件
10    Password   string `json:"password" binding:"required"`                     //必填
11    RePassword string `json:"re_password" binding:"required,eqfield=Password"` //RePassword和Password值一致
12}

这些标签定义了如下验证规则:

  • required:字段不能为空
  • gte=18:值必须大于等于18
  • email:必须是有效的邮箱格式
  • eqfield=Password:必须与Password字段的值相等

翻译器设置

InitTrans 函数初始化了验证错误消息的翻译
1func InitTrans(locale string) (err error) {
2    // ...
3    zhT := zh.New() // 中文翻译器
4    enT := en.New() // 英文翻译器
5    uni := ut.New(enT, zhT, enT)
6    // ...
7}

这允许验证错误消息以不同语言显示(本例中为中文或英文)。

错误消息中的自定义JSON字段名

代码注册了一个自定义函数,用于在错误消息中使用JSON字段名
1v.RegisterTagNameFunc(func(field reflect.StructField) string {
2    name := strings.SplitN(field.Tag.Get("json"), ",", 2)[0]
3    if name == "-" {
4        return ""
5    }
6    return name
7})

错误消息清理

RemoveTopStruct 函数从错误消息中删除结构体名称前缀
1func RemoveTopStruct(fields map[string]string) map[string]string {
2    res := map[string]string{}
3    for field, value := range fields {
4        res[field[strings.Index(field, ".")+1:]] = value
5    }
6    return res
7}

例如,它将错误消息中的SignUp.name转换为仅name

请求处理流程

定义了两个端点
1router.POST("/loginJSON", func(c *gin.Context) {
2    // 登录验证逻辑
3})
4
5router.POST("/signupJSON", func(c *gin.Context) {
6    // 注册验证逻辑
7})

验证工作流程:

  1. 使用c.ShouldBind(&struct)将传入的JSON绑定到结构体
  2. 检查验证错误
  3. 将错误转换为适当的类型(validator.ValidationErrors
  4. 使用配置的翻译器翻译错误
  5. 将干净、翻译好的错误消息返回给客户端

错误响应示例

对于无效输入,API返回翻译后的验证错误
1{
2  "error": {
3    "age": "Age必须大于等于18",
4    "email": "Email为必填字段",
5    "re_password": "Re_password必须等于Password"
6  }
7}

这些验证规则来自 github.com/go-playground/validator 包,这是一个专门用于Go语言结构体和字段验证的开源库

Validator 提供了大量内置的验证标签,可以直接在结构体标签(struct tags)中使用。

Validator 内置验证标签

比较验证
  • eq: 等于
  • ne: 不等于
  • gt: 大于
  • gte: 大于等于
  • lt: 小于
  • lte: 小于等于
字段间比较
  • eqfield: 与另一个字段相等
  • nefield: 与另一个字段不相等
  • gtfield: 大于另一个字段
  • gtefield: 大于等于另一个字段
  • ltfield: 小于另一个字段
  • ltefield: 小于等于另一个字段
字符串验证
  • alpha: 仅包含字母
  • alphanum: 仅包含字母和数字
  • numeric: 仅包含数字字符
  • number: 有效数字值
  • email: 有效电子邮件格式
  • url: 有效URL格式
  • uri: 有效URI格式
  • contains=xyz: 包含指定文本
  • containsany: 包含任何指定字符
  • excludes: 不包含指定文本
  • startswith: 以指定文本开头
  • endswith: 以指定文本结尾
  • lowercase: 全部为小写字符
  • uppercase: 全部为大写字符
长度验证
  • len: 精确长度
  • min: 最小长度
  • max: 最大长度
格式验证
  • json: 有效的JSON
  • jwt: 有效的JWT令牌
  • uuid: 有效的UUID
  • uuid3/4/5: 特定版本的UUID
  • base64: 有效的base64字符串
  • isbn/isbn10/isbn13: 有效的ISBN
网络验证
  • ip: 有效IP地址(v4或v6)
  • ipv4: 有效IPv4地址
  • ipv6: 有效IPv6地址
  • cidr: 有效CIDR表示法
  • mac: 有效MAC地址
条件验证
  • required: 字段必须存在且不为空
  • required_if: 当另一个字段等于某值时必填
  • required_unless: 除非另一个字段等于某值否则必填
  • required_with: 当任一指定字段存在时必填
  • required_without: 当任一指定字段不存在时必填
其他验证
  • oneof: 值必须是指定的多个值之一
  • dive: 深入切片、数组或映射并验证元素
  • unique: 切片/数组中的元素必须唯一
  • datetime: 根据给定格式的有效日期时间字符串
  • latitude: 有效纬度坐标
  • longitude: 有效经度坐标
  • file: 有效文件路径
  • iscolor: 是有效的颜色
自定义验证
validator还支持注册自定义验证函数,可以通过RegisterValidation方法添加自己的验证规则。
这些验证标签可以组合使用,例如 required,min=3,max=10 表示字段必填且长度在3到10之间。

Gin 中间件原理及自定义中间件

中间件系统会在 HTTP 请求到达路由处理器之前或生成响应后对其进行处理。

在此之前我们先来看一下 Gin 实例化 server, 我们在之前是使用 router := gin.Default(),但其实我们是可以直接使用 router := gin.New(), 那么在之前是实例中我们为什么不使用 gin.New() 呢?

别急,我们先来看看 gin.Default() 的源码:

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())
6    return engine
7}

我们可以看到 Default () 其实是对 gin.New() 做了一层封装,并且做了其他事情,这里的其他事情就有 “中间件”。

即:engine.Use(Logger(), Recovery()) 创建了一个新的 Gin 引擎,并已附带两个内置中间件:

  • Logger 中间件:输出请求日志
  • Recovery 中间件:从任何恐慌(panic)中恢复 (recovers) 并返回 500 错误响应

如果要使用 gin.New() 代替,那你获得的是没有附加任何中间件的干净引擎,在你想要完全控制要使用哪些中间件时很有用。

Gin 中间件原理

我们来看看:engine.Use ()

1func (engine *Engine) Use(middleware ...HandlerFunc) IRoutes {
2    engine.RouterGroup.Use(middleware...)
3    engine.rebuild404Handlers()
4    engine.rebuild405Handlers()
5    return engine
6}

入参是 HandlerFunc 类型,Use 方法接受一个或多个类型为 HandlerFunc 的中间件函数。那么我们接着往下看 HandlerFunc

1// HandlerFunc defines the handler used by gin middleware as return value.
2type HandlerFunc func(*Context)

其实 HandlerFuncfunc(*Context) 类型,到这里中间件我们就可以自定义了。

自定义中间件

我们定义一个监控服务运行时间,运行状态的中间件:

 1//自定义中间件,这里我们以函数调用的形式,对中间件进一步封装
 2func MyTimeLogger() gin.HandlerFunc {
 3    return func(c *gin.Context) {          //真正的中间件类型
 4        t := time.Now()
 5        c.Set("msg", "This is a test of middleware")
 6        //它执行调用处理程序内链中的待处理处理程序
 7        //让原本执行的逻辑继续执行
 8        c.Next()
 9
10        end := time.Since(t)
11        fmt.Printf("耗时:%D\n", end.Seconds())
12        status := c.Writer.Status()
13        fmt.Println("状态监控:", status)
14    }
15}

我们在 main 函数中:

 1func main() {
 2    router := gin.Default()
 3    router.Use(MyTimeLogger())   //这里使用函数调用
 4    router.GET("/ping", func(c *gin.Context) {
 5        c.JSON(http.StatusOK, gin.H{
 6            "msg": "Pong",
 7        })
 8    })
 9    router.Run(":8083")
10}
访问 localhost:8083/ping
{"msg":"Pong"}

完整代码

 1package main
 2
 3import (
 4    "fmt"
 5    "net/http"
 6    "time"
 7    
 8    "github.com/gin-gonic/gin"
 9)
10
11// 自定义中间件,记录执行时间和状态
12func MyTimeLogger() gin.HandlerFunc {
13    return func(c *gin.Context) {
14        // 预处理:请求处理前的操作
15        t := time.Now()
16        c.Set("msg", "This is a test of middleware")
17        
18        // 将控制权传递给下一个中间件/处理程序
19        c.Next()
20        
21        // 后处理:请求处理后的操作
22        duration := time.Since(t)
23        fmt.Printf("请求耗时: %v 秒\n", duration.Seconds())
24        status := c.Writer.Status()
25        fmt.Printf("响应状态: %d\n", status)
26    }
27}
28
29func main() {
30    // 创建带有默认中间件的 Gin 路由器
31    router := gin.Default()
32    
33    // 添加我们的自定义中间件
34    router.Use(MyTimeLogger())
35    
36    // 定义路由
37    router.GET("/ping", func(c *gin.Context) {
38        // 如果需要,获取中间件数据
39        msg, exists := c.Get("msg")
40        if exists {
41            fmt.Println("来自中间件的消息:", msg)
42        }
43        
44        // 模拟一些工作
45        time.Sleep(200 * time.Millisecond)
46        
47        // 返回响应
48        c.JSON(http.StatusOK, gin.H{
49            "msg": "Pong",
50        })
51    })
52    
53    // 启动服务器
54    fmt.Println("服务器在 :8083 端口启动...")
55    router.Run(":8083")
56}

中间件工作步骤详解

当您访问 localhost:8083/ping 时:

  1. 请求首先通过 Gin 的默认中间件(Logger 和 Recovery);
  2. 然后通过我们的自定义 MyTimeLogger 中间件,记录当前时间、向上下文添加消息、调用 c.Next(),暂停中间件并将控制权传递给后续处理程序;
  3. /ping 的路由处理程序执行并返回 JSON 响应;
  4. 控制权返回到我们的中间件,然后计算经过的时间,持续时间和响应状态
  5. 响应被发送到客户端:{"msg":"Pong"}
关键概念
  • 中间件链:多个中间件可以一起使用,形成一个链,每个中间件按顺序处理请求。
  • c.Next():这个函数至关重要 - 它暂停当前中间件,执行剩余的链,然后返回继续在当前中间件中执行。
  • 上下文(Context)Context 对象允许中间件相互之间以及与处理程序共享数据。

这种方法提供了一种干净的方式来将关注点(如日志记录、身份验证和错误处理)与主应用程序逻辑分开。

中间件实际应用

中间件非常适合用来实现身份验证逻辑,因为它可以在请求到达业务处理逻辑之前进行拦截和验证。下面的例子展示了如何使用自定义中间件来验证请求头中的令牌(Token),即基于中间件模拟登录:

 1//自定义中间件
 2func TokenRequired() gin.HandlerFunc {
 3    return func(c *gin.Context) {
 4        var token string
 5    //从请求头中获取数据
 6        for k, v := range c.Request.Header {
 7            if k == "X-Token" {
 8                token = v[0]
 9            } else {
10                fmt.Println(k, v)
11            }
12        }
13        fmt.Println(token)
14        if token != "ice_moss" {
15            c.JSON(http.StatusUnauthorized, gin.H{
16                "msg": "认证未通过",
17            })
18
19      //return在这里不会有被执行
20            c.Abort()   //这里先不用理解,后面会讲解,这里先理解为return
21        }
22    //继续往下执行该执行的逻辑
23        c.Next()
24    }
25}

将中间件加入 gin 中:

 1func main() {
 2    router := gin.Default()
 3    //中间件
 4    router.Use(TokenRequired())
 5    router.GET("/ping", func(c *gin.Context) {
 6        c.JSON(http.StatusOK, gin.H{
 7            "msg": "Pong",
 8        })
 9    })
10    router.Run(":8083")
11}

我们在 postman 中进行请求:localhost:8083/ping,在 Headers 中增加字段:

Key Value
X-Token ice_moss

正确参数返回
1{
2    "msg": "Pong"
3}
不正确参数返回
1{
2    "msg": "认证未通过"
3}

这里我们围绕 c.Abort()c.Next() 两个方法来对中间件原理进一步剖析。

c.Abort()

在上面的例子中我们看到:

 1func TokenRequired() gin.HandlerFunc {
 2   return func(c *gin.Context) {
 3      var token string
 4 for k, v := range c.Request.Header {
 5         if k == "X-Token" {
 6            token = v[0]
 7         } else {
 8            fmt.Println(k, v)
 9         }
10      }
11      fmt.Println(token)
12      if token != "ice_moss" {
13         c.JSON(http.StatusUnauthorized, gin.H{
14            "msg": "认证未通过",
15  })
16         //return 不会被执行,需要使用c.Abort()来结束当前
17         c.Abort()
18      }
19      //继续执行该执行的逻辑
20      c.Next()
21   }
22}
为什么 return 不能直接返回,而是使用 c.Abort()?
当我们启动服务后,Gin 会有一个类似于任务队列将所有配置的中间件和在注册处理方法压入队列中:

在处理业务代码之前,会将所有注册路由中的中间件以队列的执行方式执行,比如上面我们:

当我们在上面的例子中执行 return 时,他只是将当前函数返回,但是后面的方法仍然是按逻辑执行的,很显然这不是我们想要的结果,不满足验证条件的情况,应该将对此时的 client 终止服务。如果要终止服务就应该将图中的箭头跳过所有方法:

这样整个服务才是真正的终止。

下面来看看 c.Abort() 的代码:

1func (c *Context) Abort() {
2   c.index = abortIndex
3}

当代码执行到 Abort() 时,index 被赋值为 abortIndex

abortIndex 是什么?
const abortIndex int8 = math.MaxInt8 >> 1

可以看到,最后 index 指向任务末端,这就是 const abortIndex int8 = math.MaxInt8 >> 1 作用的效果。

c.Next()

理解了 Abort()Next() 自然就好理解了,我们来看看它的定义

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

Gin 返回 html 模板

我们使用 html 模板,将后端获取到的数据,直接填充至 html 中。我们先来编写一个 html (实例为 tmpl, 无影响):

 1<!DOCTYPE html>
 2<html lang="en">
 3<head>
 4 <meta charset="UTF-8">
 5 <title>{{ .title }}</title>
 6</head>
 7<body>
 8<h1>{{ .menu }}</h1>
 9</body>
10</html>

其中数据以 {{ .title }} 从 web 层填充进来。

1ch11
2├─ main.go
3└─ templates
4         └─ index.tmpl

我们需要注意目录结构,程序的执行入口 main 需要和模板 templates 放置同一目录下,这样保证 main 能读取文件 html:

 1package main
 2
 3import (
 4 "net/http"
 5 "github.com/gin-gonic/gin")
 6
 7func main() {
 8   router := gin.Default()
 9   //读取文件
10   router.LoadHTMLFiles("templates/index.tmpl")
11   router.GET("/index", func(c *gin.Context) {
12       //写入数据,  key必须要tmpl一致
13      c.HTML(http.StatusOK, "index.tmpl", gin.H{
14         "title": "购物网",  
15         "menu":  "菜单栏",
16  })
17   })
18   router.Run(":8085")
19}
访问 localhost:8085/index

当然 router.LoadHTMLFiles() 方法可以加载多个 html 文件:

 1package main
 2
 3import (
 4 "net/http"
 5 "github.com/gin-gonic/gin")
 6
 7func main() {
 8   router := gin.Default()
 9
10   //读取模板文件,按指定个读取
11  router.LoadHTMLFiles("templates/index.tmpl", "templates/goods.html")
12   router.GET("/index", func(c *gin.Context) {
13      c.HTML(http.StatusOK, "index.tmpl", gin.H{
14         "title": "shop",
15         "menu":  "菜单栏",
16  })
17   })
18
19   router.GET("goods", func(c *gin.Context) {
20      c.HTML(http.StatusOK, "goods.html", gin.H{
21         "title": "goods",
22         "goods": [4]string{"矿泉水", "面包", "薯片", "冰淇淋"},
23  })
24   })
25   router.Run(":8085")
26}

这样就可以访问:localhost:8085/goods 或者 localhost:8085/index 了。返回结果略

当然如果 html 文件很多,Gin 还提供了 :

1func (engine *Engine) LoadHTMLGlob(pattern string) {……}

我们只需要这样调用:

1//将"templates文件夹下所有文件加载
2router.LoadHTMLGlob("templates/*")

对应二级目录,我们又是如何处理的呢?

1//加载templates目录下的目录中的所有文件
2router.LoadHTMLGlob("templates/**/*")

实例十六:

 1package main
 2
 3import (
 4 "net/http"
 5 "github.com/gin-gonic/gin")
 6
 7func main() {
 8   router := gin.Default()
 9 router.LoadHTMLGlob("templates/**/*")
10   router.GET("user/list", func(c *gin.Context) {
11      c.HTML(http.StatusOK, "list.html", gin.H{
12         "title": "shop",
13         "list":  "用户列表",
14  })
15   })
16
17   router.GET("goods/list", func(c *gin.Context) {
18      c.HTML(http.StatusOK, "list.html", gin.H{
19        "title": "shop",
20        "list":  "商品列表",
21  })
22   })
23   router.Run(":8085")
24}

与上节相似,创建 list.html

1<!DOCTYPE html>
2<html>
3<head>
4    <title>{{ .title }}</title>
5</head>
6<body>
7    <h1>{{ .list }}</h1>
8</body>
9</html>

这样我们访问:localhost:8085/goods/list 或者 localhost:8085/user/list 都能访问到。

Gin 静态文件的挂载

在 web 开发中经常需要将 js 文件和 css 文件,进行挂载,来满足需求。目录结构:

1ch11
2├─ main.go
3├─ static
4│      └─ style.css
5└─ templates
6        └─ user
7                └─ list.html
html 文件
 1<!DOCTYPE html>
 2<html lang="en">
 3<head>
 4 <meta charset="UTF-8">
 5 <title>{{ .title }}</title>
 6 <link rel="stylesheet" href="/static/style.css">
 7</head>
 8<body>
 9<h1>{{ .list }}</h1>
10</body>
11</html>
css 文件
1*{
2    background-color: aquamarine;
3}

静态文件挂载方法:router.Static("/static", "./static")
该方法会去在 html 文件中 <link> 标签中找到以 static 开头的链接,然后去找在当前 main 所在的目录下找到以第二个参数 ./static 名称的目录下找到静态文件,然后挂载。

实例十七:

 1package main
 2
 3import (
 4 "net/http"
 5 "github.com/gin-gonic/gin")
 6
 7func main() {
 8   router := gin.Default()
 9   //挂载静态文件
10   router.Static("/static", "./static")
11   router.LoadHTMLGlob("templates/**/*")
12   router.GET("user/list", func(c *gin.Context) {
13      c.HTML(http.StatusOK, "list.html", gin.H{
14         "title": "shop",
15  "list":  "用户列表",
16  })
17   })
18
19   router.Run(":8085")
20}
访问 localhost:8085/user/list

Gin 优雅退出

在业务中,我们很多时候涉及到服务的退出,如:各种订单处理中,用户突然退出,支付费用时,程序突然退出,这里我们是需要是我们的服务合理的退出,进而不造成业务上的矛盾。实例十八:

 1package main
 2
 3import (
 4 "net/http"
 5 "github.com/gin-gonic/gin")
 6
 7func main() {
 8   router := gin.Default()
 9   router.GET("ping", func(c *gin.Context) {
10      c.JSON(http.StatusOK, gin.H{
11         "msg": "ping",
12  })
13   })
14
15   go func() {
16      router.Run(":8085")
17   }()
18
19   qiut := make(chan os.Signal)
20   //接收control+c
21   //当接收到退出指令时,我们向chan收数据
22  signal.Notify(qiut, syscall.SIGINT, syscall.SIGTERM)
23   <-qiut
24
25 //服务退出前做处理
26   fmt.Println("服务退出中")
27   fmt.Println("服务已退出")
28}

terminal 中运行:go run main.go。服务启动后在 terminal 中退出 (control+c) 就可以看到:

1[GIN-debug] Listening and serving HTTP on :8085
2服务退出中
3服务已退出

小记

累 ~

Licensed under CC BY-NC-SA 4.0
最后更新于 2025-09-16 11:05 +0800
本文属于 Golang 学习日记 系列:
  1. Go 萌新的初学之路
  2. Go 萌新的初学之路 II
  3. Go 萌新的初学之路 III
  4. Go 萌新的初学之路 IV
  5. Go 萌新的初学之路 V (本文)
给博主施舍一个赞吧(;へ:) ❤️