GoWeb开发的gin框架学习 part five

Posted by OuterCyrex on June 5, 2024

五.中间件和路由

一.中间件

1.引入

经过观察GET等方法的源码我们会发现:

1
2
3
func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes {
	return group.handle(http.MethodGet, relativePath, handlers)
}

GET为例,我们发现他可以接受一个相对路径和多个HandlerFunc

如:

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

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

func main() {
	router := gin.Default()
	router.GET("/", func(c *gin.Context) {
		c.JSON(200, gin.H{"message": "1"})
	}, func(c *gin.Context) {
		c.JSON(200, gin.H{"message": "2"})
	})
	_ = router.Run(":8080")
}

该程序的返回值为:

1
{"message": "1"}{"message": "2"}

由此我们可知,GET方法对于每一个HandlerFunc函数都会进行一次响应。因此,我们可以选取其中的一些函数作为中间件,来对前端传入的数据进行检测等操作。

注意,GET方法在处理多个HandlerFunc函数时依照线性的先后顺序,先进先输出。


一些概念的阐释:

a.视图

视图 (view),通常负责处理用户请求并生成相应的响应图函数视图类接收HTTP请求(如GETPOST等),执行相应的业务逻辑(可能包括与数据库的交互),然后返回一个HTTP响应,这个响应通常是一个HTML页面、JSON数据或其他格式的数据。

b.中间件

中间件 (Middleware)是一个可以介入HTTP请求和响应处理过程的程序。在Web开发框架中,中间件通常用于处理全局性的任务,如日志记录身份验证异常处理等。


2.拦截

通过abort方法,我们可以对响应的进程进行拦截

1
2
3
4
5
6
7
8
9
10
11
12
func abort(c *gin.Context) {
	c.JSON(200, gin.H{"msg": "yes"})
	c.Abort()
}

func main() {
	router := gin.Default()
	router.GET("/", abort, func(c *gin.Context) {
		c.JSON(200, gin.H{"msg": "No"})
	})
	_ = router.Run(":8080")
}

这里我们定义了一个函数abort,他将返回一个内容为yesJSON值,并通过c.Abort()GET的进程进行拦截

因此,其输出内容应为:

1
{"msg": "yes"}

此处GET的进程在abort函数处被拦截,所以输出"msg":"No的函数未能响应

abort函数的拦截是在该函数彻底完成后才开始的。

即下述代码能够正常输出

1
2
3
4
5
6
7
8
router.GET("/", func(c *gin.Context) {
		c.JSON(200, gin.H{"msg": "yes"})
	}, func(c *gin.Context) {
		c.Abort() //此处abort但该函数仍会继续进行
		c.JSON(200, gin.H{"msg": "No"})
	})
//返回值为:
{"msg":"yes"}{"msg":"No"}

3.Next

通过gin框架提供的Next方法,我们可以轻松的将请求中间件响应中间件进行划分。

首先定义一下请求中间件相应中间件


a.请求中间件

请求中间件(Request Middleware)是在HTTP请求到达视图函数之前被调用的中间件

b.响应中间件

响应中间件(Response Middleware)是在HTTP响应被返回给客户端之前被调用的中间件


我们先定义两个中间件:

1
2
3
4
5
6
7
8
9
10
11
func MiddleWare1(c *gin.Context) {
	fmt.Println("first In")
	c.Next()
	fmt.Println("first Out")
}

func MiddleWare2(c *gin.Context) {
	fmt.Println("second In")
	c.Next()
	fmt.Println("second Out")
}

然后运行GET请求:

1
2
3
router.GET("/", MiddleWare1, func(c *gin.Context) {
		c.JSON(200, gin.H{"msg": "I am View Func"})
	}, MiddleWare2)

我们忽略返回的JSON值,终端给出的返回值为:

1
2
3
4
first In
second In
second Out
first Out

我们会发现,c.Next()执行了某种类似的执行顺序。

但实际上Gin并没有使用显式的栈结构来管理中间件的执行,但整体是采取先进后出的原则。Gin通过维护一个中间件列表,并根据当前处理到的位置来决定是否调用 c.Next() 来继续执行后续中间件。

Abort方法也受Next执行顺序影响。

1
2
3
4
5
router.GET("/", MiddleWare1, func(c *gin.Context) {
		c.JSON(200, gin.H{"msg": "I am View Func"})
		c.Abort()
    //在视图函数中插入Abort,则视图函数后的中间件无法被执行
	}, MiddleWare2)

上述代码的输出结果为:

1
2
first In
first Out

注意,next只是决定了语句的执行顺序,而中间件函数本身是已经被执行了的。一旦Abort方法位于next之后,即使按照执行顺序Abort函数应该被先执行,但他也无法拦截next之后的那些响应中间件

1
2
3
4
5
6
func MiddleWare2(c *gin.Context) {
	fmt.Println("second In")
	c.Next()
	c.Abort()
	fmt.Println("second Out")
}

此处Abort是在所有中间件函数视图函数均已被执行之后才执行的,所以他无法拦截任何一个函数。

输出结果:

1
2
3
4
first In
second In
second Out
first Out

通过Next方法,我们也可以对视图函数进行计时

1
2
3
4
5
6
func TimeMiddleware(c *gin.Context) {
	start := time.Now()
	c.Next()
	end := time.Now()
	fmt.Printf("耗时%d秒", end.Sub(start)/time.Second)
}

4.注册全局中间件

此前的过程是为路由单独注册中间件。接下来我们将注册全局中间件

首先,我们声明一个中间件函数

1
2
3
func MiddleWare(c *gin.Context) {
	fmt.Println("MiddleWare used")
}

主函数为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func main() {
	router := gin.Default()

	router.Use(MiddleWare)

	router.GET("/test1", func(c *gin.Context) {
		c.JSON(200, gin.H{"msg": "test1"})
	})
	router.GET("/test2", func(c *gin.Context) {
		c.JSON(200, gin.H{"msg": "test2"})
	})

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

此时,忽略json返回值,每当我们访问router时,MiddleWare函数都会响应一次。

1
2
3
4
5
6
//访问http://localhost:8080
MiddleWare used
//访问http://localhost:8080/test1
MiddleWare used
//访问http://localhost:8080/test2
MiddleWare used

其中Use方法的传参形式如下,即可以接受多个HandlerFunc

1
func (engine *Engine) Use(middleware ...HandlerFunc) IRoutes

多个HandleFunc也是按线性顺序,先进先执行

5.中间件传输数据

很多时候我们需要中间件来传出某些信号或数据

此时可以使用gin框架提供的SetGet方法。

定义一个中间件

1
2
3
4
func sendMW(c *gin.Context) {
	fmt.Println("Data Sent")
	c.Set("name", "Outer")
}

主函数

1
2
3
4
5
6
7
8
9
10
11
12
func main() {
	router := gin.Default()

	router.Use(sendMW)

	router.GET("/test", func(c *gin.Context) {
		name, _ := c.Get("name")
		c.JSON(200, gin.H{"name": name})
	})

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

上述过程中间件sendMW每响应一次,都会传出一个"name"值,其value"Outer",之后通过Get方法进行接受并用json格式输出。

1
2
//访问http://localhost:8080/test
{"name":"Outer"}

注:Get会返回接受到的any类型以及一个bool值。


补充:

由于Get方法返回的是any类型,因此若传入的是结构体,则无法直接对结构体的某个字段进行读取。

比如:

1
2
3
4
5
6
7
8
9
10
11
type User struct {
	Name string
	Age  int
}
func sendMW(c *gin.Context) {
	fmt.Println("Data Sent")
	c.Set("user", User{
		Name: "Outer",
		Age:  18,
	})
}

此时如果我们想输出userName字段,直接使用下述语句是不行的:

1
2
user, _ := c.Get("user")
c.JSON(200, gin.H{"username": user.Name})

因为此时的userany类型,而非User类型。

想要达成原本的目的,则需类型断言

1
2
3
user, _ := c.Get("user")
U, _ := user.(User)
c.JSON(200, gin.H{"username": U.Name})

此时,则能成功输出UserName字段。

二.路由

1.路由分组

通过对路由进行分组,可以实现对路由统一操作

1
2
3
4
5
6
7
8
9
api := router.Group("api")
{
	api.GET("/test1", func(c *gin.Context) {
		c.JSON(200, gin.H{"msg": "test1"})
	})
	api.GET("/test2", func(c *gin.Context) {
		c.JSON(200, gin.H{"msg": "test2"})
	})
}

上述的代码中,我们创建了一个名为apiGroup,其类型为RouterGroup

1
2
3
4
5
6
type RouterGroup struct {
    Handlers HandlersChain
    basePath string
    engine   *Engine
    root     bool
}

由于创建了一个组,我们便可以通过在url中加入/api来访问组内的视图函数

1
2
3
4
//访问http://localhost:8080/api/test1
{"msg":"test1"}
//访问http://localhost:8080/api/test2
{"msg":"test2"}

通过Group方法,我们可以更方便的进行访问操作。

例如:

1
2
3
4
5
6
7
8
9
10
func ShowUser(g *gin.RouterGroup) {
	user := g.Group("user")
	{
		user.POST("/info", func(c *gin.Context) {
			var user User
			_ = c.ShouldBindJSON(&user)
			c.JSON(200, user)
		})
	}
}

上述代码封装了一个user, 并令其返回接受到的json数据。

1
2
3
4
api := router.Group("api")
{
	ShowUser(api)
}	

通过上述操作,我们可以得到以下返回值:

1
2
3
4
5
6
7
8
9
10
11
//访问http://localhost:8080/api/user/info
//输入
{
    "username":"Outer",
    "age":18
}
//返回
{
    "username":"Outer",
    "age":18
}

2.路由分组中间件

通过在中加入中间件,可以实现对特定内容进行校验等中间件操作。

1
2
3
4
5
6
7
8
9
func MiddleWare(c *gin.Context) {
	token := c.GetHeader("token")
	if token == "123" {
		c.Next()
		return
	}
	c.JSON(200, gin.H{"msg": "failed"})
	c.Abort()
}

上述的代码通过检验Header"token"字段是否为"123"来实现校验。若"token"不为"123",则返回{"msg": "failed"}并拦截的剩余操作。

1
2
3
4
5
6
7
8
9
10
func ShowUser(g *gin.RouterGroup) {
	user := g.Group("user").Use(MiddleWare)
	{
		user.POST("/info", func(c *gin.Context) {
			var user User
			_ = c.ShouldBindJSON(&user)
			c.JSON(200, user)
		})
	}
}

仍以之前的读取数据和输出的函数为例。这样每次访问该时,都会运行一次中间件来检测"token"字段。

1
2
3
4
5
//注:
user := g.Group("user").Use(MiddleWare)
//是下述语句的复合形式,也可以写作下述语句
user := g.Group("user")
user.Use(MiddleWare)

输出结果:

1
2
3
4
//输入时Header无token=123
{"msg":"failed"}
//输入时Header的token字段为123
{"username":"Outer","age":18}

中间件进行分组操作,可以便于对特定操作进行校验,其余操作不需要参与校验

1
2
3
4
5
6
7
api := router.Group("api")
	{
		ShowUser(api)
		api.GET("/login", func(c *gin.Context) {
			c.JSON(200, gin.H{"msg": "login"})
		})
	}

showUser函数如上方的定义,则此时我们若访问localhost:8080/api/login,由于中间件是被封装在showUser函数内部,所以我们在外部访问时并不会被校验

1
2
//访问localhost:8080/api/login
{"msg": "login"}

也可以通过闭包来自定义中间件的返回值。

3.什么是gin.Default

源码中,gin.Default会返回一个*Engine类型

1
2
3
4
5
6
func Default(opts ...OptionFunc) *Engine {
	debugPrintWARNINGDefault()
	engine := New()
	engine.Use(Logger(), Recovery())
	return engine.With(opts...)
}

在初始化时,Default会使用两个中间件

其中,Logger()会返回日志信息Recovery会将触发panicrouter状态码改为500,且不会影响其他router访问,可以认为是一种自修复

Default函数相较于New函数更加稳定可查询