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}
- 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格式的商品列表,包含矿泉水和功能饮料的信息。
- 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"的路径参数,并返回特定商品信息和该参数值。(参考下一节)
- 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}
{"action":"product1","id":"01","矿泉水":["娃哈哈矿泉水","2元","500","",""]}{"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 路径中对应位置的实际值,并将其赋给变量以便在处理函数中使用。
除了路径参数外,Gin 还支持其他方式获取 URL 数据:
- 查询参数:使用
c.Query()获取 URL 查询字符串中的参数。例如:/products?category=electronics&sort=price - 表单数据:使用
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}
{"id":2000,"name":"test100"}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}
c.ShouldBindUri(&porsen):尝试从请求的 URI 中解析参数,并将它们绑定到porsen结构体中。它会根据结构体中定义的标签(uri:"id",uri:"name")来匹配 URL 参数,并进行类型验证。if err := ... ; err != nil:这是 Go 中常见的写法,将ShouldBindUri的返回值赋给err,然后检查它是否为 nil(即是否有错误)。c.Status(404):如果存在错误(例如,参数无法正确绑定),此代码将响应的 HTTP 状态码设置为 404(资源未找到)。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}
{"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
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}
- 创建一个默认的 Gin 路由器(
gin.Default()),它预先配置了 Logger 和 Recovery 中间件; - 在"/Postform"路径注册一个 POST 路由,由
Postform函数处理; - 在 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}
- 获取 GET 参数:
c.Query("id"): 获取 URL 中的 id 参数,如果不存在则返回空字符串c.DefaultQuery("page", "未知的"): 获取 URL 中的 page 参数,如果不存在则返回默认值"未知的"
- 获取 POST 表单数据:
c.DefaultPostForm("name", "未知的"): 获取 POST 表单中的 name 字段,如果不存在则返回默认值"未知的"c.PostForm("password"): 获取 POST 表单中的 password 字段,如果不存在则返回空字符串
- 返回响应:
- 使用
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}
- 创建一个 Gin 默认路由器实例
- 注册 POST 路由 “/Post”,由 GetPost 函数处理
- 启动 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}
{"UserName":"ice_moss","Message":"This is a test of JSOM","Number":101}当访问 http://localhost:8083/someProtoBuf 的时候,会返回 someProtoBuf 数据的下载文件,当然我们可以使用 GRPC 中的方法将数据接收解析出来。
- 数据格式
- JSON: 一种基于文本、人类可读的格式,广泛用于网络 API 和前后端通信。
- ProtoBuf: 由 Google 开发的二进制序列化格式,更加紧凑高效。
- 可读性与效率对比
- JSON: 当访问
/moreJSON时,得到的是人类可读的响应:{"UserName":"ice_moss","Message":"This is a test of JSOM","Number":101} - ProtoBuf: 当访问
/someProtoBuf时,得到的是二进制数据,会被下载而不是直接显示,因为它是设计给程序解析的,而非直接阅读。
- 工作原理
- 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}
{"html":"\u003cb\u003e您好,世界!\u003c/b\u003e"}{"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:
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:值必须大于等于18email:必须是有效的邮箱格式eqfield=Password:必须与Password字段的值相等
翻译器设置
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字段名
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})
错误消息清理
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})
验证工作流程:
- 使用
c.ShouldBind(&struct)将传入的JSON绑定到结构体 - 检查验证错误
- 将错误转换为适当的类型(
validator.ValidationErrors) - 使用配置的翻译器翻译错误
- 将干净、翻译好的错误消息返回给客户端
错误响应示例
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: 是有效的颜色
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)
其实 HandlerFunc 是 func(*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}
{"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 时:
- 请求首先通过 Gin 的默认中间件(Logger 和 Recovery);
- 然后通过我们的自定义
MyTimeLogger中间件,记录当前时间、向上下文添加消息、调用c.Next(),暂停中间件并将控制权传递给后续处理程序; /ping的路由处理程序执行并返回 JSON 响应;- 控制权返回到我们的中间件,然后计算经过的时间,持续时间和响应状态
- 响应被发送到客户端:
{"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}
下面来看看 c.Abort() 的代码:
1func (c *Context) Abort() {
2 c.index = abortIndex
3}
当代码执行到 Abort() 时,index 被赋值为 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}
当然 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
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>
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}
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服务已退出
小记
累 ~