五.中间件和路由
一.中间件
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请求(如GET
、POST
等),执行相应的业务逻辑(可能包括与数据库的交互),然后返回一个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
,他将返回一个内容为yes
的JSON
值,并通过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
框架提供的Set
和Get
方法。
定义一个中间件:
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,
})
}
此时如果我们想输出user
的Name
字段,直接使用下述语句是不行的:
1
2
user, _ := c.Get("user")
c.JSON(200, gin.H{"username": user.Name})
因为此时的user
为any
类型,而非User
类型。
想要达成原本的目的,则需类型断言。
1
2
3
user, _ := c.Get("user")
U, _ := user.(User)
c.JSON(200, gin.H{"username": U.Name})
此时,则能成功输出User
的Name
字段。
二.路由
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"})
})
}
上述的代码中,我们创建了一个名为api
的Group
,其类型为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
会将触发panic
的router
状态码改为500,且不会影响其他router
的访问,可以认为是一种自修复。
Default
函数相较于New
函数更加稳定且可查询。