go 进阶 gin底层原理相关: 四. gin中间件底层原理

2023-10-31

一. gin 中间件基础

  1. 中间件是什么?: 是为了过滤路由而发明的一种机制,有点像责任链,当接收到请求时先经过中间件,再到具体的处理函数
  2. gin中执行业务时的先后流程分为: 全局中间件–>路由组中间件—>然后是执行实际业务逻辑的Handler,按照官方的定义处理实际业务的Handler称为main handler,剩下其它的称为middleware handler(也就是中间件), middleware handle 可以分为两部分,随着 request 的流动,左边是入,右边为出,而分割点就是 next, next 的作用就是明确在这个地方进入到下一个 handler ,如果没有指定,默认是当前 handler 结束后进入下一个 handler, 本质是一个匿名回调函数,和绑定到一个路径下的处理函数本质是一样的
  3. 中间件是实现流程
  1. 提供func(c *gin.Context)类型的函数,编写中间件业务
  2. 调用Engine下的Use()方法或RouterGroup下的Use()方法注册全局或路由组中间件
  3. 执行时,通过Context的index当前中间件下标获取到对应的中间件执行
  4. 通过Context.Next方法获取下一个中间件执行
  5. 最终执行到main Handler,当main Handler执行完毕后依次流出中间件
  6. 或通过Context.Abort() 将index移动末尾,也就是最后一个中间件,跳出中间件链

二. 中间件初始化流程

  1. 初始化中间件底层可以细分为两步
  1. 初始化中间件,将中间件保存到RouterGroup的HandlersChain数组中
  2. 新建切片,获取RouterGroup的HandlersChain数组中保存的中间件函数,插入新切片的头部,再把用户自定义处理某路径下请求的handler插入到尾部,构建前缀树

1. 初始化中间件保存到RouterGroup的HandlersChain数组中

  1. 在启动服务初始化容器时,会获取Engine实例,Engine中存在一个RouterGroup属性,在获取Engine实例时会先创建这个默认的RouterGroup
func New() *Engine {
	debugPrintWARNINGNew()
	engine := &Engine{
		//实例化默认的RouterGroup,其中Handlers为中间件数组
		RouterGroup: RouterGroup{ 
			Handlers: nil,
			basePath: "/",
			root:     true,
		},
		FuncMap:                template.FuncMap{},
		RedirectTrailingSlash:  true,
		RedirectFixedPath:      false,
		HandleMethodNotAllowed: false,
		ForwardedByClientIP:    true,
		AppEngine:              defaultAppEngine,
		UseRawPath:             false,
		RemoveExtraSlash:       false,
		UnescapePathValues:     true,
		MaxMultipartMemory:     defaultMultipartMemory,
		//trees 是最重要的点!!!!负责存储路由和handle方法的映射,采用类似字典树的结构
		trees:                  make(methodTrees, 0, 9), 
		delims:                 render.Delims{Left: "{{", Right: "}}"},
		secureJsonPrefix:       "while(1);",
	}`
	engine.RouterGroup.engine = engine
	//这里采用 sync/pool 实现context池,减少频繁context实例化带来的资源消耗
	engine.pool.New = func() interface{} {
		return engine.allocateContext()
	}
	return engine
}
  1. Engine 与 RouterGroup 都实现了IRouter 接口,实现了IRouters下注册中间件的Use()函数,通过Use()函数注册中间件,将中间件函数保存到RouterGroup的HandlersChain数组中
func (engine *Engine) Use(middleware ...HandlerFunc) IRoutes {
	//获取Engine中默认的RouterGroup执行注册中间件逻辑
	engine.RouterGroup.Use(middleware...)
	engine.rebuild404Handlers()
	engine.rebuild405Handlers()
	return engine
}

// Use adds middleware to the group, see example code in GitHub.
func (group *RouterGroup) Use(middleware ...HandlerFunc) IRoutes {
	group.Handlers = append(group.Handlers, middleware...)
	return group.returnObj()
}

HandlersChain是什么

type RouterGroup struct {
	Handlers HandlersChain        // 中间件函数执行链
	basePath string               // 路由组的基础路径
	engine   *Engine              // Gin引擎对象
	root     bool                 // 是否为根路由组
}
  1. HandlersChain 实质就是一个方法数组,每个方法的方法签名为 func(*Context)
  2. 在执行gin.Use(func…)注册中间件, gin.Get(“”, func)注册路由, gin.Group(“”, func)创建分组等操作时最终函数会在gin初始化的时候按顺序,将这些func放到HandlersChain数组中,等待被依次调用
  3. 此处我们先只关注中间件,中间件会保存到HandlersChain数组中
// HandlersChain defines a HandlerFunc array.
type HandlersChain []HandlerFunc

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

2. 整合中间件函数与业务相关的mainHandler构建前缀树

  1. 在调用
func (group *RouterGroup) Group(relativePath string, handlers ...HandlerFunc) *RouterGroup {
	return &RouterGroup{
		Handlers: group.combineHandlers(handlers),
		basePath: group.calculateAbsolutePath(relativePath),
		engine:   group.engine,
	}
}
  1. 查看路由注册函数,以GET为例,方法内会调用RouterGroup的handle()方法
router.GET("/hello", getting)
……
func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes {
	return group.handle("GET", relativePath, handlers)
}
 
func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes {
	//1.通过相对路径获取绝对路径
	absolutePath := group.calculateAbsolutePath(relativePath)
	//2.合并前面Handlers(例如前面添加到中间件函数等)
	handlers = group.combineHandlers(handlers)
	//2.将对absolutePath路径Get请求对应的处理方法(handlers)加入到引擎的路由中
	group.engine.addRoute(httpMethod, absolutePath, handlers)
	return group.returnObj()
}
  1. 在RouterGroup.handle()中调用的combineHandlers()比较重要,生成一个新的handler切片,然后先把中间件的handler插入到头部,然后把用户自定义处理某路径下请求的handler插入到尾部。最后返回的是这个新生成的切片,而引擎中之前设置的中间件handlers(group.Handlers)并没改变。所以针对每个需要被路由的请求,之前注册的中间件对应的handler都会被调用
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
}
  1. 再然后执行addRoute()也就是路由注册的过程了,参考二. gin的路由注册原理

三. 中间件的获取执行

  1. 中间件的获取执行,实际也可以说成接收请求后的路由匹配,执行对应的路由,实现指定的业务
  2. 中间件的执行可以细分为两步
  1. 接收请求
  2. 接收到请求后的路由匹配与执行

监听接收连接请求

  1. 编写gin服务时,通过Engine下的Run()监听端口,启动服务,该方法内部最终会执行net/http下的ListenAndServe()
func (engine *Engine) Run(addr ...string) (err error) {
	defer func() { debugPrintError(err) }()

	if engine.isUnsafeTrustedProxies() {
		debugPrint("[WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.\n" +
			"Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.")
	}

	address := resolveAddress(addr)
	debugPrint("Listening and serving HTTP on %s\n", address)
	err = http.ListenAndServe(address, engine.Handler())
	return
}
  1. net/http下的ListenAndServe(),该方法内部会调用Server下的ListenAndServe()
func ListenAndServe(addr string, handler Handler) error {
	server := &Server{Addr: addr, Handler: handler}
	//调用Server下的ListenAndServe()
	return server.ListenAndServe()
}
  1. 查看Server下的ListenAndServe()方法内部会调用Server下的Serve()
  2. 在Server下的Serve()中,会获取连接,设置连接状态,基于协程启动监听连接请求
//1.
func (srv *Server) ListenAndServe() error {
	if srv.shuttingDown() {
		return ErrServerClosed
	}
	addr := srv.Addr
	if addr == "" {
		addr = ":http"
	}
	ln, err := net.Listen("tcp", addr)
	if err != nil {
		return err
	}
	//调用Server下的Serve()
	return srv.Serve(tcpKeepAliveListener{ln.(*net.TCPListener)})
}

//2.该方法内部重会获取连接,设置连接状态,基于协程处理请求
func (srv *Server) Serve(l net.Listener) error {
	//……
	for {
		rw, e := l.Accept()
		//……
		tempDelay = 0
		c := srv.newConn(rw)
		c.setState(c.rwc, StateNew) // before Serve can return
		//基于协程
		go c.serve(ctx)
	}
}
  1. 查看conn下的serve(),当接收到连接请求后,会执行该方法,该方法内部会调用ServeHTTP()
// Serve a new connection.
func (c *conn) serve(ctx context.Context) {
		//……
		//通过 ServeHTTP(w, w.req) 处理请求
		serverHandler{c.server}.ServeHTTP(w, w.req)
		//……
}

接收到请求后的处理

  1. 上面了解到当监听接收到请求后,会执行ServeHTTP()
  2. Gin的底层使用了net/http包,初始化启动服务时要获取Engine实例,Engine实现了ServeHTTP()接口,该接口是索引请求的入口, 并且在获取该实例的过程中会创建一个用来处理请求的Context

在ServeHTTP中会基于Pool获取到Context,通过Context处理请求

type Handler interface {
	ServeHTTP(ResponseWriter, *Request)
}
 
// ServeHTTP conforms to the http.Handler interface.
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
	c := engine.pool.Get().(*Context)
	c.writermem.reset(w)
	c.Request = req
	c.reset()
 	//处理请求逻辑
	engine.handleHTTPRequest(c)
 
	engine.pool.Put(c)
}
  1. 在Engine实现的ServeHTTP()方法中会调用Engine.handleHTTPRequest(),该方法会找到当前请求方式对应methodTree然后找到路径对应的处理方法
  1. 在handleHTTPRequest方法中当获取到对应的树节点后,会获取到对应的Handlers列表,封装到Context中, 并且初始化Context的index属性,
func (engine *Engine) handleHTTPRequest(c *Context) {
	httpMethod := c.Request.Method
	path := c.Request.URL.Path
	//……
	//获取到当前请求对应的树
	t := engine.trees
	for i, tl := 0, len(t); i < tl; i++ {
		if t[i].method != httpMethod {
			continue
		}
		root := t[i].root
		//1.获取RouterGroup上的handlers,获取请求参数等等
		handlers, params, tsr := root.getValue(path, c.Params, unescape)
		if handlers != nil {
			//将handlers设置到Context上
			c.handlers = handlers
			//设置请求参数
			c.Params = params
			//初始化index
			c.Next()
			c.writermem.WriteHeaderNow()
			return
		}
	//……
  1. 查看Context下的Next(),就是累加Context的index属性,执行下一个Handler,这就是前面中间件的获取与执行
func (c *Context) Next() {
	c.index++
	for s := int8(len(c.handlers)); c.index < s; c.index++ {
		c.handlers[c.index](c)
	}
}
  1. 具体参考前面做的笔记 三. gin接收请求匹配路由原理

四. 总结

  1. 中间件底层可以封中间件注册与路由匹配后中间件执行两部分看,要先了解gin中的Engine 与RouterGroup
  2. RouterGroup内部有:
  1. HandlersChain属性,内部存储了中间件函数执行链
  2. basePath属性: 当前路由组的基础路径(根节点是"/",在Group分组时对应的是basePath+分组路径)
  3. root 表示当前是否是跟路由的布尔属性
  4. Engine当前Gin引擎实例
  1. 在我们编写gin服务时首先要调用gin.New()或gin.Default()创建引擎实例, Engine 与 RouterGroup 实现了IRouter与IRouters 接口,实现了下注册中间件的Use()函数,通过Use()函数注册中间件
  1. Engine 隐式的继承了RouterGroup,在调用New()函数初始化时,会给这个默认的RouterGroup进行赋值
  2. 调用Engine 上的User()方法时,内部或获取这个默认的RouterGroup调用RouterGroup上的Use()注册中间件,会将中间件函数保存到RouterGroup的HandlersChain中间件执行链中
  3. 在调用RouterGroup的Group()进行分组时可以注册分组中间件,查看Group函数,内部会创建一个新的RouterGroup,调用RouterGroup上的combineHandlers()方法生成一个新的handler切片,把新的中间件函数的handler插入到头部,把前面注册的某路径下请求的handler中间件插入到尾部
  4. 最终中间件函数按照注册的先后顺序保存到了RouterGroup的HandlersChain中
  1. 接着看一下路由注册,RouterGroup 实现了IRouter与IRouters 接口,通过GET,POST等方法实现路由接口注册,以GET为例,内部会调用RouterGroup上的handle()方法,在该方法中:
  1. 也会执行calculateAbsolutePath()获取到当前接口上对应的中间件函数
  2. 调用Engine上的addRoute()方法进行路由注册,最终将所有的handler包括中间件与接口函数,接口路径等维护路由关系封装为node结构变量,保存到Engine的trees属性上
  1. 中间件执行也就是路由匹配执行的过程,参考前面做的笔记
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

go 进阶 gin底层原理相关: 四. gin中间件底层原理 的相关文章

  • Golang-使用 goroutine 运行闭包的“坑”

    介绍 在 Go 语言中 函数支持匿名函数 闭包就是一种特殊的匿名函数 它可以用于访问函数体外部的变量 需要注意的是 在 for range 中 使用 goroutine 执行闭包时 经常会掉 坑 因为匿名函数可以访问函数体外部的变量 而 f
  • Go_关键字、编译、转义字符

    关键字 关键字是指被go语言赋予了特殊含义的单词 共25个 关键字不能用于自定义名字 只能在特定语法结构中使用 break default func interface select case defer go map struct cha
  • Go切片排序

    Go 语言标准库提供了sort包 用于对切片和用户定义的集合进行排序 具体示例如下 基本排序 package main import fmt sort func main float 从小到大排序 f float64 5 2 1 3 0 7
  • 权重实现随机抽奖

    一般抽奖是怎么实现的 在实习期间学会了一种通用的写法 在这里记录一下 最近在学Golang语法基础 这里就用Golang来写 package main import fmt time math rand func main r rand N
  • golang:环境变量GOPROXY和GO111MODULE设置

    我们安装完golang后 我们在windows的cmd命令下就可以直接查看和使用go命令和环境变量了 同样的在linux下可以在控制台使用 如下图所示 C Users lijie1 gt go env set GO111MODULE set
  • beego+goAdmin+mysql+docker+natapp作为微信小程序地服务器“伪部署”

    写在前面的话 1 为什么我要叫伪部署 答 因为我把它们放在服务器运行 都是开发模式 生产模式实在不会弄 所以就这样了 2 系统环境 答 腾讯云服务器 系统为 ubuntu 版本不记得 应该是比较高的 3 前提假设 答 假设你的服务器已经安装
  • Keycloak概述

    这里写自定义目录标题 Keycloak概述 Single Sign On Kerberos 社交登录 用户合并 客户端适配 管理控制台 用户管理控制台 标准协议 授权服务 Getting Started Keycloak概述 keycloa
  • 两点。。。等来金蝶中间件的面试通知

    晚上笔试 说是大约一点通知 等到十二点 困了 上床 睡不着啊 1点半翻起来 打开手机 没有 打开Gmail 没有 于是抽了支烟 等到两点多一点 手机响了 接到面试通知 下午一点 石头终于落地 因为上午还有一场网易游戏的笔试 担心冲突 还好
  • 16-MyCat

    一 Mycat概述 1 什么是Mycat 什么是Mycat Mycat是数据库中间件 所谓数据库中间件是连接Java应用程序和数据库中间的软件 为什么要用Mycat 遇到问题 Java与数据库的紧耦合 高访问量高并发对数据库的压力 读写请求
  • Golang三剑客之Pflag、Viper、Cobra

    如何构建应用框架 想知道如何构建应用框架 首先你要明白 一个应用框架包含哪些部分 在我看来 一个应用框架需要包含以下 3 个部分 命令行参数解析 主要用来解析命令行参数 这些命令行参数可以影响命令的运行效果 配置文件解析 一个大型应用 通常
  • Go 语言输出文本函数详解

    Go语言拥有三个用于输出文本的函数 Print Println Printf Print 函数以其默认格式打印其参数 示例 打印 i 和 j 的值 package main import fmt func main var i j stri
  • Go 程序编译过程(基于 Go1.21)

    版本说明 Go 1 21 官方文档 Go 语言官方文档详细阐述了 Go 语言编译器的具体执行过程 Go1 21 版本可以看这个 https github com golang go tree release branch go1 21 sr
  • 有哪些不错的 Golang 开源项目?

    目前人在字节做 Go 开发 寻找 Golang 开源项目学习目的可能是 想学习或者提高自己对 Go 项目的组织和编排能力 想学习 Go 项目的框架设计 想在一些 Go 语法上细节的优化和进阶 我推荐两个项目 一 tinode 这是一个开源的
  • 【go语言开发】编写单元测试

    本文主要介绍使用go语言编写单元测试用例 首先介绍如何编写单元测试 然后介绍基本命令的使用 最后给出demo示例 文章目录 前言 命令 示例 前言 在go语言中编写单元测试时 使用说明 测试文件命名 在 Go 语言中 测试文件的命名应与被测
  • go-zero目录结构和说明

    code of conduct md 行为准则 CONTRIBUTING md 贡献指南 core 框架的核心组件 bloom 布隆过滤器 用于检测一个元素是否在一个集合中 breaker 熔断器 用于防止过多的请求导致系统崩溃 cmdli
  • go-zero 的 etcd 配置

    实现代码在 core discov config go 文件中 type EtcdConf struct Hosts string Key string ID int64 json optional User string json opt
  • 【golang】go执行shell命令行的方法( exec.Command )

    所需包 import os exec cmd 的用法 cmd exec Command ls lah ls是命令 后面是参数 e cmd Run 多个参数的要分开传入 如 ip link show bond0 cmd
  • AUTOSAR从入门到精通-中间件通信DDS(三)

    目录 前言 原理 常见中间件模型 第一代 点对点CS Client Server 模型
  • go-carbon v2.3.4 发布,轻量级、语义化、对开发者友好的 Golang 时间处理库

    carbon 是一个轻量级 语义化 对开发者友好的 golang 时间处理库 支持链式调用 目前已被 awesome go 收录 如果您觉得不错 请给个 star 吧 github com golang module carbon gite
  • 【go语言】AST抽象语法树详解&实践之扫描代码生成错误码文档

    背景 为了能识别出代码中抛出错误码的地址和具体的错误码值 再根据错误码文件获取到错误码的具体值和注释 方便后续的排错 这里使用AST进行语法分析获取到代码中的目标对象 一 编译过程 在开始解析代码之前先补充了解一下编译过程 编译过程是将高级

随机推荐