Go 源码之 gin 框架

Go 源码之 gin 框架

go源码之gin - Jxy 博客

一、总结

  • gin.New()初始化一个实例:gin.engine,该实例实现了http.Handler接口。实现了ServeHTTP方法

  • 注册路由、注册中间件,调用addRoute将路由和中间件注册到 methodTree 前缀树(节省空间,搜索快)下,methodTree 的非叶子节点是相同 method 的 url 的最长公共前缀字符串,叶子节点是完整的 url 路径

  • http请求会执行ServeHTTP方法,内部会根据请求的 method 和 url 到前缀树 methodTree 中匹配 url,然后遍历 handlers 数组,依次执行c.next()执行,可以通过c.Abort()进行中断

  • 流程:

    • 启动:

      初始化engine;初始化一个长度为 9 的 methodTrees 数组;调用 addRoute 注册全局中间件、路由到 methodTree 数组中

    • 处理:

      gin.engine 实现了 http.handler 接口,实现了 ServeHTTP(ResponseWriter, *Request),所有的请求都会经过 ServeHTTP 处理;

      ServeHTTP 方法:从 methodTrees 中找到本次请求的 httpMethod 的 Tree-->根据请求Url Path 找到Tree下对应的节点nodeValue(包含了中间件handler)---> 执行c.Next() 依次执行handler,,handler内部 可以调用c.JSON等往响应的http写入数据

  • 注意点:

    • 路由中间件的执行顺序和添加顺序一致,遵循先进先出规则
    • c.JSON()等是http请求的末端函数,如果要添加后置拦截器,需要在此之前执行c.Next()即可, c.Abort()为终止执行后续的中间件
  • 使用 sync.Pool 来复用上下午对象

  • gin 底层依旧是依赖 net/http 包,本质上是一个路由处理器,实现了 http.handler 接口,实现了 ServeHTTP

  • 维护了 method 数组,每个元素是一个 radix 树(压缩前缀树)

  • gin 的中间件是使用切片实现的,添加中间件也就是切片追加元素的过程,中间件按追加先后顺序依次执行

  • gin 允许为不同的路由组添加不同的中间件

  • 路由组本质上是一个模版,维护了路径前缀、中间件等信息,让用户省去重复配置相同前缀和中间件的操作

  • 新路由组继承父路由组的所有处理器

  • 如果上下文需要并发处理使用,需要使用上下文副本copy

二、源码

(一)engine结构

go 复制代码
// Engine的实例,包括了路由组,路由树,中间件和其他等一系列配置
type Engine struct {
  // 路由组
	RouterGroup

  // 如果当前路径无法匹配,但存在带有(不带有)尾斜杠的路径处理程序,RedirectTrailingFlash将启用自动重定向
  // 例如,如果请求了/foo/,但只有/foo的路由存在,则客户端将被重定向到/foo
  // GET请求的http状态代码为301,所有其他请求方法的http状态为307。
	RedirectTrailingSlash bool

	// RedirectFixedPath如果启用,如果没有为其注册句柄,则路由器将尝试 修复 当前请求路径。
  // 首先删除像../或//这样的多余路径元素。然后,路由器对清理后的路径进行不区分大小写的查找。如果可以找到该路由的句柄,则路由器对GET请求使用状态代码301,
  // 对所有其他请求方法使用状态代码307,重新定向到正确的路径。例如/FOO和/..//FOO可以重定向到/FOO。
  // RedirectTrailingFlash独立于此选项。
	RedirectFixedPath bool

  // HandleMethodNotAllowed如果启用,则如果当前请求无法路由,则路由器会检查当前路由是否允许其他方法。
  // 如果是这种情况,则使用"Method Not Allowed"和 HTTP状态代码405 回答请求。
  // 如果不允许其他方法,则将请求委托给NotFound处理程序。
	HandleMethodNotAllowed bool

	// ForwardedByClientIP(如果启用),将从与存储在"(*gin.Engine).RemoteIPHeaders"中的标头匹配的请求标头解析客户端IP。
  // 如果未提取任何IP,它将返回到从"(*gin.Context).Request.Remoddr"获取的IP。
	ForwardedByClientIP bool

	// AppEngine was deprecated.
	// Deprecated: USE `TrustedPlatform` WITH VALUE `gin.PlatformGoogleAppEngine` INSTEAD
	// #726 #755 If enabled, it will trust some headers starting with
	// 'X-AppEngine...' for better integration with that PaaS.
	AppEngine bool

	// UseRawPath if enabled, the url.RawPath will be used to find parameters.
  // UseRawPath(如果启用),url.RawPath将用于查找参数。
	UseRawPath bool

	// UnescapePathValues如果为true,则路径值将被取消转义。
	// 如果UseRawPath为false(默认情况下),则UnescapePathValues实际上为true,
	// 作为url。将使用路径,该路径已未被覆盖。
	UnescapePathValues bool

	// RemoveExtraSlash a parameter can be parsed from the URL even with extra slashes.
	// See the PR #1817 and issue #1644
	RemoveExtraSlash bool

	// RemoteIPHeaders list of headers used to obtain the client IP when
	// `(*gin.Engine).ForwardedByClientIP` is `true` and
	// `(*gin.Context).Request.RemoteAddr` is matched by at least one of the
	// network origins of list defined by `(*gin.Engine).SetTrustedProxies()`.
	RemoteIPHeaders []string

	// TrustedPlatform if set to a constant of value gin.Platform*, trusts the headers set by
	// that platform, for example to determine the client IP
	TrustedPlatform string

	// MaxMultipartMemory value of 'maxMemory' param that is given to http.Request's ParseMultipartForm
	// method call.
	MaxMultipartMemory int64

	// UseH2C enable h2c support.
	UseH2C bool

	// ContextWithFallback enable fallback Context.Deadline(), Context.Done(), Context.Err() and Context.Value() when Context.Request.Context() is not nil.
	ContextWithFallback bool

	delims           render.Delims
  // https://cloud.tencent.com/developer/article/1580456
	secureJSONPrefix string  
	HTMLRender       render.HTMLRender
  // FuncMap是定义从名称到函数的映射的映射类型。
  // 每个函数必须有一个返回值或两个返回值,其中第二个返回值具有类型错误。
  // 在这种情况下,如果在执行期间第二个(error)参数的计算结果为非nil,则执行终止,Execute返回该错误。
  // FuncMap在"text/template"中具有与FuncMap相同的基本类型,复制到此处,因此客户端无需导入"text/template"。
	FuncMap          template.FuncMap 
	
	trees            methodTrees // 请求的method数组,每个元素是一个前缀树

}

(二)ServeHTTP(核心处理http方法)

go 复制代码
// 所有的http请求最终都会走这里
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
	c := engine.pool.Get().(*Context) // 从池里取一个context
	c.writermem.reset(w) // 重置
	c.Request = req // 赋值请求
	c.reset() // 重置数据

	engine.handleHTTPRequest(c) // 核心处理函数

	engine.pool.Put(c) // 放回缓冲池
}
// 核心处理函数
func (engine *Engine) handleHTTPRequest(c *Context) {
  // 请求的Method类型,如GET等
	httpMethod := c.Request.Method
  // 请求的路径
	rPath := c.Request.URL.Path
	unescape := false
	if engine.UseRawPath && len(c.Request.URL.RawPath) > 0 {
		rPath = c.Request.URL.RawPath
		unescape = engine.UnescapePathValues
	}

	if engine.RemoveExtraSlash {
		rPath = cleanPath(rPath)
	}

	// 从methodTree中匹配method和请求url
	t := engine.trees
	for i, tl := 0, len(t); i < tl; i++ {
		if t[i].method != httpMethod {
			continue
		}
		root := t[i].root
		// 从前缀树methodTree中匹配到叶子节点value
		value := root.getValue(rPath, c.params, c.skippedNodes, unescape)
		if value.params != nil {
			c.Params = *value.params
		}
    // 执行中间件
		if value.handlers != nil {
			c.handlers = value.handlers
			c.fullPath = value.fullPath
			c.Next() // 依次从handlers数组中FIFO执行中间件
			c.writermem.WriteHeaderNow() // 最终写入http响应流responseWriter中
			return
		}
		if httpMethod != http.MethodConnect && rPath != "/" {
			if value.tsr && engine.RedirectTrailingSlash {
				redirectTrailingSlash(c)
				return
			}
			if engine.RedirectFixedPath && redirectFixedPath(c, root, engine.RedirectFixedPath) {
				return
			}
		}
		break
	}

	if engine.HandleMethodNotAllowed {
		for _, tree := range engine.trees {
			if tree.method == httpMethod {
				continue
			}
			if value := tree.root.getValue(rPath, nil, c.skippedNodes, unescape); value.handlers != nil {
				c.handlers = engine.allNoMethod
				serveError(c, http.StatusMethodNotAllowed, default405Body)
				return
			}
		}
	}
	c.handlers = engine.allNoRoute
	serveError(c, http.StatusNotFound, default404Body)
}

(三)methodTrees

在New() 中会初始化容量为9,匹配9个http.Method

每个节点都是压缩前缀树:将相同请求method的url计算出最长公共前缀字符串然后作为子节点

go 复制代码
type methodTrees []methodTree

// 压缩前缀树,存储了http.Method的请求路径
type methodTree struct {
	method string
	root   *node
}
type node struct {
	path      string // 存储:共同的最长前缀字符
	indices   string
	wildChild bool
	nType     nodeType
	priority  uint32
	children  []*node // 有共同的最长前缀字符path的url path
	handlers  HandlersChain
	fullPath  string  // 叶子节点存储的是完整的请求路径
}
// 构建树的函数
func addRoute(){}
go 复制代码
r := gin.Default()
	r.Use(gin.Recovery(), gin.Logger())
	r.GET("/user/GetUserInfo", func(context *gin.Context) {})
	r.GET("/user/GetManyUserInfo", func(context *gin.Context) {})
	r.Run(":9091")

(四)context结构

go 复制代码
// gin.Context是gin框架中最重要的一部分
type Context struct {
	writermem responseWriter // 响应的数据流
	Request   *http.Request //  请求句柄
	Writer    ResponseWriter // 响应的Writer

	handlers HandlersChain // 中间件handler数组
	index    int8 // handler数组的下标,表示已经执行的下标
	fullPath string // 完整的url路径

	engine       *Engine
}

(五)RouterGroup

go 复制代码
// RouterGroup is used internally to configure router, a RouterGroup is associated with
// a prefix and an array of handlers (middleware).
// RouterGroup在内部用于配置路由器,RouterGroup与前缀和处理程序(中间件)数组相关联。
type RouterGroup struct {
	Handlers HandlersChain  // 中间件handler
	basePath string // 基础路径
	engine   *Engine 
	root     bool // 是否是根节点
}

(六)c.GET()

go 复制代码
// 调用了handler,httpMethod=http.MethodGet,其他什么c.POST等都是差不多
func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes {
  
	return group.handle(http.MethodGet, relativePath, handlers) 
}
func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes {
  // 计算出绝对路径
	absolutePath := group.calculateAbsolutePath(relativePath)
  // 将请求的handler和group的全局handler合并
	handlers = group.combineHandlers(handlers)
  // 添加路由到前缀树methodTree中
	group.engine.addRoute(httpMethod, absolutePath, handlers)
	return group.returnObj()
}
// 根据请求的method和path,构建前缀树methodTree
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)

  // 从数组methodTrees中获取对应method的根节点
	root := engine.trees.get(method)
	if root == nil {
    // 不存在,常见根节点
		root = new(node)
		root.fullPath = "/"
		engine.trees = append(engine.trees, methodTree{method: method, root: root})
	}
  // 构建前缀树
	root.addRoute(path, handlers)

	// Update maxParams
	if paramsCount := countParams(path); paramsCount > engine.maxParams {
		engine.maxParams = paramsCount
	}

	if sectionsCount := countSections(path); sectionsCount > engine.maxSections {
		engine.maxSections = sectionsCount
	}
}

(七)c.JSON

go 复制代码
func (c *Context) JSON(code int, obj any) {
	c.Render(code, render.JSON{Data: obj})
}
// 将数据写入c.Writer,但是还没有响应http
func (c *Context) Render(code int, r render.Render) {
	c.Status(code)

	if !bodyAllowedForStatus(code) {
		r.WriteContentType(c.Writer)
		c.Writer.WriteHeaderNow()
		return
	}

	if err := r.Render(c.Writer); err != nil {
		panic(err)
	}
}

(八)c.Next()

非常巧妙的设计,这个设计可以用来暂停执行当前handler,先执行后面的handler,然后再执行当前handler后面的代码

go 复制代码
// handlers是一个中间件执行函数的数组,[]HandlerFunc
func (c *Context) Next() {
  // index记录 已经执行到数组[]HandlerFunc的下标,
  // index++ 继续执行后面的handlerFunc
	c.index++ 
	for c.index < int8(len(c.handlers)) {
		c.handlers[c.index](c)
		c.index++
	}
}

(九)c.Abort()

可以用来终止后面所有handler的执行,

go 复制代码
// 这里将 c.index的值改了超级大,在c.Next()中会判断c.index<len(handler),从而达到终止handler执行的效果
func (c *Context) IsAborted() bool {
	return c.index >= abortIndex
}

三、常见问题

如何设置前置拦截器和后置拦截器

  • 方法一:

    利用 handler 的存储结构:所有的 handler 会按顺序添加到数数组 []HandlerFunc 中,执行的是按FIFO遍历执行,所有先添加的handler会先执行,也就是说越先添加的就是前置拦截器,越晚添加的就是后置拦截器

    go 复制代码
    	r := gin.Default()
    	r.Use(gin.Recovery()) // 前置拦截器
    	r.GET("/user/GetUserInfo", func(context *gin.Context) {}) // 中间执行函数
      r.Use(func(context *gin.Context) {}) // 后置拦截器
  • 方法二

    使用c.Next()方法,在handler函数内部,可以先执行一部分代码,然后执行c.Next(),会遍历执行后续的handler,当所有的handler结束后,在执行当前handler c.Next()之后的代码

    go 复制代码
    	r := gin.Default()
    	r.Use(, func(c *gin.Context) {
        // 前置代码
        
        c.Next() // 执行所有handler
        
        // 后置代码
      }) 
    	r.GET("/user/GetUserInfo", func(context *gin.Context) {}) // 中间执行函数
有劳各位看官 点赞、关注➕收藏 ,你们的支持是我最大的动力!!!
接下来会不断更新 golang 的一些底层源码及个人开发经验(个人见解)!!!
同时也欢迎大家在评论区提问、分享您的经验和见解!!!
相关推荐
捂月11 分钟前
Spring Boot 深度解析:快速构建高效、现代化的 Web 应用程序
前端·spring boot·后端
瓜牛_gn37 分钟前
依赖注入注解
java·后端·spring
Estar.Lee1 小时前
时间操作[取当前北京时间]免费API接口教程
android·网络·后端·网络协议·tcp/ip
喜欢猪猪1 小时前
Django:从入门到精通
后端·python·django
一个小坑货1 小时前
Cargo Rust 的包管理器
开发语言·后端·rust
bluebonnet271 小时前
【Rust练习】22.HashMap
开发语言·后端·rust
uhakadotcom1 小时前
如何实现一个基于CLI终端的AI 聊天机器人?
后端
Iced_Sheep2 小时前
干掉 if else 之策略模式
后端·设计模式
XINGTECODE2 小时前
海盗王集成网关和商城服务端功能golang版
开发语言·后端·golang
程序猿进阶2 小时前
堆外内存泄露排查经历
java·jvm·后端·面试·性能优化·oom·内存泄露