第22关 go的web框架-gin

怕什么真理无穷,进一寸有一寸的欢喜。

22-1 gin的helloworld体验

go代理设置
事先设置好:
在这里插入图片描述
GOPROXY=goproxy.cn,direct
https://github.com/gin-gonic/gin

简单的描述就是gin是一个HTTP的web框架,是用go语言写的。
这个框架它的性能比较高,也是简单的,‍‍它就像Python里边的flask一样,它是一个小而轻的web框架。‍‍

我们在学习的时候是没有必要去学习重复学习的。‍‍
第一你转过来很简单,
第二没有必要重复的学习。

我们先来看一下怎么使用它,使用它比较简单,首先要安装它,
go get -u github.com/gin-gonic/gin

示例代码:

package main

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

func main() {
	r := gin.Default()
	r.GET("/ping", func(c *gin.Context) {
		c.JSON(200, gin.H{
			"message": "pong",
		})
	})
	r.Run() // listen and serve on 0.0.0.0:8080
}

代码解读:
r := gin.Default()可以把它理解为‍‍实例化一个gin的server对象。

现在在r.GET("/ping", func(c *gin.Context)这里边给ta配置一个URL,‍‍然后URL它对应的处理函数什么?‍‍
关键是我们要知道这个函数,函数里边关键是这个参数,‍‍这个参数必须是一个gn的context即*gin.Context
而且它是一个指针的类型,它的参数比较简单,就只有这一个,‍‍
然后我们可以点进 GET函数里面看一下:
在这里插入图片描述
关键是你要知道前边是一个relativePath string,‍‍然后后边是一连串的handlers ...HandlerFunc,我们看到三个点...,实际上可以添加多个handler。
我们来看HandlerFunc是什么?
在这里插入图片描述
它是一个type, 这个type是一个function,它实际上‍‍把你的function换了一个名称,‍‍这个方可以是一个Context它的指针。‍‍
所以说对于我们来说,我们要注册的函数是很简单的,‍‍你随便写一个函数,比如说这里边写一个ping返回一个pong:

package main

import (
	"net/http"

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

func pong(c *gin.Context) {
	c.JSON(http.StatusOK, gin.H{
		"message": "pong",
	})
}
func main() {
	//实例化一个gin的server对象
	r := gin.Default()
	r.GET("/ping", pong)
	r.Run(":8083") // listen and serve on 0.0.0.0:8080
}

这里面我定义一个函数pong,‍‍然后我把c *gin.Context给它拿过来。‍‍
我用c.JSON,然后是一个本次请求的HTTP的状态码,‍‍这个状态码我们一般不写死,我们一般是在这里边找到它内置的一个HTTP的库,‍‍然后大家看到这是一个StatusOK
在这里边第一个状态码,‍‍第二个就是它的一个内容,这个内容里边我们可以看到它是一个gin.H
H是什么呢?‍‍
在这里插入图片描述
h实际上就是一个map它的别名,map它的key是string,‍‍它的value就是interface,也就是任何类型。‍‍

最后我怎么运行起来它,我直接r.Run,‍‍
Run里边我们可以自动定义它的端口,如果我不定义端口的话,它默认是监听在0.0.0.0的‍‍8080端口上。
我如果想改端口的话,我前面加一个冒号叫8083,看一下,‍‍这样的话我就可以自己给指定一个端口。

运行起来:
在这里插入图片描述

可以看到它会有一些日志信息,
在端口8083上监听,
我们在浏览器里边直接来访问,然后8083,‍‍后边注意到我们如果不加的话,它会返回一个404的页面:
在这里插入图片描述
因为我们并没有配置一个主页,‍‍所以说我们要在后边给它加一个ping:
在这里插入图片描述

22-2 使用New和Default初始化路由器的区别

本小节我们来看一下如何为一个请求配置:
get方法
post方法
put方法等等

代码如下:

package main

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

func main() {
	// 使用默认中间件创建一个gin路由器
	// logger and recovery (crash-free) 中间件
	router := gin.Default()

	//restful 的开发中
	router.GET("/someGet", getting)
	router.POST("/somePost", posting)
	router.PUT("/somePut", putting)
	router.DELETE("/someDelete", deleting)
	router.PATCH("/somePatch", patching)
	router.HEAD("/someHead", head)
	router.OPTIONS("/someOptions", options)

	// 默认启动的是 8080端口,也可以自己定义启动端口
	router.Run()
	// router.Run(":3000") for a hard coded port
}

router := gin.Default()r:=gin.New()
router := gin.Default()使用默认的中间件,也就是default,创建一个路由器。
r:=gin.New()用这个方法会返回一个gin的实例。
这两个方法都没有问题。‍‍
但是如果使用Default的话,它会默认帮我们开启两个中间件,‍‍一个就是logger,一个就是recover。‍‍

logger和recover分别有什么用?‍‍
比如logger,我在浏览器发起get请求它会在后台打印日志的:
在这里插入图片描述
如果我们使用New的方法,这个方法它是去掉了默认的‍‍两个中间件的,我们再来运行一下:
在这里插入图片描述
可以看到,请求的时候没有问题正常响应,但它并没有打印出日志信息。
然后是recovery,也就是故障的恢复,如果我们使用New的方法那么就没有recover,
没有recover的话,假设我们在这里边这个函数里边,‍‍它抛了一个异常,最终的效果是什么的?
先贴出使用New方法的代码,并看看效果:
在这里插入图片描述
可以看到:
ERR_EMPTY_RESPONSE
这里边它的状态码是什么样子的,‍‍这个状态码它是ERR_EMPTY_RESPONSE,也就是说服务器没有做任何的返回,‍‍
那服务器能做返回吗?
那是不能的,
那是因为什么呢?
因为你这只出现异常的,‍‍本身这个异常它只是你函数错误对不对?函数错误正常情况下你给它返回一个500的状态码,这不就行了吗?‍‍
但是因为你这里边抛出了异常,程序不能继续往下执行。

再贴出使用Default方法的代码,并看看效果:
在这里插入图片描述

可以看到:
HTTP ERROR 500
如果我们在这里使用Default,我们可以点击源码看一下:
在这里插入图片描述
第一个是Logger打印日志,
第二个是Recovery。‍‍
这个Recovery就能把你的异常‍‍给捕获到,捕获到之后它会做一个处理,就是向我们的浏览器报告一个, 我现在给你返回了,‍‍只不过我告诉你我的服务器现在是500的状态码,也就说是内部我的问题。
不再是不给你返回任何信息了。‍‍

我们现在用的是chrome浏览器,它的响应状态码是HTTP ERROR 500,‍‍服务器现在响应你了,只不过响应你的是500的状态码。

比如说我现在
想发起一个get请求,‍‍
想发起一个post请求,
想发起一个put请求,
在这里插入图片描述
现在我们可以看到我们可以给它配置不同的HTTP方法,‍‍
这些方法在我们后面的restfulapi开发中就很有用了,

实际上给它配置一个URL,然后给它配置一个函数。
这些函数我们就不再一一写了,我们后面具体的也会用到的POS请求。‍‍
这个router我们也可以叫路由器,‍‍现在我给它配置一个get方法,
也就是浏览器发送过来的get请求,你的 URL是/someGet的话它会匹配到这里。‍‍

对于同一个URL来说,实际上它既可以配置get方法,也可以配置我们的post处理,‍‍
这就是如何为它配置不同的HTTP方法。‍‍

22-3 gin的路由分组

本小节我们来看一下gin的URL和路由分组。

对于我们来说 URL 过多的话,‌‌我们一定会面临着一个问题,就是路由的分组的问题。
路由分组是什么意思?

我们先来讲解一下什么叫路由分组,以及为什么需要路由分组。‌

比如说我这里边既要提供商品服务,‌‌又要提供用户服务,
商品服务这个功能它的接口也会比较多,‌‌比如说我要获取商品的列表页,比如说我要获取某一个商品的详细信息,
‌这样的话我们为了区别它们,
对于商品我们给它加同样的前缀,比如说我现在给商品服务先添加第一个前缀:

router.GET("/goods/list",goodsList)
router.GET("/goods/1",goodsDetail)
router.GET("/goods/add", createGoods)

比如我想获取商品的列表信息,‌‌即router.GET("/goods/list",goodsList)
比如我要获取商品的详细信息,比如说‌‌要获取商品id为1的商品的详细信息,即router.GET("/goods/1",goodsDetail)

比如说我们仍然还需要添加一个商品,即router.GET("/goods/add", createGoods)

你会发现,这三个url的前缀都是一样的,
所以我们可以把它分到一组里边,这样的话一样的东西就没有必要这些东西都重复写了,这就是我们路由的一个分组的功能。‌
‌路由的分组怎么用的?

我们首先要新建一个分组,‌‌这样做就对了:
在这里插入图片描述
后期所有的绑定,我就直接在goodsGroup上进行绑定。

你看我前面已经有这个前缀了,我后面在写的时候就没有必要加这个前缀了,它会自动帮我匹配到,‌‌

‌这个时候‌‌我们通过‌‌ goodsGroup就知道,下边的路由都归属于它上边管,请求你只有带着goods然后list‌‌,才会进入这个函数。

一般情况下因为这个代码如果‌‌就这样近凑的写的话,实际上仍然不太好区别,所以说在我们的静态语言当中,它都有一对大括号,‌‌我们可以把这个逻辑放到大括号里边,这个大括号不用依附于任何一个,比如说函数的函数体,你直接就写就行了,‌‌这样的话你们看到我的代码整个的层次结构就会比较好。

func main() {
	router := gin.Default()
	// Simple group: v1
	v1 := router.Group("/v1")
	{
		v1.POST("/login", loginEndpoint)
		v1.POST("/submit", submitEndpoint)
		v1.POST("/read", readEndpoint)
	}
	// Simple group: v2
	v2 := router.Group("/v2")
	{
		v2.POST("/login", loginEndpoint)
		v2.POST("/submit", submitEndpoint)
		v2.POST("/read", readEndpoint)
	}
	router.Run(":8083")
}

‌现在我们对于我们来说一个版本号的问题,我们在开发的过程中肯定会有一些比如说版本号,‌‌我后边迭代的时候把v1改成v2。

‌我们就体验一个最简单的:

package main

import (
	"github.com/gin-gonic/gin"
	"net/http"
)

func main() {
	router := gin.Default()
	goodsGroup := router.Group("/goods")
	{
		goodsGroup.GET("", goodsList)
		goodsGroup.GET("/:id/:action/add", goodsDetail) //获取商品id为1的详细信息 模式
		goodsGroup.POST("", createGoods)
	}

	router.Run(":8083")
}

func createGoods(c *gin.Context) {

}

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

func goodsList(context *gin.Context) {
	context.JSON(http.StatusOK, gin.H{
		"name": "goodsList",
	})
}

在这里插入图片描述
‌这就是路由分组,当我们的URL过多的时候,我们肯定是会用到的。​

22-4 获取url中的变量

在上一小节中我们讲解了路由的分组,本小节我们来看一个‌‌路由分组里边,也就是我们URL当中,如果有一个它是变动的量,也就是说‌‌ URL有一个部分它是一个变量,这个时候我们如何来配置?

比如我想获取某个商品的详情,‌‌即goodsGroup.GET("/1", goodsDetail)
假如是获取商品ID为1的详细信息,
‌对于一个URL当中,‌‌
如果你采用这种硬编码的方式,也就意味着后续所有的内容你只能获取商品ID为1的‌‌URL。

如果你想获取商品ID为2怎么办?
干就完了,再写一条:goodsGroup.GET("/2", goodsDetail)
这对我们来说肯定是一个致命性的问题。‌
‌比如说商品里面有10万件,
比如说我都不知道商品里面有多少件,‌‌这个时候我应该怎么办,我不可能配置10万个URL,你来一个商品我就要重新部署一下代码,‌‌这肯定是不行的,所以对于我们来说,我们希望你这里边能够符合某种模式,只要符合某种模式‌‌都可以,或者说我把你后边的内容把它提取到一个变量里边来,‌‌然后在这个函数里边我能够拿到这个变量,然后我去查询数据库,这个就非常符合我们的要求了。‌

‌这个怎么来配置?
这个配置还是比较简单的,我们只需要在这里边加一个冒号,冒号id即goodsGroup.GET("/:id", goodsDetail),‌‌
如果你放过来的是一个/goods/1,ta就会1把它取出来,放到goods里边,我怎么在detail里边把它取出来,‌‌

这就是我们要讲解到的如何从URL中取出一个变量,‌‌当然这里边变量它不止一个,你可以做两个变量。‌
代码1:

package main

import (
	"github.com/gin-gonic/gin"
	"net/http"
)

func main() {
	router := gin.Default()
	goodsGroup := router.Group("/goods")
	{
		goodsGroup.GET("/list", goodsList)
		goodsGroup.GET("/:id", goodsDetail) //获取商品id为1的详细信息 模式
		goodsGroup.POST("/add", createGoods)
	}

	router.Run(":8083")
}

func createGoods(c *gin.Context) {

}

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

func goodsList(context *gin.Context) {
	context.JSON(http.StatusOK, gin.H{
		"name": "goodsList",
	})
}

在这里插入图片描述

可以看到,匹配规则还是蛮严格的,重点是
我框起来的红色框部分,这就是restfulapi的规范问题了,我应该对id进行类型限制——数字类型,否则很奇怪。

那怎么对url进行类型的设置呢?
代码2:

package main

import (
	"github.com/gin-gonic/gin"
	"net/http"
)

type Person struct {
	ID   int    `uri:"id" binding:"required"`
	Name string `uri:"name" binding:"required"`
}

func main() {
	router := gin.Default()
	router.GET("/:name/:id", func(c *gin.Context) {
		var person Person
		if err := c.ShouldBindUri(&person); err != nil {
			c.Status(404)
			return
		}
		c.JSON(http.StatusOK, gin.H{
			"name": person.Name,
			"id":   person.ID,
		})
	})
	router.Run(":8083")
}

在这里插入图片描述
这样就做到了对url中的id进行限制。
如果你输入的id不是一个int类型,那么浏览器/后台就会返回404的http状态码。

22-5 获取get和post表单信息

在上一小节中我们讲解了如何取出URL当中的这些变量。‌‌
本小节我们来讲解一下如何获取get和post里边的参数。‌‌

首先我们来看一下这里边我们获取参数的时候,
一般情况下我们会从get‌‌中获取参数,也会从post中去获取参数,这是我们获取参数非常常见的两种情况。‌‌

首先我们来看一下在URL当中的参数,‌‌它实际上已经完整的包含在路径当中,我们无非就是把它进行匹配,‌‌匹配出一个变量来,然后把它在我们的函数当中把它取出来而已。‌
‌但是一般传递参数的时候,这种其实是属于URL中的一部分,‌‌真正的传递参数我们会带在URL当中,URL当中我们把它叫做get参数,post的参数它是在HTTP的文本当中,
这两个是我们非常常见的两种参数,‌‌
我们就来看一下怎么从我们的get和post中来获取参数。‌
代码1:【代码不可折叠】

package main

import (
	"github.com/gin-gonic/gin"
	"net/http"
)

func main() {
	router := gin.Default()

	router.GET("/welcome", welcome)
	router.POST("/form_post", formPost)
	router.POST("/post", getPost)

	router.Run(":8083")
}

func getPost(c *gin.Context) {
	id := c.Query("id")
	page := c.DefaultQuery("page", "0")
	name := c.PostForm("name")
	message := c.DefaultPostForm("message", "信息")
	c.JSON(http.StatusOK, gin.H{
		"id":      id,
		"page":    page,
		"name":    name,
		"message": message,
	})
}

func formPost(c *gin.Context) {
	message := c.PostForm("message")
	nick := c.DefaultPostForm("nick", "anonymous")
	c.JSON(http.StatusOK, gin.H{
		"message": message,
		"nick":    nick,
	})
}

func welcome(c *gin.Context) {
	firstName := c.DefaultQuery("firstname", "yang")
	lastName := c.DefaultQuery("lastname", "keegan")
	c.JSON(http.StatusOK, gin.H{
		"first_name": firstName,
		"last_name":  lastName,
	})
}

做什么+代码+解读逻辑
直接取默认值:
在这里插入图片描述
手动添加值:
在这里插入图片描述
以上就是从get请求获取参数。

接下来看如何从post请求获取参数。
除了用postman测试,还可以用https://www.apipost.cn/,还可以用Python的requests模块。
在这里插入图片描述

接下来讲解get、post混合获取。
在这里插入图片描述
通过这种方式,既获取get 也就是url中的参数,又能获取post的表单里的内容。

22-6 gin返回protobuf

本小节来讲解我们如何在函数当中return的json和protobuf,‌‌
gin它是默认直接返回protobuf的。

json是我们HTTP开发中最常用的一种格式了,我们对它做进一步的讲解。‌
代码1:

package main

import (
	"net/http"

	"github.com/gin-gonic/gin"

	"OldPackageTest/gin_start/ch06/proto"
)

func main() {
	router := gin.Default()

	router.GET("/moreJSON", moreJSON)
	router.GET("/someProtoBuf", returnProto)

	router.Run(":8083")
}

func returnProto(c *gin.Context) {
	course := []string{"python", "go", "微服务"}
	user := &proto.Teacher{
		Name:   "keegan",
		Course: course,
	}
	c.ProtoBuf(http.StatusOK, user)
}

func moreJSON(c *gin.Context) {
	var msg struct {
		Name    string `json:"user"`
		Message string
		Number  int
	}
	msg.Name = "bobby"
	msg.Message = "这是一个测试json"
	msg.Number = 20

	c.JSON(http.StatusOK, msg)
}

json tag,注意到前面一定要是json,‌‌
在返回的时候,‌‌实际上我希望你把Name它变成json的时候,把它变成user。‌
在这里插入图片描述
可以看到,返回的时候,Name它变成了user。
因为Message和Number我们没有给它配置成的json tag,‌‌它就使用了默认的自己的名字。

‌接下来我们来看一下如何返回一个‌‌ protobuff 的原始字符串。
在这里插入图片描述

➜  proto docker run --rm -v $(PWD):$(PWD) -w $(PWD) -e ICODE=1A200AC1B14BC22 cap1573/cap-protoc -I ./ --go_out=plugins=grpc:./ ./user.proto

生成user.pb.go源文件:
在这里插入图片描述
然后main.go代码:

package main

import (
	"net/http"

	"github.com/gin-gonic/gin"

	proto "OldPackageTest/gin_start/ch06/proto"
)

func main() {
	router := gin.Default()

	router.GET("/moreJSON", moreJSON)
	router.GET("/someProtoBuf", returnProto)

	router.Run(":8083")
}

func returnProto(c *gin.Context) {
	course := []string{"python", "go", "微服务"}
	user := &proto.Teacher{
		Name:   "keegan",
		Course: course,
	}
	c.ProtoBuf(http.StatusOK, user)
}

func moreJSON(c *gin.Context) {
	var msg struct {
		Name    string `json:"user"`
		Message string
		Number  int
	}
	msg.Name = "keegan"
	msg.Message = "这是一个测试json"
	msg.Number = 20

	c.JSON(http.StatusOK, msg)
}

在这里插入图片描述

查看文件内容:
在这里插入图片描述
这是protobuf原始字符串。
可以对它进行解析。
确保你做了以下工作:

python -m pip install grpcio
python -m pip install grpcio-tools

然后在含有user.proto文件的当前目录中执行:
python -m grpc_tools.protoc --python_out=. --grpc_python_out=. -I. user.proto
在这里插入图片描述
在这里插入图片描述
这就是返回的 protobuff 的原始字符串。

22-7 登录的表单验证

‌本小节我们来讲解一下表单验证。‌‌

首先我们来看一下在gin当中如何去验证前端传递过来的表单。‌‌

在前面的小节中,我们讲解了如何去获取用户传递过来的参数,‌‌这些参数实际上我们在做后续的业务处理之前,我们往往需要验证一下它的这些信息是否‌‌合法,所以说表单验证是web框架当中非常常见也是非常重要的功能。‌

在开始之前我们需要知道怎么去做表单的验证。【步骤是什么?代码案例?】

表单验证这gin提供了两种方法来实现:

在这里插入图片描述

https://github.com/go-playground/validator
这两种方法其实是基于 validator之上做了一层封装,‌‌可以看到两个方法,
第一个方法是 Must bind,
第二个方法是 Should bind。‌

它们之间的区别是什么?
Must bind 就是我在调用它的时候,你如果‌‌不符合我的要求,我直接就给客户端抛出异常,这个异常它是一个400的异常400, err,‌‌这是符合我们restful设计风格的一个异常。‌

‌如果我们调用Should bind的话,实际上它就说这个异常它会返回给你,ta不会‌‌给客户端直接返回状态码,这个错误由我们开发人员自己来做。‌
在这里插入图片描述

当我们在绑定某一个数据的时候,我们如果使用Should bind,‌‌它是会猜测你前端传递过来的数据是json是xml还是你的表单,‌‌
它会自己根据你传递过来的Content-Type,ta会自己去猜测的,‌‌然后猜测到之后它会调用对应的这些方法,‌‌
也就说Should bind它会动态地去决定应该调 用ShouldBindJSON 还是 ShouldBindXML 还是 ShouldBindQuery 还是 ShouldBindYAML
所以说我们都不用担心直接使用它就行了。‌
https://github.com/go-playground/validator

https://github.com/wtforms/wtforms
功能是一样的。

假设我现在要做登录和注册的功能,‌‌所以我要设计登录和注册的表单,

代码1:

package main

import (
	"fmt"
	"github.com/gin-gonic/gin"
	"net/http"
)

type LoginForm struct {
	User string `json:"user" binding:"required,min=3,max=10"`
	Password string `json:"password" binding:"required"`
}

func main(){
	router := gin.Default()
	router.POST("/loginJSON",func(c *gin.Context){
		var loginForm LoginForm
		if err := c.ShouldBind(&loginForm);err != nil{
			fmt.Println(err.Error())
			c.JSON(http.StatusBadRequest,gin.H{
				"error":err.Error(),
			})
			//return // 出错也要return
		}

		c.JSON(http.StatusOK,gin.H{
			"msg":"登录成功",
		})
	})
	router.Run(":8083")
}

运行结果:
在这里插入图片描述
如果
在这里插入图片描述
如果
在这里插入图片描述
可以看到,它出现了"msg":"登录成功",这个消息不应该出现的,解决方法是加上return【结束标志】,结束main函数,代码就不会继续往下走了。
在这里插入图片描述
如果
在这里插入图片描述

在这里插入图片描述
代码解读:
其实就是你在json中传递的key是什么,‌‌真正对于它的约束,除了string的类型之外,我们还要有更多的约束,‌‌
比如说字段是不是必填,
比如说你的 user 长度最短是多少,最长是多少‌‌等等一系列的需求。

我们就来讲解一个最简单的配置,就是required。‌
如果你觉得字段是必填,那么你就把它配置为required就行了。‌

‌这里边非常多配置项是非常多的,‌‌可以在官方的‌‌文档当中去看
https://github.com/go-playground/validator/
比如说我现在有多个约束在上面,‌‌比如说它的最短长度是三,‌‌你就用逗号把它隔开就行了。

假设你有一个最大长度max‌‌,这里如果你有多个配置项用逗号隔开,对password我们也给它配置一个required。‌

我们表单验证就可以开始了。
首先来‌‌获得一个router,
router := gin.Default()
然后我们给它配置一个post请求,比如说用json tag做的就这样写loginJSON,
我们现在给它来一个函数,这个函数就这样写,‌‌然后func(c *gin.Context){}即gin它的context,是一个指针类型,

我们现在在函数里面来‌‌配置具体的逻辑,这个逻辑你想要配置它的话,你就先实例化一个var loginForm LoginForm
然后form,‌‌然后它是我们的login form。‌
接着我们怎么使用?‌‌
我们直接使用 context,它有一个方法叫ShouldBind,‌‌即c.ShouldBind

然后对它做if判断,你如果没有,我就给你抛一个异常,我就给你返回错误,
这个时候把它的指针传递过来,就是我们的&loginForm。‌‌

‌一般情况下,‌‌在我们的restful设计风格里边,我们一般都会直接给ta来 return,比如说HTTP,‌‌然后400是参数错误,我们叫StatusBadRequest。点击源码看一下,‌‌这是一个400的状态码。
在这里插入图片描述

如果它没有错误的话,我们直接来http.StatusOK
现在我们就做了简单的login表单。

我们看到运行结果,特别注意的是:
‌在这里边我们可以看到有一些问题,
第一个就是它把‌‌go语言的命名风格的东西‌‌返回给我们前端了,这种是我们要解决的问题,然后就是它的英语的问题,对于我们来说我们肯定是希望它成中文的,所以说接下来我们要解决这些问题,但是在解决这些问题之前,‌‌我们再来丰富一下这里边的逻辑,因为这个逻辑是很简单的,我们再来一个稍微复杂一点的这样一个form,‌‌那就是我们的注册的form,

我们把这些form复杂一点的form做完了之后,‌‌我们再来讲解如何进行翻译。​

22-8 注册表单的验证

本小节我们来讲解一下注册的form表单的验证。

所设置字段的参考链接:
https://github.com/go-playground/validator/
这里的重点是跨字段的设置。

代码1:

package main

import (
	"fmt"
	"github.com/gin-gonic/gin"
	"net/http"
)

type LoginForm struct {
	User string `json:"user" binding:"required,min=3,max=10"`
	Password string `json:"password" binding:"required"`
}

type SignUpForm struct {
	Age        uint8  `json:"age" binding:"gte=1,lte=130"`
	Name       string `json:"name" binding:"required,min=3"`
	Email      string `json:"email" binding:"required,email"`
	Password   string `json:"password" binding:"required"`
	RePassword string `json:"re_password" binding:"required,eqfield=Password"` //跨字段
}

func main(){
	router := gin.Default()
	router.POST("/loginJSON",func(c *gin.Context){
		var loginForm LoginForm
		if err := c.ShouldBind(&loginForm);err != nil{
			fmt.Println(err.Error())
			c.JSON(http.StatusBadRequest,gin.H{
				"error":err.Error(),
			})
			return // 出错也要return
		}

		c.JSON(http.StatusOK,gin.H{
			"msg":"登录成功",
		})
	})

	router.POST("/signup", func(c *gin.Context) {
		var signUpFrom SignUpForm
		if err := c.ShouldBind(&signUpFrom); err != nil {
			fmt.Println(err.Error())
			c.JSON(http.StatusBadRequest, gin.H{
				"error": err.Error(),
			})
			return
		}

		c.JSON(http.StatusOK, gin.H{
			"msg": "注册成功",
		})
	})

	router.Run(":8083")
}

如果
在这里插入图片描述
如果
在这里插入图片描述
如果
在这里插入图片描述
可以看到,字段的类型校验是非常严格的。

22-9 表单验证错误翻译成中文

本小节我们来讲解一下如何将Key: 'SignUpForm.Email' Error:Field validation for 'Email' failed on the 'email' tag"这些错误信息翻译成中文,‍‍
主要是因为validator本身是支持国际语言的,我们通过一定的配置就可以‍‍让服务器给我们返回中文。

首先我们来看一下如何解决中文的问题。‍‍在解决中文的问题之前,我们来看一下这里的文档,然后搜索一下叫translation。‍‍
https://github.com/go-playground/validator/blob/master/_examples/translations/main.go

代码1:

package main

import (
	"fmt"
	"net/http"
	"reflect"
	"strings"

	"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"
	en_translations "github.com/go-playground/validator/v10/translations/en"
	zh_translations "github.com/go-playground/validator/v10/translations/zh"
)

var trans ut.Translator

type LoginForm struct {
	User     string `json:"user" binding:"required,min=3,max=10"`
	Password string `json:"password" binding:"required"`
}

type SignUpForm struct {
	Age        uint8  `json:"age" binding:"gte=1,lte=130"`
	Name       string `json:"name" binding:"required,min=3"`
	Email      string `json:"email" binding:"required,email"`
	Password   string `json:"password" binding:"required"`
	RePassword string `json:"re_password" binding:"required,eqfield=Password"` //跨字段
}

func removeTopStruct(fileds map[string]string) map[string]string {
	rsp := map[string]string{}
	for field, err := range fileds {
		rsp[field[strings.Index(field, ".")+1:]] = err
	}
	return rsp
}

func InitTrans(locale string) (err error) {
	//修改gin框架中的validator引擎属性, 实现定制
	if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
		//注册一个获取json的tag的自定义方法
		v.RegisterTagNameFunc(func(fld reflect.StructField) string {
			name := strings.SplitN(fld.Tag.Get("json"), ",", 2)[0]
			if name == "-" {
				return ""
			}
			return name
		})

		zhT := zh.New() //中文翻译器
		enT := en.New() //英文翻译器
		//第一个参数是备用的语言环境,后面的参数是应该支持的语言环境
		uni := ut.New(enT, zhT, enT)
		trans, ok = uni.GetTranslator(locale)
		if !ok {
			return fmt.Errorf("uni.GetTranslator(%s)", locale)
		}

		switch locale {
		case "en":
			en_translations.RegisterDefaultTranslations(v, trans)
		case "zh":
			zh_translations.RegisterDefaultTranslations(v, trans)
		default:
			en_translations.RegisterDefaultTranslations(v, trans)
		}
		return
	}

	return
}

func main() {
	//代码侵入性很强 中间件
	if err := InitTrans("zh"); err != nil {
		fmt.Println("初始化翻译器错误")
		return
	}
	router := gin.Default()
	router.POST("/loginJSON", func(c *gin.Context) {

		var loginForm LoginForm
		if err := c.ShouldBind(&loginForm); err != nil {
			errs, ok := err.(validator.ValidationErrors)
			if !ok {
				c.JSON(http.StatusOK, gin.H{
					"msg": err.Error(),
				})
			}
			c.JSON(http.StatusBadRequest, gin.H{
				"error": removeTopStruct(errs.Translate(trans)),
			})
			return
		}

		c.JSON(http.StatusOK, gin.H{
			"msg": "登录成功",
		})
	})

	router.POST("/signup", func(c *gin.Context) {
		var signUpFrom SignUpForm
		if err := c.ShouldBind(&signUpFrom); err != nil {
			fmt.Println(err.Error())
			c.JSON(http.StatusBadRequest, gin.H{
				"error": err.Error(),
			})
			return
		}

		c.JSON(http.StatusOK, gin.H{
			"msg": "注册成功",
		})
	})

	_ = router.Run(":8083")
}

可以看到:
在这里插入图片描述

我们就完成了第一个比较重要的步骤,将英文给它翻译成了中文。‍​

22-10 表单中文翻译的json格式化细节

先来看一下问题:
在这里插入图片描述

发生该问题的代码1:

package main

import (
	"fmt"
	"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"
	en_translations "github.com/go-playground/validator/v10/translations/en"
	zh_translations "github.com/go-playground/validator/v10/translations/zh"
	"net/http"
)

var trans ut.Translator

type LoginForm struct {
	User     string `json:"user" binding:"required,min=3,max=10"`
	Password string `json:"password" binding:"required"`
}

type SignUpForm struct {
	Age        uint8  `json:"age" binding:"gte=1,lte=130"`
	Name       string `json:"name" binding:"required,min=3"`
	Email      string `json:"email" binding:"required,email"`
	Password   string `json:"password" binding:"required"`
	RePassword string `json:"re_password" binding:"required,eqfield=Password"` //跨字段
}

func InitTrans(locale string) (err error) {
	//修改gin框架中的validator引擎属性, 实现定制
	if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
		zhT := zh.New() //中文翻译器
		enT := en.New() //英文翻译器
		//第一个参数是备用的语言环境,后面的参数是应该支持的语言环境
		uni := ut.New(enT, zhT, enT)
		trans, ok = uni.GetTranslator(locale)
		if !ok {
			return fmt.Errorf("uni.GetTranslator(%s)", locale)
		}

		switch locale {
		case "en":
			en_translations.RegisterDefaultTranslations(v, trans)
		case "zh":
			zh_translations.RegisterDefaultTranslations(v, trans)
		default:
			en_translations.RegisterDefaultTranslations(v, trans)
		}
		return
	}

	return
}

func main() {
	//代码侵入性很强 中间件
	if err := InitTrans("zh"); err != nil {
		fmt.Println("初始化翻译器错误")
		return
	}
	router := gin.Default()
	router.POST("/loginJSON", func(c *gin.Context) {

		var loginForm LoginForm
		if err := c.ShouldBind(&loginForm); err != nil {
			errs, ok := err.(validator.ValidationErrors)
			if !ok {
				c.JSON(http.StatusOK, gin.H{
					"msg": err.Error(),
				})
			}
			c.JSON(http.StatusBadRequest, gin.H{
				"error": errs.Translate(trans),
			})
			return
		}

		c.JSON(http.StatusOK, gin.H{
			"msg": "登录成功",
		})
	})

	router.POST("/signup", func(c *gin.Context) {
		var signUpFrom SignUpForm
		if err := c.ShouldBind(&signUpFrom); err != nil {
			fmt.Println(err.Error())
			c.JSON(http.StatusBadRequest, gin.H{
				"error": err.Error(),
			})
			return
		}

		c.JSON(http.StatusOK, gin.H{
			"msg": "注册成功",
		})
	})

	_ = router.Run(":8083")
}

对比上一小节的结果:
在这里插入图片描述
可以看到,我期望返回的是json 的字符串user,而不是LoginForm.UserUser
怎么解决?
我们需要
LoginForm.User先获取LoginForm.user然后在去掉/删掉LoginForm.user中的LoginForm.得到user即可。
实现如下:
代码2:

package main

import (
	"fmt"
	"net/http"
	"reflect"
	"strings"

	"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"
	en_translations "github.com/go-playground/validator/v10/translations/en"
	zh_translations "github.com/go-playground/validator/v10/translations/zh"
)

var trans ut.Translator

type LoginForm struct {
	User     string `json:"user" binding:"required,min=3,max=10"`
	Password string `json:"password" binding:"required"`
}

type SignUpForm struct {
	Age        uint8  `json:"age" binding:"gte=1,lte=130"`
	Name       string `json:"name" binding:"required,min=3"`
	Email      string `json:"email" binding:"required,email"`
	Password   string `json:"password" binding:"required"`
	RePassword string `json:"re_password" binding:"required,eqfield=Password"` //跨字段
}

func removeTopStruct(fileds map[string]string) map[string]string {
	rsp := map[string]string{}
	for field, err := range fileds {
		rsp[field[strings.Index(field, ".")+1:]] = err
	}
	return rsp
}

func InitTrans(locale string) (err error) {
	//修改gin框架中的validator引擎属性, 实现定制
	if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
		//注册一个获取json的tag的自定义方法
		v.RegisterTagNameFunc(func(fld reflect.StructField) string {
			name := strings.SplitN(fld.Tag.Get("json"), ",", 2)[0]
			if name == "-" {
				return ""
			}
			return name
		})

		zhT := zh.New() //中文翻译器
		enT := en.New() //英文翻译器
		//第一个参数是备用的语言环境,后面的参数是应该支持的语言环境
		uni := ut.New(enT, zhT, enT)
		trans, ok = uni.GetTranslator(locale)
		if !ok {
			return fmt.Errorf("uni.GetTranslator(%s)", locale)
		}

		switch locale {
		case "en":
			en_translations.RegisterDefaultTranslations(v, trans)
		case "zh":
			zh_translations.RegisterDefaultTranslations(v, trans)
		default:
			en_translations.RegisterDefaultTranslations(v, trans)
		}
		return
	}

	return
}

func main() {
	//代码侵入性很强 中间件
	if err := InitTrans("zh"); err != nil {
		fmt.Println("初始化翻译器错误")
		return
	}
	router := gin.Default()
	router.POST("/loginJSON", func(c *gin.Context) {

		var loginForm LoginForm
		if err := c.ShouldBind(&loginForm); err != nil {
			errs, ok := err.(validator.ValidationErrors)
			if !ok {
				c.JSON(http.StatusOK, gin.H{
					"msg": err.Error(),
				})
			}
			c.JSON(http.StatusBadRequest, gin.H{
				"error": removeTopStruct(errs.Translate(trans)),
			})
			return
		}

		c.JSON(http.StatusOK, gin.H{
			"msg": "登录成功",
		})
	})

	router.POST("/signup", func(c *gin.Context) {
		var signUpFrom SignUpForm
		if err := c.ShouldBind(&signUpFrom); err != nil {
			fmt.Println(err.Error())
			c.JSON(http.StatusBadRequest, gin.H{
				"error": err.Error(),
			})
			return
		}

		c.JSON(http.StatusOK, gin.H{
			"msg": "注册成功",
		})
	})

	_ = router.Run(":8083")
}

再来看一下测试结果:
在这里插入图片描述
这样user就是json的字符串类型的值了。

对于
在这里插入图片描述
json它有一个比较特殊的地方,如果是json配置的是横线-的话,‌‌这个对于我们来说它是不应该处理的,
所以说为了严谨性,我们在这里边给它配置一个name给它判断一下,‌‌如果你等于横线-我不处理,这个主要是json tag中的一种约束,‌‌
这里边我就返回一个空就行了,否则的话返回name。‌

然后点击Translate里面看下源码,看一下Translate返回的是一个什么类型:
在这里插入图片描述

在这里插入图片描述
可以看到它的本质就是map[string]string类型,
所以我们可以自定义方法,自己手动处理它。
在这里插入图片描述

22-11 自定义gin中间件

‍什么是中间件?
假设‍‍我希望来统计一下一个函数,比如说注册函数 或者 登录‍‍函数,它的运行时长是多少?

对于我们来说‍‍最简单的方法就是在函数里边,我给它设置一个开始时间,代码运行完成之后,在return之前来给它设置一个结束时间,‍‍
结束时间减去开始时间,得到的就是我们的一个总的时长。

这种做法其实‍‍比较简单,但是它有一个致命的问题,就是的代码侵入性很强,这是非常‍‍不建议做的一件事,想象一下,假设我已经写了几百个接口了,难道我到每一个接口里边都去这样做吗?

这种在我们的gin中,有一个中间件专门来解决这个问题,‍‍中间件的技术在web框架中是非常常用的,几乎每一个web框架都会有中间件来解决这个问题,‍‍中间件这种技术其实是很常见、很‍‍成熟的技术,
中间件它就可以让我们‍‍以不侵入代码的方式去完成这些功能。

问题来了:
怎么使用中间件?
如何自定义中间件?

回忆一下,gin.Default()默认启动了2个中间件:【Logger(), Recovery()】
在这里插入图片描述

如果我不想要它默认的中间件,我想根据需求自己手写中间件,
那么这样写就对了:
router := gin.New()

代码1:

package main

import (
	"fmt"
	"github.com/gin-gonic/gin"
	"net/http"
	"time"
)

func MyLogger() gin.HandlerFunc {
	return func(c *gin.Context) {
		t := time.Now()
		c.Set("example", "123456")
		//让原本改执行的逻辑继续执行
		c.Next()

		end := time.Since(t)
		fmt.Printf("耗时:%V\n", end)
		status := c.Writer.Status()
		fmt.Println("状态", status)
	}
}

func main() {
	router := gin.Default()
	//使用logger和recovery中间件 全局所有
	router.Use(MyLogger())

	router.GET("/ping", func(c *gin.Context) {
		c.JSON(http.StatusOK, gin.H{
			"message": "pong",
		})
	})

	router.Run(":8083")
}

结果:
在这里插入图片描述
在这里插入图片描述

这就是中间件的一个使用。‍‍
下一节给大家分析一下,‍‍中间件的原理是如何做的,我们来看一下源码。‍‍
大家就知道源码,我们一定要懂它的原理过程什么样子的,‍‍不然的话它会出现一些很奇怪的问题,
了解了中间件实现的具体的原理是什么样子,后续在遇到各种问题的时候才会知道如何排查。‍‍

22-12 通过abort终止中间件后续逻辑的执行

‌本小节我们来对gin它的中间件的源码进行分析,‌‌我们来搞懂它的原理。

首先在搞懂原理之前,我们来看一个奇怪的现象,‌‌什么奇怪的现象?就是为什么连return都不能结束后续逻辑的执行。
代码1:

package main

import (
	"fmt"
	"github.com/gin-gonic/gin"
	"net/http"
	"time"
)

func MyLogger() gin.HandlerFunc {
	return func(c *gin.Context) {
		t := time.Now()
		c.Set("example", "123456")
		//让原本改执行的逻辑继续执行
		c.Next()

		end := time.Since(t)
		fmt.Printf("耗时:%V\n", end)
		status := c.Writer.Status()
		fmt.Println("状态", status)
	}
}

func TokenRequired() gin.HandlerFunc {
	return func(c *gin.Context) {
		var token string
		for k, v := range c.Request.Header {
			if k == "X-Token" {
				token = v[0]
			} else {
				fmt.Println(k, v)
			}
		}

		if token != "keegan" {
			c.JSON(http.StatusUnauthorized, gin.H{
				"msg": "未登录",
			})
			// 为什么连return都阻止不了后续逻辑的执行
			return
			//c.Abort()
		}
		c.Next()
	}
}

func main() {
	router := gin.Default()
	//使用logger和recovery中间件 全局所有
	router.Use(TokenRequired())

	router.GET("/ping", func(c *gin.Context) {
		c.JSON(http.StatusOK, gin.H{
			"message": "pong",
		})
	})

	router.Run(":8083")
}

在这里插入图片描述

先给结论,这样做:
在这里插入图片描述
在这里插入图片描述

可以看到,这样做就可以终止后续的逻辑的执行了。

为什么‌‌连return都阻止不了后续逻辑的执行?
这其实是有点‌‌奇怪的,因为return我们都知道它能直接终止当前的函数,那也就是说你终止了,你这个代码肯定是运行不到的,‌‌它为什么还能够运行?
这是一个很奇怪的问题。
这个问题我们在下一小节给大家‌‌来分析一下中间件的具体的执行原理。大家就能够明白了。‌​

22-13 gin的中间件原理源码分析

本小节我们来讲解为什么我必须要通过abort方法来终止‌‌我们中间件后续的逻辑的执行,以及为什么后面我使用return它都能够正确的执行。‌‌

现在我们就来分析一下,如何实现的中间件的逻辑,‌‌首先我们从哪里看?‌
‌我们从Use这个地方看,
点击Use
在这里插入图片描述
然后点击Use
在这里插入图片描述
这个方法的核心逻辑:
在这里插入图片描述
也就是说它实际上会给 group 给它加一个Handlers,‌‌然后 Handles什么类型,点击源码看一下:
在这里插入图片描述
它是一个切片。
所以说当你调用Use这个方法,‌‌把你的中间件给注册进来的时候,它实际上无非就是把你的中间件这个函数‌‌把它放到 Handlers 切片的后边,追加到它的尾部上。‌

我们来画图来看一下:

也就是说当你Use一个中间件的时候,它会在 Handlers 这样一个队列里边,‌‌这个 Handlers 它实际上就是一个函数而已,比如说这里边现在是我们Auth的这样一个组件,‌‌然后你又Use一个logger,然后就放进来一个logger。‌

在这里插入图片描述

‌当你给它配置一个GET的时候,配置一个GET的时候,‌‌点进去看源码:
注意到combineHandlers
combineHandlers它是怎么做的呢?‌‌
在这里插入图片描述
217行有个mergedHandlers,实际上就是如果你给它在这个地方也给它配置了一个函数,‌‌比如说我们叫ping的函数:
在这里插入图片描述
其实点进去combineHandlers
在这里插入图片描述
它这个逻辑就是如果你有一个 Handlers 配置进来之后,‌‌它会计算一下 group里边已经有多少Handlers,和你到底传进了几个Handlers,‌‌然后把你的长度重新算一个长度,然后重新来申请一个新的空间。‌
‌这个空间大小就是它原有的 Handlers的个数len(group.Handlers),加上你传递过来的handlers的个数len(handlers)
然后把它原来的group Handlers‌‌即group.Handlers拷贝到mergedHandlers里边,
最后再把你传递过来的 handlers,从哪个位置开始拷,从[len(group.Handlers):]往后拷,其实就是把你的配置的函数往队列的尾部放,‌‌
也就是往我们的切片的尾部放,

比如说如下配置:
在这里插入图片描述

比如把ping的函数给放到mergedHandlers尾巴上。
假设最终的长度是这样的:
在这里插入图片描述
然后这个长度就是我们的整个的Handlers‌‌,‌‌然后在执行的过程中是如何来执行的?

看代码:
在这里插入图片描述

因为你先使用的是Use【见蓝色部分】,所以说你的 TokenRequired肯定放在第一个,
当然我们不考虑 Default 的情况,‌‌我们就考虑如果没有Default的情况,就先把 TokenRequired放在前面,放在前面就会先执行。

当你执行的过程中,如果你调用c.Next(),‌
在这里插入图片描述

点击源码可以看到,‌‌它会先把 index初始化为0,也就是说它其实在这个地方它有一个游标,‌‌这个游标就是指明当前我应该执行到哪一个函数当中了,向下的箭头就是我的index:
在这里插入图片描述

一开始的时候我执行到Auth的,所以说刚开始的时候我执行的就是Auth。‌
‌如果你在你这里边的函数里边,‌‌你调一个Next,我们看一下源码:
在这里插入图片描述

当你调Next的时候,我就把索引往后移一个‌‌/挪一位,
在这里插入图片描述

如果我判断一下你的 handlers是否‌‌小于你这个队列的长度,即c.index < int8(len(c.handlers))
如果你小于这个长度的话,就说你还没有越界,那我就从handlers把这个函数取出来,把ta调用一下,即c.handlers[c.index](c)

‌所以说当你Auth里边调用Next的话,‌‌它就会把这个地方我往后挪一位,开始调用logger了。
logger里边仍然一样,它会去调用我们这里边的‌‌ Next ,它再往后挪一位,开始调用ping。‌
在这里插入图片描述

假如我不在Auth里边调用Next,改用return作为结束,
结束之后,后面loggerping难道就不调用了吗?‌‌它还在我的切片里边,而且我的index并没有改变,
‌所以说即使你结束了,我的index仍然可以加加,‌‌只是加加的操作不由你驱动了,是由gin驱动了,它仍然会来到 logger,logger里边你不调用也没关系,只要你不去改我的index,我仍然会按照原来的正常的逻辑‌‌往ping执行。
所以说你的return它是你代表你结束了,并不代表我的 gin 后边这一串我都不调用了。‌
在这里插入图片描述
我们要如何真正的结束它,‌‌这种调法比较奇怪,它们都是一个队列,它们是平级的,‌‌这个队列是gin的队列,不是单独的Auth或者logger或者ping的队列。
你可以驱动gin,当你在执行到一定逻辑的时候,没有执行logger,你仍然可以来执行ping,‌‌
‌因为你可以通过Next 来驱动进而执行到ping,
正常情况下,因为logger先于ping放进来,所以说你即使调用 Next,它一定会到ping这里边来,

所以说当你return并不能够改变我这里边的一个关系,‌‌我仍然会被gin驱动着一步一步的往后走,直到我调用完成。‌

如何真正的结束,我结束了你就结束了?
这个逻辑其实比较简单,‌‌我如果把这个index给它指到一个不存在的位置,也就指到最后一个位置,
当你下一次想调的话,你的gin驱动它是通过index来驱动的,‌‌它再往后驱动,它发现这是个空的位置:
在这里插入图片描述
它就认为我没有必要执行了。

所以说你想让它不执行就来控制这里边的index就可以了。

怎么来控制这个index,‌‌它就是我们的c.Abort()方法。‌
我们来看一下c.Abort()源码:
在这里插入图片描述

Abort里边它会把 index 给它改成‌‌ abortIndex ,
在这里插入图片描述
abortIndex是多少?
可以看到它是MaxInt8
MaxInt8我们看一下值是多大,
在这里插入图片描述
大小是127。【2的7次方减1 即128-1=127】

它会一直往后移移移,移到‌‌最大的数,当然你移到最大的数肯定就没法执行了。

这就是gin 中间件执行原理的过程。

22-14 gin返回html

‌本小节我们来介绍一下gin中的html的相关功能,即模板的功能。‌‌
我们来看一下这里边的文档,
官方文档:https://pkg.go.dev/html/template
翻译:https://colobu.com/2019/11/05/Golang-Templates-Cheatsheet/#if/else_%E8%AF%AD%E5%8F%A5

我们这里边就直接上手来使用它,‌‌
我们简单的来画一个图,说明一下这个模板它是一个什么意思。‌

我们来看一下一个web系统当中它的整个请求到返回 是如何来完成的?‌‌

比如说现在这里边有一个浏览器,浏览器它要发起一个请求,‌‌比如说发起我们的主域名的请求,比如说我们现在有个域名,它直接发起了一个请求,
对于我们的一个web框架来说,
首先它得在它的端口上去监听客户端的请求,比如说我们这里边使用8083,你来监听一个请求,你的一个请求发过来,‌‌发过来之后在我内部,比如说我现在是gin,模板语法它并不是gin特有的,‌‌基本上所有的web框架它都会有这些功能的,所以说它本身没有什么‌‌特殊的。

‌我们继续回来8083端口在接收到你的请求之后,这里边的请求你有可能是比如说是一个‌‌URL路径。‌
‌那路径对于我们来说,比如说我要获取一个/goods/list,假设是这样的。
对于我们的gin来说,它接收到这个请求之后,它就能把你这个路径提取出来,‌‌提取出来之后给路由分发器,
这里边有一个路由器,‌‌路由器的话它会维护一张路由表,这个表在我们之前启动整个gin系统的时候,它就已经知道了,‌‌
比如说拿着你原来的请求路由/goods/list到路由表里边去查询一下,最终会映射到一个函数当中,‌‌

路由器找的话,路由器最终会找到一个函数,‌‌假设我们把这个函数把它叫做goodsList,
‌这个函数在拿到你的请求之后,它会执行一系列的操作,
比如说数据库的查询等等,‌‌反正就是一系列的逻辑,
那逻辑拿到之后,它最终要给你返回的是什么?它最终要给你返回的要么就是json,要么就是html。
假设我们这里就返回html。

但是html里面其实是有很多标签的,‌‌所以说goodsList怎么把html给凑出来,
怎么给你凑出一段完整的符合要求的界面?这其实就是我们前端写好的一套模板。‌
在这里插入图片描述
goodsList函数它能够从数据库里边查询到,比如说它查询到mysql,
查询到信息之后,‌‌它肯定要把查询到的信息拼凑成一段htm才能够给你返回。‌

‌所以说goodsList是怎么生成html的?
它得先拿到模板,‌‌拿到模板还不行,它还要把它的从数据库里面查询出来的数据,‌‌也就是大堆数据、变量。‌
‌它从数据库里边先查询,
查询到之后它把这个数据‌‌填充到你的模板里边,
比如说它查询到了姓名就填上了,【就是你CSDN的个人信息的字段】
这个模板它是一段完整的html结构,
但是html结构里面,我要把某些信息,比如说姓名,我要把它从数据库里面取出来,把它填充到具体的地方,
填充完了之后,这个模板机制它会把你‌‌这个模板本身写好的一些模板,以及把你的传递的值给它替换进来,
最终这个模板它就会生成我们的html,‌‌也就是我们要返回的html。

把拿到完整的html你再返回个浏览器。

首先我们要知道的‌‌怎么取出这个模板,
第二怎么把我的变量填充到这里边来,这就是我们要讲解到的模板机制。

代码1:

package main

import (
	"github.com/gin-gonic/gin"
	"net/http"
)

func main() {
	router := gin.Default()

    router.LoadHTMLFiles("templates/index.tmpl")

	router.GET("/index", func(c *gin.Context) {
		c.HTML(http.StatusOK, "index.tmpl", gin.H{
			"title": "CSDN--->代码写注释<---",
		})
	})
	router.Run(":8083")
}

在这里插入图片描述
index.tmpl

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <h1>
        {{ .title }}
    </h1>
</body>
</html>

在这里插入图片描述

简单介绍下代码逻辑:
(1)先启动gin
(2)定义一个处理函数
配置的是/index,
index函数里边功能比较简单,给它配置返回一段模板里边的html,‌‌把简单的值给它填充进来就可以了。

‌现在来看一下怎么来返回html。
可以用c.HTML,这里边第一个参数是什么?‌‌
首先第一个参数就是我要去设置我的这次返回的状态码,返回的是一个200,也就是http.StatusOK。‌
接着要指明我的模板文件在哪里,这个模板文件我们一般都会写成一个独立的文件,‌‌这个文件,一般情况下我们都会放到项目的主目录的templates下边,当然‌‌名称是无所谓的,‌‌
对于go语言来说,它有专门的模板文件,比如说叫index‌‌点tmpl即index.tmpl
‌然后在 index.tmpl 里边去写html的逻辑,‌‌然后注意到这个地方实际上对于我们的go语言来说,‌‌ html你完全可以建立成普通的html文件,比如说叫index.html,在goland编辑器它会自动帮我们生成html代码。
如果我们写这种 index.tmpl,它会有一些提示,相对友好一些,

‌我们把index.html给它拷贝到 index.tmpl中。【tmpl语法跟html的{{}}插值表达式稍微有点不同,一个词,抄呗】

回到代码1,现在我们要指明我们的文件,这个文件我们可以叫index.tmpl,然后我们给它传递一个变量到 index.tmpl里面来【我猜你也想到了—插值表达式{{}}】,‌‌

然后在index.tmpl中变量给它拿过来,怎么来取?
用大括号两个花括号【{{}}】把变量包裹起来。‌

对于Go来说,gin怎么知道你的文件它在哪个目录之下?这就成了一个问题了。

‌因此我们需要手动的来指明一下,它在哪个目录之下来找,用这个方法它叫做router.LoadHTMLFiles,然后给它指明一下是哪个 文件即"templates/index.tmpl"
这个函数的意思就是说LoadHTMLFiles这个方法会‌‌将指定的目录下的文件加载好,‌‌但是一定要注意到这个目录是一个相对路径。

干就完了,运行看效果:
在这里插入图片描述

它抛了HTTP ERROR 500的错误,‌500的错误,也就是服务端的代码出现了异常,如下:
在这里插入图片描述
异常的意思是说它找不到这样一个文件,主要是因为路径的问题。‌
明确了问题,就开始介绍解决方法。
你有没有发现,你在goland中运行main.go文件,它是能跑起来,但是它并没有生成main.exe可执行的二进制文件。【macOS环境准确的说文件名是main,下文不再赘述,为了可读性】
当然,我们在main.go的同级目录下 执行go build main.go会生成main.exe二进制文件。
在这里插入图片描述
我们现在这样跑服务:
在这里插入图片描述
然后刷新一下网页,效果如下:
在这里插入图片描述
可以看到,现在数据就渲染出来了。

一个问题:当文件的路径是相对路径,浏览器访问报500的错,后台提示找不到该路径怎么办?
一个方法:在main.go同级目录执行以下操作:

go build main.go
./main

即可解决问题。

22-15 加载多个html文件

‌在上一小节中我们讲解了如何去返回html内容。本小节将讲解加载多个html文件的处理方法。

首先看一个简单的例子,代码如下:
代码1:
goods.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>
    {{ .name }}
</h1>
</body>
</html>

在这里插入图片描述
main.go

package main

import (
	"github.com/gin-gonic/gin"
	"net/http"
)

func main() {
	router := gin.Default()

    router.LoadHTMLFiles("templates/defaults/index.tmpl","templates/defaults/goods.html")

	router.GET("/index", func(c *gin.Context) {
		c.HTML(http.StatusOK, "index.tmpl", gin.H{
			"title": "CSDN --->代码写注释<---",
		})
	})
	
	router.GET("/goods", func(c *gin.Context) {
		c.HTML(http.StatusOK, "goods.html", gin.H{
			"name": "秀儿",
		})
	})
	_ = router.Run(":8083")
}

在main.go同级目录执行以下操作:

go build main.go
./main

在这里插入图片描述
效果如下:
在这里插入图片描述
在这里插入图片描述

我们点击源码看一下,它是一个省略号的,所以说我是可以传递多个进来的。‌
刚刚已经演示了传递2个文件,
但是现在如果我里边的文件特别多怎么办?‌‌
比如说有10个文件,难道我这里面写10个吗?100个文件我写100个吗?‌

我们现在需要的我要加载这里边的所有的文件,我们这样写就对了:
router.LoadHTMLGlob("templates/**/*")
现在的main.go代码:

package main

import (
	"github.com/gin-gonic/gin"
	"net/http"
)

func main() {
	router := gin.Default()

	router.LoadHTMLGlob("templates/**/*")
    //router.LoadHTMLFiles("templates/defaults/index.tmpl","templates/defaults/goods.html")

	router.GET("/index", func(c *gin.Context) {
		c.HTML(http.StatusOK, "index.tmpl", gin.H{
			"title": "CSDN --->代码写注释<---",
		})
	})

	router.GET("/goods", func(c *gin.Context) {
		c.HTML(http.StatusOK, "goods.html", gin.H{
			"name": "秀儿",
		})
	})
	_ = router.Run(":8083")
}

然后
在main.go同级目录执行以下操作:

go build main.go
./main

然后
看一下效果:
在这里插入图片描述
再来解决一个问题:不同文件夹下同名的html文件的问题。
看例子。
在这里插入图片描述
代码1:
goods/list.html

<!DOCTYPE html>
<html lang="en">
<link rel="stylesheet" href="/static/css/style.css">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <h1>商品列表页</h1>
</body>
</html>

代码2:
users/list.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <h1>用户列表页</h1>
</body>
</html>

代码3:
main.go

package main

import (
	"github.com/gin-gonic/gin"
	"net/http"
)

func main() {
	router := gin.Default()

	router.LoadHTMLGlob("templates/**/*")
    //router.LoadHTMLFiles("templates/defaults/index.tmpl","templates/defaults/goods.html")

	router.GET("/index", func(c *gin.Context) {
		c.HTML(http.StatusOK, "index.tmpl", gin.H{
			"title": "CSDN --->代码写注释<---",
		})
	})

	router.GET("/goods", func(c *gin.Context) {
		c.HTML(http.StatusOK, "goods.html", gin.H{
			"name": "秀儿",
		})
	})

	router.GET("/goods/list", func(c *gin.Context) {
		c.HTML(http.StatusOK, "list.html", gin.H{
			"title": "商品列表页",
		})
	})

	router.GET("/users/list", func(c *gin.Context) {
		c.HTML(http.StatusOK, "list.html", gin.H{
			"title": "用户列表页",
		})
	})
	_ = router.Run(":8083")
}

然后
在main.go同级目录执行以下操作:

go build main.go
./main

然后
看一下效果:
在这里插入图片描述
可以看到有问题。
那根据自己解决问题的逻辑,【自己一定要积累自己的解决问题的思路,操碎了心~因为都是这么过来的】
首先,认识问题,这是一个什么样的问题?
这是一个这样的问题,如下:
在这里插入图片描述
用什么技术点解决呢?
重命名文件名。

这样做就对了。
代码1:
goods/list.html

{{define "goods/list.html"}}
<!DOCTYPE html>
<html lang="en">
<link rel="stylesheet" href="/static/css/style.css">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <h1>商品列表页</h1>
</body>
</html>
{{end}}

代码2:
users/list.html

{{define  "users/list.html"}}
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <h1>用户列表页</h1>
</body>
</html>
{{end}}

代码3:
main.go

package main

import (
	"github.com/gin-gonic/gin"
	"net/http"
)

func main() {
	router := gin.Default()

	router.LoadHTMLGlob("templates/**/*")
    //router.LoadHTMLFiles("templates/defaults/index.tmpl","templates/defaults/goods.html")

	router.GET("/index", func(c *gin.Context) {
		c.HTML(http.StatusOK, "index.tmpl", gin.H{
			"title": "CSDN --->代码写注释<---",
		})
	})

	router.GET("/goods", func(c *gin.Context) {
		c.HTML(http.StatusOK, "goods.html", gin.H{
			"name": "秀儿",
		})
	})

	router.GET("/goods/list", func(c *gin.Context) {
		c.HTML(http.StatusOK, "goods/list.html", gin.H{
			"title": "商品列表页",
		})
	})

	router.GET("/users/list", func(c *gin.Context) {
		c.HTML(http.StatusOK, "users/list.html", gin.H{
			"title": "用户列表页",
		})
	})
	_ = router.Run(":8083")
}

然后
在main.go同级目录执行以下操作:

go build main.go
./main

然后
看一下效果:
在这里插入图片描述
可以看到,文件重名、命名冲突的问题就解决了。就这么简单。

强调一下,如果我们没有在模板中使用define定义,‌‌那么我们就可以使用默认的文件名来找,你如果定义的话,它就会使用你定义的,‌‌这个技术点可以解决我们的文件名冲突的问题。‌

想要自己实现一个非前后端分离的系统,可以使用这里边的模板系统。‌‌

22-16 static静态文件的处理

‍上一小节中我们讲解了templates相关的功能,本小节我们来讲解一下‍‍静态文件,在我们的非前后端开发过程当中,除了这个模板文件之外,还有一个重要的点就是‍‍静态文件,在我们的html文件当中,我们往往会配置一些静态文件,‍‍比如说我在我的html当中会有一些我的图片文件,会有一些我的css文件,我的js文件,‍‍这些都是属于静态资源,静态资源在gin当中如何配置呢?‍‍

假如我现在在我的goods/list页面,我现在‍‍给ta引入一个css文件‍‍,在html当中如何去引用css文件?就是在 head 前面给它加一个link,我们‍‍输了link之后 tab键就可以自动补全。
href我们可以给它配置一个路径,注意到我们前面是一个斜线,‍‍具体写法是:<link rel="stylesheet" href="/static/css/style.css">
代码1:
goods/list.html

{{define "goods/list.html"}}
<!DOCTYPE html>
<html lang="en">
<link rel="stylesheet" href="/static/css/style.css">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <h1>商品列表页</h1>
</body>
</html>
{{end}}

我们原本的意图,是这样新建static路径,‍‍ static 路径之下我给它放一个比如说style,

在这里边我们给他写一个样式,代码如下:
/static/css/style.css

*{
    background-color: aqua;
}

首先注意到在我们的gin当中,它针对静态资源,有一个单独的函数来处理,‍‍即router.Static("/static", "./static")
首先是一个路径,也就是说你只要以/static‍‍开始的URL,我都会去哪个路径下找,还有一个相对路径./static。‍‍
这两个参数的区别是什么?
第一个参数就是你的URL里边只要是以/static‍‍开头,我就会去‍‍当前目录./static下面去找,所以说你后边如果是一个style.css‍‍,我这个文件style.css 只需要放到 static 下边的 css文件夹就行了,然后我这里边它就能够跳转过来找到它。‍‍

在这里插入图片描述
代码2:
main.go

package main

import (
	"github.com/gin-gonic/gin"
	"net/http"
)

func main() {
	router := gin.Default()
	router.Static("/static", "./static")
	router.LoadHTMLGlob("templates/**/*")
    //router.LoadHTMLFiles("templates/defaults/index.tmpl","templates/defaults/goods.html")

	router.GET("/index", func(c *gin.Context) {
		c.HTML(http.StatusOK, "index.tmpl", gin.H{
			"title": "CSDN --->代码写注释<---",
		})
	})

	router.GET("/goods", func(c *gin.Context) {
		c.HTML(http.StatusOK, "goods.html", gin.H{
			"name": "秀儿",
		})
	})

	router.GET("/goods/list", func(c *gin.Context) {
		c.HTML(http.StatusOK, "goods/list.html", gin.H{
			"title": "商品列表页",
		})
	})

	router.GET("/users/list", func(c *gin.Context) {
		c.HTML(http.StatusOK, "users/list.html", gin.H{
			"title": "用户列表页",
		})
	})
	_ = router.Run(":8083")
}

然后
在main.go同级目录执行以下操作:

// 先control + c 终止进程
go build main.go
./main

然后
看一下效果:
在这里插入图片描述
在这里插入图片描述
web框架大同小异,静态资源的URL基本上都是http://127.0.0.1:8083/static这种形式。
这个路径没问题的:
在这里插入图片描述
主要就是用到了这个东西:router.Static("/static", "./static")

以上就是的‍静态资源文件的处理。

22-17 gin的优雅退出

‌本小节来介绍一下如何优雅的退出程序。
首先我们来认识一下什么叫优雅的退出。

这个优雅退出在实际的开发过程中还是比较重要的。‌‌很多人【我】在写程序的时候,‌‌在处理退出的时候并没有太过在意,‌‌大家以后自己去写很多服务的时候,‌‌一定要注意什么叫优雅退出。‌

‌优雅退出的就是当我们关闭程序的时候,‌‌应该做的后续处理。‌

比如说大家以后去处理订单数据等等之类的,你的程序被你强制关闭掉,那有一部分数据你没有处理完,‌‌这个时候你的程序被强制中断了,这样的话你的数据就会出现不一致的情况。‌

‌所以说当我们如果想去关闭一个进程,比如说使用 control + c 或者使用 kill命令去关闭一个进程的时候,这个时候‌‌我们如果能知道用户使用 control + c 或者使用 kill命令,这个时候我赶紧把当前的数据给ta做一个处理,比如说没处理完的数据我‌‌先处理完,然后我再关闭,让你等一下,这要不然就是我把这个数据做一个日志记录等等一系列的操作,‌‌都叫做优雅的退出。

现在假设我用gin去做了一个微服务,这个微服务在一开始之前,在启动之前或者启动之后做一件事,‌‌什么事?就是将‌‌当前的服务的IP地址和端口号注册到注册中心,‌‌也就是服务发现和服务注册中心。

‌我希望我的服务能够被别人发现,我在我的服务启动的时候,我会把它注册进去,‌‌但是这个时候由于我们没有做优雅的推出,导致了我们在结束进程的时候‌‌导致一个什么问题?‌我们当前‌‌的服务停止了以后,并没有告知注册中心说我现在下线了。

注册中心它就会向你‌‌维护一个心跳,它会不停的向你发送请求,如果它发现你挂了的,它会把与你建立的连接断开的。‌

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

代码1:
main.go

package main

import (
	"fmt"
	"github.com/gin-gonic/gin"
	"net/http"
	"os"
	"os/signal"
	"syscall"
)

func main(){
	router := gin.Default()
	router.GET("/",func(c *gin.Context){
		c.JSON(http.StatusOK,gin.H{
			"msg":"pong",
		})
	})
	go func(){
		_ = router.Run(":8083")
	}()

	// 如果想要接收到关闭的信号
	quit := make(chan os.Signal)
	signal.Notify(quit,syscall.SIGINT,syscall.SIGTERM)
	<- quit

    // 处理后续的逻辑
    fmt.Println("关闭server中...")
    fmt.Println("注销服务...")
}

重点讲解:
router.Run(":8083")这个启动模式有一个好处就是启动之后它会一直hang住,我们可以点击源码来看一下,‌‌在这里边它是怎么启动的?它的启动实际上它采用了http,这是go语言内置的一个包:
在这里插入图片描述
是调用 http.ListenAndServe方法来启动。‌

如果我们启动main.go的时候,主进程是不会挂掉的,它会停在这里的,即router.Run(":8083")
如果我想结束它,‌‌我想让我们接收到信号能够处理它,这个时候我们应该怎么来做?‌

‌我们就得把启动的过程‌‌把它放到另外一个协程当中去运行的,这样的话我的主进程就可以等待你的‌‌信号,也就是说你使用 control + c 或者使用 kill命令这种信号,‌‌当你的信号一旦发过来之后,这个时候我直接将我的主的进程退出,router.Run(":8083")就会被退出。‌

‌所以我们用协程来启动它router.Run(":8083")
在这里插入图片描述
启动了协程之后,因为它是一个协程,‌‌所以说我的主进程会仍然继续往下执行,
如果我想要接收到信号量:
在这里插入图片描述
‌然后我要调用最关键的函数叫signal,然后它这里边有一个叫Notify的关键函数,这个函数它能够传递第一个参数,就是我的一个channel。‌
‌第二个参数就是我要处理的信号,比如说我们关键的两个信号,一个就是我们的ctrl + c,‌‌另外一个就是 kill 的信号。

这两个信号在我们的系统当中,我们使用syscall.SIGINT,syscall.SIGTERM,都是固定的,我们就处理这两个信号就行了。‌

‌好了,我们现在有了信号之后,这两个信号任何一个信号有了之后,它就会向channel发起一个消息,我们只需要干一件事,我就是看一下你有没有数据就行了,我不关心你的数据是什么,只要你有数据进来,‌‌我就知道你要退出了。<- quit

‌这个时候我就可以处理后续的逻辑了,‌‌这个逻辑我们就直接用打印的方式来做:
在这里插入图片描述

现在我们‌‌模拟一下,比如说注销服务把它注销了,如果你没有的话,我就等在这儿。‌<- quit
‌你一旦有了,我这个地方<- quit立马就能执行,执行完之后,就会来到打印语句:

在这里插入图片描述
然后来到 },此时主进程关闭,协程也就关闭了,这样的话我们就能够达到一个效果,‌‌就是退出的过程中,也就是说我接受到退出信号【是ctrl + c 还是 kill】,我可以加一系列的后续的处理逻辑,‌‌这个逻辑我们可以把它写成一个函数,当你接收到退出信号之后,我来调用这个函数就可以了。‌

我们来运行一下:
通过 control + c 终止进程
在这里插入图片描述
通过kill pid的方式终止进程:【这里不能使用kill -9 pid的方式,它是强杀命令,跟kill pid关闭还是有区别的,我已经试过了,用 kill pid的方式关闭./main可行】
在这里插入图片描述
在这里插入图片描述

ps -e | grep "./main"
kill pid

这就是优雅的退出。‌

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

代码写注释

请赞赏我

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值