🔥码云GVP开源项目 12k star Uniapp+ElementUI 功能强大 支持多语言、二开方便! 广告
通过上个章节我们已经学会了中间件的用法,在 gin 框架中路由的回调函数、中间件回调函数本质都是一样的,我相信大家使用已经没有任何问题了,那么在本章节我们就由浅入深,继续深度学习中间件. ## 基础用法 ### 1.中间件的加载(注册) 注意:Use 函数仅仅只是负责加载(注册)中间件,不负责调用. ``` // 路由可以载入很多个中间件,很多个到底是多少个? // 想知道答案,那我们继续追踪gin源码去揭晓答案,这里请带着你的疑问向后学习 backend.Use(authorization.CheckTokenAuth(), 下一个中间件,继续下一个中间件, 省略很多个...) ``` ## 进阶学习 后续知识点对于初学者来说存在一定的难度,主要是因为在 gin 中,加载的中间件函数: `func ( *gin.Context){ }` 在后续执行时会和注册的路由回调函数在同一个逻辑处执行,中间件函数、路由回调函数都是平行关系,不同的区别是先后顺序不同,gin 对这块逻辑处理是相同的方式,这就需要我们从路由开始追踪,因此涉及到的代码会比较多,过程比较复杂. 学完本章节,gin 最核心的主线逻辑也就彻底搞明白了。 这个过程其实就是对一个 request -> response 的全过程源代码剖析,难度比较大, ### 2.gin 中间件加载过程 ``` // Use 的作用就是首先载入中间件回调函数:func(*Context) // 首先存储起来,然后等着被后续逻辑调用 // 我们使用 goland ,ctrl+鼠标左键,点击 Use 继续追踪gin源码 func (group *RouterGroup) Use(middleware ...HandlerFunc) IRoutes { group.Handlers = append(group.Handlers, middleware...) return group.returnObj() } // group.Handlers 定义的最终原型如下,本质上就是 func(*Context) 的切片,存储很多个回调函数 type HandlersChain []HandlerFunc // type HandlerFunc func(*Context) // 学习到这里,你必须知道的就是: // gin的中间件回调函数全部被存储在 group.Handlers 变量了 // 这里您对该变量有印象就行,后面还需要继续使用该变量 ``` 习到这里,我们已经知道中间件处理函数被 `Use` 函数注册了在了 `group.Handlers` 变量存储起来了, `Use` 函数可以加载很多个中间件,究竟是多少个,这里我们依然不知道他的具体数量,还有它们什么时候执行,我们也不知道 ... ... 我们只看见 Use 函数最后返回了 `group.returnObj()` ,它是所有路由的处理接口:IRoutes ,已经结束了,我们无法向下追踪了,那就只能从其他地方入手追踪了,既然中间件是加载在具体的路由前面,那么它肯定在某个具体的路由被访问时执行。 ### 3.gin 中间件执行逻辑 这个过程很漫长,逻辑比较复杂,我们分步骤分析,去追踪 . #### 3.1 一个具体的路由地址被请求后的 gin 源码究竟是什么 **3.1.1 定义一个具体的路由以及回调函数** ``` // 1.省略其它无关代码 users.GET("list", func (c *gin.Context){ // 编写该路由路径对应的业务处理回调函数 // 我们省略具体过程 ... ... }) ``` **3.1.2** `**users.GET**` **背后的 gin 源码追踪分析** ``` // 2.在 goland 中,ctrl+鼠标左键 点击 GET 函数,源代码如下: func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes { // 很明显该函数是 GET + relativePath(开发者定义的路由路径)对应的业务处理逻辑 // 与中间件一样,他的请求回调函数也可以有很多个,具体数量不知道... // 那么就需要继续追踪 group.handle 源代码 return group.handle(http.MethodGet, relativePath, handlers) } // 3.group.handle 函数源码 func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes { // 将定义的路由相对路径拼接为完整的绝对路径 // 因为一个完整的路径往往和前面定义的路由组路径有关系,因此需要一系列拼接 absolutePath := group.calculateAbsolutePath(relativePath) // 针对完整路由路径将关联的回调函数全部组合出来 // 究竟如何组合,后续继续追踪源码 handlers = group.combineHandlers(handlers) // 将请求方式(GET、POST等)结合完整路径作为key,处理函数作为 value // 以 key => value 的形式注册,value 可以是很多个回调函数 group.engine.addRoute(httpMethod, absolutePath, handlers) return group.returnObj() } //4. group.combineHandlers 源代码追踪 func (group *RouterGroup) combineHandlers(handlers HandlersChain) HandlersChain { // 看到这里还记得 group.Handlers 变量吗?如果不记得请查看本章节 2 标题部分 // finalSize 表示 中间件回调函数的数量 + 具体路由的回调函数数量的总和 finalSize := len(group.Handlers) + len(handlers) // 如果 finalSize >= abortIndex 就会发生panic, // abortIndex 的定义值: math.MaxInt8 / 2 , // 在go语言中,官方定义: MaxInt8 = 1<<7 - 1, 表示 1*(2^7)-1,最终值:127 // 那么 abortIndex = 127/2 取整 = 63 // 至此我们终于知道, gin 的中间件函数数量 + 路由回调函数的数量总和最大允许 63 个. if finalSize >= int(abortIndex) { panic("too many handlers") } // 以上条件检查全部通过后,将中间件回调函数和路由回调函数全部合并在一起存储 // HandlersChain 本质就是 [] func(*Context) mergedHandlers := make(HandlersChain, finalSize) // group.Handlers 是中间件函数,他在 mergedHandlers 中存储的顺序靠前,也就是索引比较小 copy(mergedHandlers, group.Handlers) // handlers 是路由回调函数,他的存储位置比中间函数靠后 copy(mergedHandlers[len(group.Handlers):], handlers) // 最终返回中间件函数+路由函数组合在一起的全部回调函数 return mergedHandlers } //5. group.engine.addRoute 源码分析 // gin的路由是一个很复杂的路由前缀树算法模型,完整过程很复杂 // 这里我们主要追踪路由以及回调函数的 存储/注册 过程 func (engine *Engine) addRoute(method, path string, handlers HandlersChain) { assert1(path[0] == '/', "path must begin with '/'") assert1(method != "", "HTTP method can not be empty") assert1(len(handlers) > 0, "there must be at least one handler") debugPrintRoute(method, path, handlers) root := engine.trees.get(method) if root == nil { root = new(node) root.fullPath = "/" engine.trees = append(engine.trees, methodTree{method: method, root: root}) } // 在这里,gin 将我们定义的完整路径和回调函数进行了注册 // 源码后续继续追、分析 root.addRoute(path, handlers) // Update maxParams if paramsCount := countParams(path); paramsCount > engine.maxParams { engine.maxParams = paramsCount } } //6.root.addRoute(path, handlers) 函数在按照键 => 值进行注册路由与回调函数时调用了很多其他函数 // 这里我将过程函数名字列举如下 func (n *node) addRoute(path string, handlers HandlersChain) { // 省略其他无关代码 // 又继续调用如下函数 n.insertChild(path, fullPath, handlers) // ... ... } // 省略很多其他的代码 } // 7. n.insertChild func (n *node) insertChild(path string, fullPath string, handlers HandlersChain) { //省略其他无关代码 child := &node{ priority: 1, fullPath: fullPath, // 完整的路由路径 } // 最终所有的回调函数 n.handlers = handlers } // node 的结构体定义,发现就是自己嵌套自己的一个结构体 // handlers 成员的数据类型(HandlersChain)本质就是 []func(*gin.Context) // 至此我们彻底明白:路由的存储模型是一个树形结构,每个节点都有自己路由路径以及回调函数 handlers type node struct { path string indices string wildChild bool nType nodeType priority uint32 children []*node // child nodes, at most 1 :param style node at the end of the array handlers HandlersChain fullPath string } ```