一文讲透gin中间件使用及源码解析

2,539 阅读4分钟

这是我参与11月更文挑战的第10天,活动详情查看:2021最后一次更文挑战

pexels-charles-roth-2797318.jpg

在Gin框架中,中间件可谓是其精髓。一个个中间件组成一条中间件链,对HTTP Request请求进行拦截处理,实现了逻辑的解耦和分离。中间件之间互相独立,每个中间件只需要处理各自需要处理的事情即可。今天我们来详细地介绍Gin中间件的使用和原理。

中间件使用介绍

默认中间件

一般可以通过Gin提供的默认函数,来构建一个自带默认中间件的*Engine

r := gin.Default()

Default函数会默认设置两个系统中间件,即Logger 和 Recovery,实现打印日志输出和painc处理。

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

从第4行可以看到,Gin是通过Use方法设置中间件的,它接收一个可变参数,所以我们同时可以设置多个中间件。

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

这时可以看到,一个Gin的中间件,其实就是Gin定义的一个HandlerFunc, 而它跟普通的处理器没有两样,比如:

r.GET("/", func(c *gin.Context) {
		fmt.Println("hello world")
		c.JSON(200, "")
	})

后面的func(c *gin.Context)这部分其实就是一个HandlerFunc

自定义中间件

在上文中我们已经知道,Gin的中间件其实就是一个HandlerFunc, 那么我们只要实现一个HandlerFunc,就可以实现一个自定义的中间件。

现在假设我们要统计每次请求的执行时间,应该怎么定义这个中间件呢?

func costTime() gin.HandlerFunc {
	return func(c *gin.Context) {
		//请求前获取当前时间
		nowTime := time.Now()

		//请求处理
		c.Next()

		log.Printf("the request URL %s cost %v", c.Request.URL.String(), time.Since(nowTime))
	}
}

然后通过在服务初始化时使用该中间件。

func main() {
	r := gin.New()

	r.Use(costTime()) // 使用自定义中间件

	r.GET("/", func(c *gin.Context) {
		c.JSON(200, "hello world")
	})

	r.Run(":8080")
}

效果示例如下:

the request URL / cost 1.003µs

原理解析

gin框架涉及中间件相关有4个常用的方法,它们分别是c.Next()c.Abort()c.Set()c.Get()

中间件的注册

首先看一下默认中间件的初始化实现流程:

func Default() *Engine {
	debugPrintWARNINGDefault()
	engine := New()
	engine.Use(Logger(), Recovery())  // 默认注册的两个中间件
	return engine
}

继续往下查看一下Use()函数的代码:

func (engine *Engine) Use(middleware ...HandlerFunc) IRoutes {
	engine.RouterGroup.Use(middleware...)  // 实际上还是调用的RouterGroup的Use函数
	engine.rebuild404Handlers() // 系统其他插件
	engine.rebuild405Handlers() // 系统其他插件
	return engine
}

从下方的代码可以看出,注册中间件其实就是将中间件函数追加到group.Handlers中:

func (group *RouterGroup) Use(middleware ...HandlerFunc) IRoutes {
	group.Handlers = append(group.Handlers, middleware...)
	return group.returnObj()
}

而我们注册路由时会将对应路由的函数和之前的中间件函数结合到一起:

func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes {
	absolutePath := group.calculateAbsolutePath(relativePath)
	handlers = group.combineHandlers(handlers)  // 将处理请求的函数与中间件函数结合
	group.engine.addRoute(httpMethod, absolutePath, handlers)
	return group.returnObj()
}

其中结合操作的函数内容如下,注意观察这里是如何实现拼接两个切片得到一个新切片的。

const abortIndex int8 = math.MaxInt8 / 2

func (group *RouterGroup) combineHandlers(handlers HandlersChain) HandlersChain {
	finalSize := len(group.Handlers) + len(handlers)
	if finalSize >= int(abortIndex) {  // 这里有一个最大限制
		panic("too many handlers")
	}
	mergedHandlers := make(HandlersChain, finalSize)
	copy(mergedHandlers, group.Handlers)
	copy(mergedHandlers[len(group.Handlers):], handlers)
	return mergedHandlers
}

也就是说,我们会将一个路由的中间件函数和处理函数结合到一起组成一条处理函数链条HandlersChain,而它本质上就是一个由HandlerFunc组成的切片:

type HandlersChain []HandlerFunc

中间件的执行

我们在上面路由匹配的时候见过如下逻辑:

value := root.getValue(rPath, c.Params, unescape)
if value.handlers != nil {
  c.handlers = value.handlers
  c.Params = value.params
  c.fullPath = value.fullPath
  c.Next()  // 执行函数链条
  c.writermem.WriteHeaderNow()
  return
}

其中c.Next()就是很关键的一步,它的代码很简单:

func (c *Context) Next() {
	c.index++
	for c.index < int8(len(c.handlers)) {
		c.handlers[c.index](c)
		c.index++
	}
}

从上面的代码可以看到,这里通过索引遍历HandlersChain链条,从而实现依次调用该路由的每一个函数(中间件或处理请求的函数)。

gin_middleware1

我们可以在中间件函数中通过再次调用c.Next()实现嵌套调用(func1中调用func2;func2中调用func3),

gin_middleware2

或者通过调用c.Abort()中断整个调用链条,从当前函数返回。

func (c *Context) Abort() {
	c.index = abortIndex  // 直接将索引置为最大限制值,从而退出循环
}

c.Set()/c.Get()

c.Set()c.Get()这两个方法多用于在多个函数之间通过c传递数据的,比如我们可以在认证中间件中获取当前请求的相关信息(user信息等)通过c.Set()存入c,然后在后续处理业务逻辑的函数中通过c.Get()来获取当前请求的用户。c就像是一根管道,将该次请求相关的所有的函数都串起来了。

image-20211125105938030

总结

在本文中我们学习了gin中间件的使用及实现原理,也学习了如何自定义中间件,需要特别指出的是中间件在后端服务开发中有非常广泛的含义,大家如果感兴趣可以自行搜索,后续有机会我们再单独介绍后端中间件的使用和框架。还有一些gin中间件的高级实现,例如职责链模式,在特定的场景下非常有用和高效,大家也可以自行查阅相关资料。

参考资料

  1. www.flysnow.org/2020/06/28/…
  2. juejin.cn/post/698720…