Go高级之从源码分析Gin框架的函数链

2023-11-06 20:27:39 浏览数 (1)

前言

本文是探讨的是"Go高级之从源码分析Gin框架的函数链"

此文章是个人学习归纳的心得,未经允许,严禁转载,如有不对, 还望斧正, 感谢!

什么是Gin框架

Gin 是一个用 Go 语言开发的 Web 框架,提供类 Martini 的 API,并且由于GO语言的特性,然后性能更好。由于采用了httprouter库,它的性能比同类框架快了 40 倍左右。如果你需要一个高性能、高开发效率的框架,那么 Gin 就非常适合你,我很喜欢Go中文网对它的描述——晶莹剔透

Gin 提供了一系列的功能,包括但不限于路由管理、中间件、上下文参数传递等。它还支持多种数据格式,包括JSON、XML、HTML 等。此外,它还具有插件机制,可以方便地扩展框架的功能。

Gin 的主要优点如下:

  • 高性能:由于采用了httprouter库,Gin 的性能非常出色,比同类框架快了 40 倍左右。
  • 易用性:Gin 提供了一系列的内置功能,使你可以轻松地实现 Web 开发。
  • 扩展性:Gin 具有插件机制,可以方便地扩展框架的功能。

其实就是一句话,Gin框架对新手很友好

Gin框架的核心

要想用一篇文章讲完一个框架,很有难度,我将分开来讲,主要是从方法链,Gin的九颗方法树,上下文处理来讲

初识Gin核心理念

Gin框架的实现原理主要涉及以下几个核心部分:

  1. 路由匹配与处理:Gin使用基数树(Radix Tree)来管理路由,将路由路径分解为多个节点,通过匹配路径的前缀来快速找到对应的路由处理函数。当有新的路由注册时,Gin会根据路径构建对应的节点,并将处理函数与该节点绑定。在请求到来时,Gin会从根节点开始遍历路由树,根据请求路径匹配到对应的处理函数进行执行。
  2. 中间件机制:Gin的中间件机制是通过方法链实现的。每个中间件都是一个函数,它接收一个上下文对象(Context)和一个函数参数(next),并在执行过程中可以处理请求和响应,然后通过调用next()函数将控制权交给下一个中间件。这样,多个中间件可以形成一个链式调用的过程,依次对请求进行处理和控制。
  3. 上下文管理:Gin的上下文对象(Context)封装了一次HTTP请求的上下文信息,包括请求参数、请求头、响应内容等。在处理请求过程中,可以通过上下文对象获取和设置这些信息。Gin通过将上下文对象作为参数传递给中间件和路由处理函数,实现了在这些函数之间共享数据和状态的能力。
  4. 异常处理:Gin框架内置了对异常的处理机制。当发生异常时,Gin会捕获异常并返回一个合适的错误响应。同时,Gin还提供了一些辅助方法,如Abort()和AbortWithStatus(),用于在处理过程中终止请求并返回特定的错误响应。
  5. 并发处理:Gin框架使用goroutine来实现并发处理请求。每个请求都会在独立的goroutine中执行,这样可以提高服务器的并发处理能力。

细品函数链

Engine结构体

gin框架的核心就是Engine结构体,我给一些字段加了必要的注释,我们一开始的

gin.Default()或者gin.New()其实都是实例化一个Engine结构体

代码语言:go复制
type Engine struct {
	RouterGroup

	// RedirectTrailingSlash启用自动重定向,如果当前路由无法匹配但存在没有尾部斜杠的路径的处理程序。
	// 例如,如果请求/foo/,但只有/foo的路由存在,那么对于GET请求,客户端将被重定向到/foo,并返回301状态码,对于其他请求方法,返回307状态码。
	RedirectTrailingSlash bool

	// RedirectFixedPath如果启用,路由器将尝试修复当前请求路径,如果没有为其注册处理程序。
	// 首先,会删除多余的路径元素,如../或//。
	// 然后,路由器对清理后的路径进行不区分大小写的查找。
	// 如果能找到此路由的处理程序,则路由器将使用状态码301对GET请求进行重定向,对于其他请求方法,返回307状态码。
	// 例如,/FOO和/..//Foo可能会被重定向到/foo。
	// RedirectTrailingSlash与此选项无关。
	RedirectFixedPath bool

	// HandleMethodNotAllowed如果启用,路由器将检查当前路由是否允许使用其他方法,
	// 如果当前请求无法路由。
	// 如果是这种情况,请求将以“Method Not Allowed”响应,并返回HTTP状态码405。
	// 如果没有其他方法被允许,则将请求委托给NotFound处理程序。
	HandleMethodNotAllowed bool

	// ForwardedByClientIP如果启用,将从请求的头部中解析与`(*gin.Engine).RemoteIPHeaders`匹配的客户端IP。
	// 如果未获取到IP,则回退到从`(*gin.Context).Request.RemoteAddr`获取的IP。
	ForwardedByClientIP bool

	// AppEngine已废弃。
	// Deprecated: 使用值为gin.PlatformGoogleAppEngine的`TrustedPlatform`代替
	// #726 #755 如果启用,将信任以“X-AppEngine…”开头的某些头部,以更好地与该PaaS集成。
	AppEngine bool

	// UseRawPath如果启用,将使用url.RawPath查找参数。
	UseRawPath bool

	// UnescapePathValues如果为true,则解码路径值。
	// 如果UseRawPath为false(默认情况下),则UnescapePathValues实际上为true,
	// 因为将使用url.Path,而url.Path已经解码。
	UnescapePathValues bool

	// RemoveExtraSlash即使有额外的斜杠,也可以从URL中解析参数。
	// 参见PR#1817和问题#1644
	RemoveExtraSlash bool

	// RemoteIPHeaders用于在`(*gin.Engine).ForwardedByClientIP`为`true`且
	// `(*gin.Context).Request.RemoteAddr`匹配`(*gin.Engine).SetTrustedProxies()`定义的网络源列表之一时,
	// 获取客户端IP的头部列表。
	RemoteIPHeaders []string

	// TrustedPlatform如果设置为值为gin.Platform*的常数,将信任该平台设置的头部,例如用于确定客户端IP
	TrustedPlatform string

	// MaxMultipartMemory是传递给http.Request的ParseMultipartForm方法调用的'maxMemory'参数的值。
	MaxMultipartMemory int64

	// UseH2C启用h2c支持。
	UseH2C bool

	// ContextWithFallback在Context.Request.Context()不为nil时,启用回退Context.Deadline(),Context.Done(),Context.Err()和Context.Value()。
	ContextWithFallback bool

	delims           render.Delims
	secureJSONPrefix string
	HTMLRender       render.HTMLRender
	FuncMap          template.FuncMap
	allNoRoute       HandlersChain
	allNoMethod      HandlersChain
	noRoute          HandlersChain
	noMethod         HandlersChain
	pool             sync.Pool    
	trees            methodTrees  //方法树列表,是一个数组
	maxParams        uint16
	maxSections      uint16
	trustedProxies   []string
	trustedCIDRs     []*net.IPNet
}

在这个结构体里面,还内嵌了一个很重要的结构体

RouterGroup,这种内嵌的效果,其实类似于其他语言中的继承,Engine结构体得到了,RouterGroup结构体的属性和方法

RouterGroup结构体

RouterGroup结构体的源码如下

代码语言:go复制
type RouterGroup struct {
   Handlers HandlersChain   //存储将要执行的函数,包括路由中间件和路由函数
   basePath string          // 路径
   engine   *Engine         // 指向根结构体
   root     bool            // 标志是不是根结构体
}

第一个字段其实就是方法链,用来储存方法,详细结构如下

代码语言:txt复制
// 起别名
type HandlerFunc func(*Context)

// 定义函数切片类型
type HandlersChain []HandlerFunc

到这里你肯定还是懵的,不急,接下来我们再分析一下RouterGroup结构体身上的几个常用的方法,相信到后面,你应该也能明白,大概是怎么一回事

Use方法使用中间件

先是Use方法,我们一般使用Use函数来使用中间件函数,看下面的代码,相信你也明白了,其实Use一个中间件,就是把这个中间件函数放到了RouterGroup中的Handlers函数链里面去了,通过使用append函数,来添加。

代码语言:txt复制
func (group *RouterGroup) Use(middleware ...HandlerFunc) IRoutes {
   group.Handlers = append(group.Handlers, middleware...)
   return group.returnObj()
}

Group方法划分路由组

然后是Group方法,这个方法,我们一般用来划分路由组,就比如说 http://192.168.127.1:9090/demo/one

和http://192.168.127.1:9090/demo/two 我们可以先用Group方法,划分一个demo路由组出来,然后再进行相关的操作。

这个方法,新建了一个RouterGroup结构体,调用了两个函数group.combineHandlers()group.calculateAbsolutePath()函数,然后返回了这个新建的RouterGroup结构体的地址

代码语言:txt复制
func (group *RouterGroup) Group(relativePath string, handlers ...HandlerFunc) *RouterGroup {
   return &RouterGroup{
      Handlers: group.combineHandlers(handlers), //该字段存储将要执行的函数,包括路由中间件和路由函数
      basePath: group.calculateAbsolutePath(relativePath), // 路径
      engine:   group.engine,   // 指向根结构体
   }
}
group.combineHandlers()

我们再看看group.combineHandlers()函数,其实就是将新使用的中间件塞入函数链,这个函数是内置,没有开放给外界使用

代码语言:go复制
func (group *RouterGroup) combineHandlers(handlers HandlersChain) HandlersChain {
   finalSize := len(group.Handlers)   len(handlers)
   assert1(finalSize < int(abortIndex), "too many handlers")
   mergedHandlers := make(HandlersChain, finalSize)
   copy(mergedHandlers, group.Handlers)
   copy(mergedHandlers[len(group.Handlers):], handlers)
   return mergedHandlers
}

来看看具体解释

该函数的目的是将当前RouterGroup的中间件处理函数和传入的handlers合并成一个新的中间件处理函数链。

函数接收一个handlers参数,它是一个HandlersChain类型,表示一组中间件处理函数。HandlersChain是一个自定义类型,它实际上是一个切片,存储了多个中间件处理函数。

函数首先计算了合并后的中间件处理函数链的长度,通过将当前RouterGroup中已有中间件处理函数的数量和传入的handlers的数量相加得到。然后,使用assert1函数进行断言,确保合并后的长度没有超过abortIndex的最大值。abortIndex是一个常量,用于限制中间件处理函数链的最大长度。

接下来,函数创建了一个长度为finalSize的切片mergedHandlers,用于存储合并后的中间件处理函数链。然后,通过copy函数将当前RouterGroup的中间件处理函数复制到mergedHandlers的起始位置,再将传入的handlers复制到mergedHandlers的末尾位置。

最后,函数返回合并后的中间件处理函数链mergedHandlers

group.calculateAbsolutePath()函数
代码语言:go复制
func (group *RouterGroup) calculateAbsolutePath(relativePath string) string {
   return joinPaths(group.basePath, relativePath)
}

group.calculateAbsolutePath()函数,用于计算绝对路径。它接收一个相对路径作为参数,然后调用joinPaths函数将相对路径与路由组的基本路径拼接在一起,得到一个完整的绝对路径。

joinPaths函数接收一个绝对路径和一个相对路径作为参数,将它们拼接在一起并返回拼接后的路径。首先,函数会检查相对路径是否为空,如果为空,则直接返回绝对路径。接下来,函数使用path.Join函数将绝对路径和相对路径拼接在一起,得到最终路径。然后,函数会检查相对路径的最后一个字符是否为'/',如果是,并且最终路径的最后一个字符不是'/',则在最终路径的末尾添加'/'。最后,函数返回最终路径。

这段代码的作用是将路由组的基本路径和相对路径拼接在一起,得到一个完整的绝对路径。它处理了相对路径为空和最终路径的最后一个字符的情况,确保返回的路径是正确的。

然后介绍的就是Next方法,这个方法常用在中间件里面,当我们在一个中间件中需要执行后面的中间件,我们就可以使用Next函数,如下,其实就是执行了函数链中的下一个函数,对了,我们通过`gin.Default()

得到的默认的日志中间件,其实就是用了Next`方法

代码语言:txt复制
func (c *Context) Next() {
   c.index  
   for c.index < int8(len(c.handlers)) {
      c.handlers[c.index](c)
      c.index  
   }
}

当路由被访问的时候,然后就会依次执行函数链里面的函数。

总结

    1. Enginegin的核心结构体,它包含了RouterGroup结构体,实现了路由的注册和中间件的添加等功能。RouterGroup用来表示一个路由组,它包含了中间件处理函数链Handlers、路径basePath、引擎指针engine等字段。RouterGroup实现了路由组相关的添加路由,添加中间件等操作。
    1. 当路由被匹配时,会依次调用Handlers链中的中间件函数和路由处理函数。
    1. EngineRouterGroup实现了分组路由和中间件的机制,形成了路由注册和中间件添加的链式调用风格式, gin通过这种机制,提供了强大的路由与中间件功能,形成了简洁的API风格。

我正在参与2023腾讯技术创作特训营第三期有奖征文,组队打卡瓜分大奖!

0 人点赞