阅读 340

gin中间件使用及源码解析(基于gin框架开源项目)

在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中间件的高级实现,例如职责链模式,在特定的场景下非常有用和高效,大家也可以自行查阅相关资料。


作者:simon8410
链接:https://juejin.cn/post/7034338727883177997


文章分类
代码人生
版权声明:本站是系统测试站点,无实际运营。本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 XXXXXXo@163.com 举报,一经查实,本站将立刻删除。
相关推荐