Yihui’s Blog

gin 中间件

Gin是一个轻便灵活的Web框架,它具有非常高的拓展性,并且对中间件的支持非常友好。在Gin中,所有的接口请求都要经过中间件的处理,这使得开发者可以自定义实现很多功能和逻辑。尽管Gin本身自带的功能很少,但是由第三方社区开发的Gin拓展中间件十分丰富。

中间件是Gin框架中非常重要的一部分,它可以在请求到达处理函数之前或之后执行一些操作,比如日志记录、权限验证、数据压缩等。通过使用中间件,开发者可以将一些通用的功能抽离出来,使得代码更加清晰和易于维护。

Gin框架本身提供了一些常用的中间件,比如Logger中间件用于记录请求日志,Recovery中间件用于恢复应用程序的panic。除此之外,Gin还支持开发者自定义中间件,这意味着可以根据具体的业务需求来编写自己的中间件,从而实现更加个性化的功能。

中间件本质上是一个接口处理器。

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

从某种意义上来说,每一个请求对应的处理器也是中间件,只不过是作用范围非常小的局部中间件。

func Default() *Engine {
   debugPrintWARNINGDefault()
   engine := New()
   engine.Use(Logger(), Recovery())
   return engine
}

在gin的源代码中,Default函数返回的默认Engine默认使用两个中间件Logger()和Recovery()。如果不想使用默认的中间件,可以使用gin.New()来代替。

全局中间件

全局中间件即作用范围为全局,整个系统所有的请求都会经过此中间件。

func GlobalMiddleware() gin.HandlerFunc {
   return func(ctx *gin.Context) {
      fmt.Println("全局中间件被执行...")
   }
}

先创建一个闭包函数来创建中间件,再通过Engine.Use()来注册全局中间件。

func main() {
   e := gin.Default()
   // 注册全局中间件
   e.Use(GlobalMiddleware())
   v1 := e.Group("/v1")
   {
      v1.GET("/hello", Hello)
      v1.GET("/login", Login)
   }
   v2 := e.Group("/v2")
   {
      v2.POST("/update", Update)
      v2.DELETE("/delete", Delete)
   }
   log.Fatalln(e.Run(":8080"))
}

测试

curl --location --request GET 'http://localhost:8080/v1/hello'

输出

[GIN-debug] Listening and serving HTTP on :8080
全局中间件被执行...
[GIN] 2022/12/21 - 11:57:52 | 200 |       538.9µs |             ::1 | GET      "/v1/hello"

Next()方法

局部中间件

局部中间件即作用范围为局部,系统中局部的请求会经过此中间件。局部中间件可以注册到单个路由上,不过更多时候是注册到路由组上。

func main() {
   e := gin.Default()
   // 注册全局中间件
   e.Use(GlobalMiddleware())
   // 注册路由组局部中间件
   v1 := e.Group("/v1", LocalMiddleware())
   {
      v1.GET("/hello", Hello)
      v1.GET("/login", Login)
   }
   v2 := e.Group("/v2")
   {
      // 注册单个路由局部中间件
      v2.POST("/update", LocalMiddleware(), Update)
      v2.DELETE("/delete", Delete)
   }
   log.Fatalln(e.Run(":8080"))
}

测试

curl --location --request POST 'http://localhost:8080/v2/update'

输出

全局中间件被执行...
局部中间件被执行
[GIN] 2022/12/21 - 12:05:03 | 200 |       999.9µs |             ::1 | POST     "/v2/update"

中间件原理

Gin中间的使用和自定义非常容易,其内部的原理也比较简单,为了后续的学习,需要简单的了解下内部原理。Gin中的中间件其实用到了责任链模式,Context中维护着一个HandlersChain,本质上是一个[]HandlerFunc,和一个index,其数据类型为int8。在Engine.handlerHTTPRequest(c *Context)方法中,有一段代码表明了调用过程:gin在路由树中找到了对应的路由后,便调用了Next()方法。

if value.handlers != nil {
   // 将调用链赋值给Context
   c.handlers = value.handlers
   c.fullPath = value.fullPath
   // 调用中间件
   c.Next()
   c.writermem.WriteHeaderNow()
   return
}

Next()的调用才是关键,Next()会遍历路由的handlers中的HandlerFunc 并执行,此时可以看到index的作用就是记录中间件的调用位置。其中,给对应路由注册的接口函数也在handlers内,这也就是为什么前面会说接口也是一个中间件。

func (c *Context) Next() {
   // 一进来就+1是为了避免陷入递归死循环,默认值是-1
   c.index++
   for c.index < int8(len(c.handlers)) {
      // 执行HandlerFunc
      c.handlers[c.index](c)
      // 执行完毕,index+1
      c.index++
   }
}

修改一下Hello()的逻辑,来验证是否果真如此

func Hello(c *gin.Context) {
   fmt.Println(c.HandlerNames())
}

输出结果为

[github.com/gin-gonic/gin.LoggerWithConfig.func1 github.com/gin-gonic/gin.CustomRecoveryWithWriter.func1 main.GlobalMiddleware.func1 main.LocalMiddleware.func1 main.Hello]

可以看到中间件调用链的顺序为:Logger -> Recovery -> GlobalMiddleware -> LocalMiddleWare -> Hello,调用链的最后一个元素才是真正要执行的接口函数,前面的都是中间件。

提示

在注册局部路由时,有如下一个断言

finalSize := len(group.Handlers) + len(handlers) //中间件总数
assert1(finalSize < int(abortIndex), "too many handlers")

其中abortIndex int8 = math.MaxInt8 >> 1值为63,即使用系统时路由注册数量不要超过63个。

计时器中间件

在知晓了上述的中间件原理后,就可以编写一个简单的请求时间统计中间件。

func TimeMiddleware() gin.HandlerFunc {
   return func(context *gin.Context) {
      // 记录开始时间
      start := time.Now()
      // 执行后续调用链
      context.Next()
      // 计算时间间隔
      duration := time.Since(start)
      // 输出纳秒,以便观测结果
      fmt.Println("请求用时: ", duration.Nanoseconds())
   }
}

func main() {
	e := gin.Default()
	// 注册全局中间件,计时中间件
	e.Use(GlobalMiddleware(), TimeMiddleware())
	// 注册路由组局部中间件
	v1 := e.Group("/v1", LocalMiddleware())
	{
		v1.GET("/hello", Hello)
		v1.GET("/login", Login)
	}
	v2 := e.Group("/v2")
	{
		// 注册单个路由局部中间件
		v2.POST("/update", LocalMiddleware(), Update)
		v2.DELETE("/delete", Delete)
	}
	log.Fatalln(e.Run(":8080"))
}

测试

curl --location --request GET 'http://localhost:8080/v1/hello'

输出

请求用时:  517600

一个简单的计时器中间件就已经编写完毕了,后续可以凭借自己的摸索编写一些功能更实用的中间件。

Maintained by · YihuiEdit on GitHub