通过上个章节我们已经学会了中间件的用法,在 gin 框架中路由的回调函数、中间件回调函数本质都是一样的,我相信大家使用已经没有任何问题了,那么在本章节我们就由浅入深,继续深度学习中间件.

基础用法

1.中间件的加载(注册)

注意:Use 函数仅仅只是负责加载(注册)中间件,不负责调用.

  1. // 路由可以载入很多个中间件,很多个到底是多少个?
  2. // 想知道答案,那我们继续追踪gin源码去揭晓答案,这里请带着你的疑问向后学习
  3. backend.Use(authorization.CheckTokenAuth(), 下一个中间件,继续下一个中间件, 省略很多个...)

进阶学习

后续知识点对于初学者来说存在一定的难度,主要是因为在 gin 中,加载的中间件函数: func ( *gin.Context){ } 在后续执行时会和注册的路由回调函数在同一个逻辑处执行,中间件函数、路由回调函数都是平行关系,不同的区别是先后顺序不同,gin 对这块逻辑处理是相同的方式,这就需要我们从路由开始追踪,因此涉及到的代码会比较多,过程比较复杂.
学完本章节,gin 最核心的主线逻辑也就彻底搞明白了。
这个过程其实就是对一个 request -> response 的全过程源代码剖析,难度比较大,

2.gin 中间件加载过程

  1. // Use 的作用就是首先载入中间件回调函数:func(*Context)
  2. // 首先存储起来,然后等着被后续逻辑调用
  3. // 我们使用 goland ,ctrl+鼠标左键,点击 Use 继续追踪gin源码
  4. func (group *RouterGroup) Use(middleware ...HandlerFunc) IRoutes {
  5. group.Handlers = append(group.Handlers, middleware...)
  6. return group.returnObj()
  7. }
  8. // group.Handlers 定义的最终原型如下,本质上就是 func(*Context) 的切片,存储很多个回调函数
  9. type HandlersChain []HandlerFunc // type HandlerFunc func(*Context)
  10. // 学习到这里,你必须知道的就是:
  11. // gin的中间件回调函数全部被存储在 group.Handlers 变量了
  12. // 这里您对该变量有印象就行,后面还需要继续使用该变量

学习到这里,我们已经知道中间件处理函数被 Use 函数注册了在了 group.Handlers 变量存储起来了,Use 函数可以加载很多个中间件,究竟是多少个,这里我们依然不知道他的具体数量,还有它们什么时候执行,我们也不知道 … …
我们只看见 Use 函数最后返回了group.returnObj() ,它是所有路由的处理接口:IRoutes ,已经结束了,我们无法向下追踪了,那就只能从其他地方入手追踪了,既然中间件是加载在具体的路由前面,那么它肯定在某个具体的路由被访问时执行。

3.gin 中间件执行逻辑

这个过程很漫长,逻辑比较复杂,我们分步骤分析,去追踪 .

3.1 一个具体的路由地址被请求后的 gin 源码究竟是什么

3.1.1 定义一个具体的路由以及回调函数

  1. // 1.省略其它无关代码
  2. users.GET("list", func (c *gin.Context){
  3. // 编写该路由路径对应的业务处理回调函数
  4. // 我们省略具体过程 ... ...
  5. })

3.1.2 **users.GET** 背后的 gin 源码追踪分析

  1. // 2.在 goland 中,ctrl+鼠标左键 点击 GET 函数,源代码如下:
  2. func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes {
  3. // 很明显该函数是 GET + relativePath(开发者定义的路由路径)对应的业务处理逻辑
  4. // 与中间件一样,他的请求回调函数也可以有很多个,具体数量不知道...
  5. // 那么就需要继续追踪 group.handle 源代码
  6. return group.handle(http.MethodGet, relativePath, handlers)
  7. }
  8. // 3.group.handle 函数源码
  9. func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes {
  10. // 将定义的路由相对路径拼接为完整的绝对路径
  11. // 因为一个完整的路径往往和前面定义的路由组路径有关系,因此需要一系列拼接
  12. absolutePath := group.calculateAbsolutePath(relativePath)
  13. // 针对完整路由路径将关联的回调函数全部组合出来
  14. // 究竟如何组合,后续继续追踪源码
  15. handlers = group.combineHandlers(handlers)
  16. // 将请求方式(GET、POST等)结合完整路径作为key,处理函数作为 value
  17. // 以 key => value 的形式注册,value 可以是很多个回调函数
  18. group.engine.addRoute(httpMethod, absolutePath, handlers)
  19. return group.returnObj()
  20. }
  21. //4. group.combineHandlers 源代码追踪
  22. func (group *RouterGroup) combineHandlers(handlers HandlersChain) HandlersChain {
  23. // 看到这里还记得 group.Handlers 变量吗?如果不记得请查看本章节 2 标题部分
  24. // finalSize 表示 中间件回调函数的数量 + 具体路由的回调函数数量的总和
  25. finalSize := len(group.Handlers) + len(handlers)
  26. // 如果 finalSize >= abortIndex 就会发生panic,
  27. // abortIndex 的定义值: math.MaxInt8 / 2 ,
  28. // 在go语言中,官方定义: MaxInt8 = 1<<7 - 1, 表示 1*(2^7)-1,最终值:127
  29. // 那么 abortIndex = 127/2 取整 = 63
  30. // 至此我们终于知道, gin 的中间件函数数量 + 路由回调函数的数量总和最大允许 63 个.
  31. // 为什么是 63 个回调函数不是62? 因为回调函数的存储索引是从0开始的,最大索引为:62
  32. if finalSize >= int(abortIndex) {
  33. panic("too many handlers")
  34. }
  35. // 以上条件检查全部通过后,将中间件回调函数和路由回调函数全部合并在一起存储
  36. // HandlersChain 本质就是 [] func(*Context)
  37. mergedHandlers := make(HandlersChain, finalSize)
  38. // group.Handlers 是中间件函数,他在 mergedHandlers 中存储的顺序靠前,也就是索引比较小
  39. copy(mergedHandlers, group.Handlers)
  40. // handlers 是路由回调函数,他的存储位置比中间函数靠后
  41. copy(mergedHandlers[len(group.Handlers):], handlers)
  42. // 最终返回中间件函数+路由函数组合在一起的全部回调函数
  43. return mergedHandlers
  44. }
  45. //5. group.engine.addRoute 源码分析
  46. // gin的路由是一个很复杂的路由前缀树算法模型,完整过程很复杂
  47. // 这里我们主要追踪路由以及回调函数的 存储/注册 过程
  48. func (engine *Engine) addRoute(method, path string, handlers HandlersChain) {
  49. assert1(path[0] == '/', "path must begin with '/'")
  50. assert1(method != "", "HTTP method can not be empty")
  51. assert1(len(handlers) > 0, "there must be at least one handler")
  52. debugPrintRoute(method, path, handlers)
  53. root := engine.trees.get(method)
  54. if root == nil {
  55. root = new(node)
  56. root.fullPath = "/"
  57. engine.trees = append(engine.trees, methodTree{method: method, root: root})
  58. }
  59. // 在这里,gin 将我们定义的完整路径和回调函数进行了注册
  60. // 源码后续继续追、分析
  61. root.addRoute(path, handlers)
  62. // Update maxParams
  63. if paramsCount := countParams(path); paramsCount > engine.maxParams {
  64. engine.maxParams = paramsCount
  65. }
  66. }
  67. //6.root.addRoute(path, handlers) 函数在按照键 => 值进行注册路由与回调函数时调用了很多其他函数
  68. // 这里我将过程函数名字列举如下
  69. func (n *node) addRoute(path string, handlers HandlersChain) {
  70. // 省略其他无关代码
  71. // 又继续调用如下函数
  72. n.insertChild(path, fullPath, handlers)
  73. // ... ...
  74. }
  75. // 省略很多其他的代码
  76. }
  77. // 7. n.insertChild
  78. func (n *node) insertChild(path string, fullPath string, handlers HandlersChain) {
  79. //省略其他无关代码
  80. child := &node{
  81. priority: 1,
  82. fullPath: fullPath, // 完整的路由路径
  83. }
  84. // 最终所有的回调函数
  85. n.handlers = handlers
  86. }
  87. // node 的结构体定义,发现就是自己嵌套自己的一个结构体
  88. // handlers 成员的数据类型(HandlersChain)本质就是 []func(*gin.Context)
  89. // 至此我们彻底明白:路由的存储模型是一个树形结构,每个节点都有自己路由路径以及回调函数 handlers
  90. type node struct {
  91. path string
  92. indices string
  93. wildChild bool
  94. nType nodeType
  95. priority uint32
  96. children []*node // child nodes, at most 1 :param style node at the end of the array
  97. handlers HandlersChain
  98. fullPath string
  99. }

通过以上分析,我们搞清楚了 users.GET(路由路径, 回调函数1,回调函数2,...) 后背的源码逻辑,总结起来就是按照 路由键 加载相关的全部回调函数,包括中间件回调函数+路由回调函数,并且所有回调函数的总数量加起来最大值为 63 个。
但是学习到这里我们依然不知道,以上所有回调函数什么时候被执行,那么继续向下学习

3.1.3 gin路由背后的所有回调函数什么时候被执行?
gin 最核心的东西其实是一个路由包,负责定义路由以及回调函数,当客户端的请求来临时负责快速匹配路径,进行组合相关的回调数。
至于什么时候执行其实是依赖于 go语言的 net/http 库的函数 ServerHTTP

  1. // 1.gin初始化一个路由引擎,它返回的是一个 *gin.Engine
  2. var router = gin.Default()
  3. // 省略无关代码
  4. // 由gin路由引擎启动一个web服务
  5. // 我们继续追踪源码去分析内部实现
  6. router.Run()
  7. // 2.router.Run() 源码定义
  8. func (engine *Engine) Run(addr ...string) (err error) {
  9. defer func() { debugPrintError(err) }()
  10. trustedCIDRs, err := engine.prepareTrustedCIDRs()
  11. if err != nil {
  12. return err
  13. }
  14. engine.trustedCIDRs = trustedCIDRs
  15. address := resolveAddress(addr)
  16. debugPrint("Listening and serving HTTP on %s\n", address)
  17. // 最关键的在这里
  18. // gin 将 engine 传递给了 http.ListenAndServe ,这个是 go 语言官方的 web 服务器启动函数
  19. // engine 其实就是我们初始化出来的一个路由引擎(router = gin.Default())
  20. // 然后不停地给这个路由引擎注册路由键,以及对应的回调函数,最终交给了 go 语言web服务入口去执行
  21. // 那么 http.ListenAndServe 为什么会调用 gin 初始化出来的路由引擎呢?
  22. // 需要我们继续追踪源代码
  23. err = http.ListenAndServe(address, engine)
  24. return
  25. }
  26. //3.ListenAndServe 官方web服务启动函数入口
  27. // 参数一:web服务器的ip端口,例如:0.0.0.0:8080
  28. // 参数二:(请看下文)
  29. func ListenAndServe(addr string, handler Handler) error {
  30. server := &Server{Addr: addr, Handler: handler}
  31. return server.ListenAndServe()
  32. }
  33. // 参数二定义原型,他是一个接口,只要开发者实现了这个接口,将你的实现原型传递进来
  34. // 官方的net库函数就会回调你
  35. type Handler interface {
  36. ServeHTTP(ResponseWriter, *Request)
  37. }
  38. //4.那么我们看一下gin 是如何实现上面这个接口的
  39. func (engine *Engine) ServeHTTP (w http.ResponseWriter, req *http.Request) {
  40. // engine.pool.Get() 本质就是创建一个空白的 Context
  41. // engine.pool 是组合的go官方 sync.pool 主要作用是就是提供一个对象池,不要频繁创建大对象
  42. c := engine.pool.Get().(*Context)
  43. // 这里的响应器实际上是由 go 官方的 net/http 库相关函数在接收到请求时初始化化的
  44. c.writermem.reset(w)
  45. // 这里的Request也是由 go 官方的 net/http 库相关函数在接收到请求时初始化化的
  46. c.Request = req
  47. // 这里有个地方也很关键,c.index 初始化值被设置为: -1 ,
  48. // 后文 Next 函数 c.index++ 其实就是从0所以开始
  49. c.reset()
  50. // gin的 Context 上下文成员参数初始化完成后
  51. // gin将处理逻辑继续交给了该函数,需要我们继续追踪
  52. engine.handleHTTPRequest(c)
  53. // 上下文对象使用完后放回对象池,下次可以直接获取,避免频繁创建大对象
  54. engine.pool.Put(c)
  55. }
  56. // 5.engine.handleHTTPRequest(c) 源码追踪
  57. // 该函数代码比较多,主要是检查核心函数执行前的所有条件必须满足,否则就在这里报错
  58. func (engine *Engine) handleHTTPRequest(c *Context) {
  59. // 省略其他代码....
  60. // 这里根据客户端实际请求的路径、参数,大小写不敏感模式去寻找已经注册的路由表中对应的调函数
  61. value := root.getValue(rPath, c.params, unescape)
  62. if value.params != nil {
  63. c.Params = *value.params
  64. }
  65. if value.handlers != nil {
  66. //value.handlers 路由键对应的全部回调函数
  67. c.handlers = value.handlers
  68. // 路由全路径
  69. c.fullPath = value.fullPath
  70. // 最核心的东西,所有回调函数要开始执行了
  71. // 内部具体是什么,需要我们继续追踪
  72. c.Next()
  73. c.writermem.WriteHeaderNow()
  74. return
  75. }
  76. // 省略其他代码....
  77. }
  78. //6. c.Next() 源码定义
  79. // 看到这里终于恍然大悟,所有的谜团全部被解开,原来 Next 是负责执行一个具体的路由定义的全部回调函数
  80. //它执行顺序是按照索引从小到大开始执行的,也就是说首先会执行中间件回调函数,然后才是路由对应的回调函数
  81. // 因为我们在定义路由回调函数的时候只有一个参数就是 *gin.Context, 开发者拿到的这个参数其实是
  82. // go 官方库 net/http 的web服务启动在接受到请求时调用了gin自己实现的接口函数,初始化好参数
  83. // 作为开发者就可以调用 context 上初始化好的参数,以及gin绑定在上面的所有函数
  84. func (c *Context) Next() {
  85. c.index++
  86. // 开发者在任何一个回调函数只要调用了 Abort 就会随时终止后面的回调函数执行
  87. // 具体参见后面第 7 条,以及前文分析的最大回调函数总数量为:63 个
  88. for c.index < int8(len(c.handlers)) {
  89. // 这里按照回调函数最开始的注册顺序,去执行.
  90. // 那么网上的教程说的gin的中间件是洋葱模型去哪里了?我们后文继续分析
  91. c.handlers[c.index](c)
  92. c.index++
  93. }
  94. }
  95. //7. 最后我们再继续分析一个函数
  96. // 如果开发者在任何一个回调函数调用了本函数,那么 index 值瞬间就被设置为 63
  97. func (c *Context) Abort() {
  98. // abortIndex 为一个常量值:63
  99. c.index = abortIndex
  100. }

3.2 深度剖析 中间件中的 Next 函数以及传说中的洋葱模型

我们先介绍一下洋葱模型概念:代码段的加载顺序和执行顺序是相反的,那么就是洋葱模型。 知道概念以后,我们就继续探索,在gin的中间件里面,洋葱模型是如何被触发的。 yangcong.png

当你看文档已经看到看到这里了,距离完全掌握gin的主线核心逻辑仅差一步之遥,那么就继续分析一下 Next 函数以及传说中的洋葱模型是如何出现的。
在这里我再次提醒一下大家:
1.gin 提供的 web 服务,是按照 go 官方的 web 服务接口要求去实现的

  1. // 1.任何人只要实现了这个接口
  2. func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
  3. }
  4. //2.将你的具体实现结构体,gin 里面就是 engine
  5. // 传递给 ListenAndServe 函数,当请求到达时就能被官方的web服务回调 ServeHTTP
  6. err = http.ListenAndServe(address, engine)
  1. gin 实现的 ServerHTTP ```go // 3. gin 实现的 ServerHTTP func (engine Engine) ServeHTTP(w http.ResponseWriter, req http.Request) {

    // engine.pool.Get() 本质就是创建一个空白的 Context // engine.pool 是组合的go官方 sync.pool 主要作用是就是提供一个对象池,不要频繁创建大对象 c := engine.pool.Get().(*Context) c.writermem.reset(w) c.Request = req

    // 该函数里面有会把 c.index设置为 -1 c.reset()

    // 这里的 c 就是客户端请求到达服务器时,gin给我们初始化的Conetxt 上下文 // 而 开发者编写的回调函数参数都是 *gin.Context // 因此所有回调函数的参数都是一个上下文指针,并且前一个函数如果操作了上下文的对象成员 // 那么下一个函数拿到的值就是前一个回调函数处理的结果 engine.handleHTTPRequest(c)

  1. // 上下文对象使用完后放回对象池,下次可以直接获取,避免频繁创建大对象
  2. engine.pool.Put(c)

}

  1. gin ServerHTTP 负责每一次客户端请求服务端时,初始化一个上下文 Context,而开发者编写的回调函数参数都是 *gin.Context gin 官方的目的就是把 Context 底层初始化工作做好,然后当做参数交给开发者,由开发者继续基于这个 Context 开发其他功能。<br />那么,所有开发者必须明确一点,这个 Context 是一个结构体的指针,他的任何一个成员在所有回调函数都是共享的。<br />例如 Contex 结构体的定义如下:
  2. ```go
  3. // gin 定义的Context,这里我们首先关注一下成员 index
  4. type Context struct {
  5. writermem responseWriter
  6. Request *http.Request
  7. Writer ResponseWriter
  8. Params Params
  9. handlers HandlersChain
  10. // index 成员在 Next 函数被 index++ 然后使用
  11. // 这里要说的是,index++ 以后在任何一个开发者编写的回调函数,该值都是共享的
  12. // 那么为什么要介绍这个变量,下文我们会使用到该变量
  13. index int8
  14. fullPath string
  15. engine *Engine
  16. params *Params
  17. }

3.2.1 在 gin 中编写一个中间件标准格式1:

所有中间件回调函数、路由对应的回调函数都是由func (c *Context) Next() 统一调用的。

  1. // 定义第一个中间件
  2. middleware1:= func(c *gin.Context) {
  3. fmt.Printf("中间件001\n")
  4. }
  5. // 定义第二个中间件
  6. middleware2:= func(c *gin.Context) {
  7. fmt.Printf("中间件002\n")
  8. }
  9. router.Use(middleware1, middleware2).GET("/test_middleware", func(context *gin.Context) {
  10. fmt.Printf("路由回调函数\n")
  11. context.String(http.StatusOK, "测试中间件")
  12. })

通过请求接口 http://127.0.0.1:20201/test_middleware 输出结果如下:
test_middleware1.png

上述代码的执行,一路下一步,按照顺利依次去执行,最后执行了路由的回调函数。
上述过程也符合我们前面所介绍的 “所有路由键对应的函数都是从中间件函数到路由回调函数依次执行的”原则。
如果其中一个中间件不通过,那么就使用如下代码即可:

  1. // 定义第一个中间件
  2. middleware1:= func(c *gin.Context) {
  3. // 通过前面的代码分析,我们都知道 Abort() 在任何一个回调函数都可以终止后续的所有回调继续执行
  4. // 但是 Abort 不终止本函数的代码
  5. c.Abort()
  6. fmt.Printf("中间件001\n")
  7. }
  8. // 定义第二个中间件
  9. middleware2:= func(c *gin.Context) {
  10. fmt.Printf("中间件002\n")
  11. }
  12. // 定义一个测试路由
  13. router.Use(middleware1, middleware2).GET("/test_middleware", func(context *gin.Context) {
  14. fmt.Printf("路由回调函数\n")
  15. context.String(http.StatusOK, "测试中间件")
  16. })

测试示例,说明了 Abort 函数的作用终止了后续的所有的回调函数,所谓的所有回调函数就是:中间件函数+路由回调函数。
test_middleware2.png
至此,我们没有发现任何洋葱模型,所有的函数都是顺序去执行。

3.2.2 在 gin 中编写一个中间件标准格式2:
这里首先我们要继续回顾一下前面的一个函数 Next ,再次查看一下源代码的定义:

  1. // 该函数在 3.1.3 章节 112 行已经做了介绍,这里复制过来
  2. func (c *Context) Next() {
  3. c.index++
  4. // 开发者在任何一个回调函数只要调用了 Abort 就会随时终止后面的回调函数执行
  5. // 具体参见后面第 7 条,以及前文分析的最大回调函数总数量为:63 个
  6. for c.index < int8(len(c.handlers)) {
  7. // 这里按照回调函数最开始的注册顺序,去执行.
  8. // 那么网上的教程说的gin的中间件是洋葱模型去哪里了?我们后文继续分析
  9. c.handlers[c.index](c)
  10. c.index++
  11. }
  12. }

开始定义中间件的另外一种形式:

  1. middleware1:= func(c *gin.Context) {
  2. fmt.Printf("中间件001--执行顺序:1\n")
  3. c.Next()
  4. fmt.Printf("中间件001---执行顺序:4\n")
  5. }
  6. middleware2:= func(c *gin.Context) {
  7. fmt.Printf("中间件002--执行顺序:2\n")
  8. c.Next()
  9. fmt.Printf("中间件002--执行顺序:3\n")
  10. }
  11. router.Use(middleware1, middleware2).GET("/test_middleware", func(context *gin.Context) {
  12. fmt.Printf("路由回调函数\n")
  13. context.String(http.StatusOK, "测试中间件")
  14. })

通过请求接口 http://127.0.0.1:20201/test_middleware 输出结果如下:
test_middleware3.png

从结果可以看出代码的执行顺序不是顺序依次执行了,而是出现了所谓的洋葱模型,Next 前面的代码都是顺序执行的,但是Next函数后面的代码执行和加载顺序是相反的,很符合标准的洋葱模型(Next 后面的代码段先进后出)。
那么为什么会这样呢?我们从 Next 函数源代码解析透视本质:

  1. // *Context 结构体指针是每一次客户端请求到达服务端时,gin 给我们初始化的
  2. // *Context 的成员在一次请求过程中被所有的回调函数共享
  3. func (c *Context) Next() {
  4. // index 的值在任何一个回调函数都是共享的
  5. // 因为一次请求只有一个 Context,但是回调函数可以有很多,他们共享了 *gin.Contex
  6. c.index++
  7. for c.index < int8(len(c.handlers)) {
  8. // 这里按照回调函数最开始的注册顺序,去执行.
  9. // 但是如果开发者编写的回调函数里面有 Next() ,等于是手动调用了本段代码第3行,
  10. // 那么就会立刻触发c.index ++ ,下一个回调函数被执行
  11. // 而原本的回调函数 Next 后面的代码就会滞后执行
  12. // 越是最先加载的回调函数 Next 函数后面的代码段越是被最后执行,从而形成了洋葱模型
  13. c.handlers[c.index](c)
  14. c.index++
  15. }
  16. }

洋葱模型主要的特点:前面的回调函数 Next 后面的代码段可以拿到后面函数的执行结果,前提是后面的回调函数需要把相关值通过 Set 函数设置在 Context 上.

开发者编写的中间件,如果没有 Next() 函数,所有的回调函数都有 gin 统一负责依次调用,如果开发者编写的中间件调用了 Next(),就相当于手动调用了 func (c *Context) Next(){ } ,c.index++ 被快速定位到下一个回调函数去执行。
由于 c.index 在 Context 是共享的,所有开发者调用 Next之后,c.index 的值会依次递增,gin 再次调用下一个时,也不会产生重复调用。
在我们刚学习时或许有一种想法:在中间件校验业务逻辑都 OK 后,是否必须调用 Next 函数才能进入下一个回调函数/下一个环节,现在我们也彻底明白了,最初的想法是错误的,在中间件使用 Next 函数或者不使用,都能进入下一个环节,但是调用 Abort函数,则直接终止后面全部回调函数的执行。