gin框架(3)- Engine与Context
前言
在上一章,我们讲述了request请求是如何在gin中流转的,其中提到了两个比较重要的结构体Engine和Context。Engine在gin中充当server的角色,Context则负责对request的封装(类似net/http中的request),本章详细介绍一下这两个结构体及其作用。
1. Engine
Engine即gin对应的服务端类(Server类),对应net/http中的ServeMux。编写gin的服务时,通常有两种初始化方法:
// 方法1,声明一个DefaultServer func main() { r := gin.Default() // gin.Default返回一个*Engine实例 r.POST("/", ...) // 添加路由 r.Run("localhost:8080") // 启动服务 } // 方法2: 声明一个Server,自主添加中间件 func main() { r := gin.New() r.Use(...) // 添加中间件 r.POST("/", ...) //添加路由 r.Run("localhost:8080") }
// Engine is the framework's instance, it contains the muxer, middleware and configuration settings. // Create an instance of Engine, by using New() or Default() type Engine struct { ... RouterGroup // 路由信息,主要包含路由的路径和对应路径的所有中间件和处理函数 trees methodTrees // 存储所有注册的路由 pool sync.Pool // 缓存所有的context,减少context的频繁gc ... } type RouterGroup struct { Handlers HandlersChain // 存储该路由下的中间件函数和处理函数 basePath string // 路由路径 engine *Engin // 对Engine的引用 root bool // 该RouterGroup实例是否是根路由 } type methodTrees []methodTree type methodTree struct { method string // 函数名称 root *node // 对应的radix_tree节点 }
- 注册路由
- 给某个路由添加中间件
- 接受新的连接
- 当已有连接有数据来临时,调用对应路由下的处理函数(请求处理,此部分开启了单独的goroutine处理)
路由注册和添加路由中间件
注册路由等功能是通过Engine下的RouterGroup实现的,RouterGroup实现了POST, GET,PUT, Group等函数。POST, GET,PUT, DELETE等函数就是在路由树radix_tree上添加一个路由节点;Group则是添加一个路由组,本质上就是在radix_tree上添加了一个非叶节点的路由节点。理解gin的路由实现,以上函数的原理理解起来就非常容易。中间件通过RouterGroup实现的Use函数添加。中间件函数的签名和请求处理函数一致,Use函数就是在RouterGroup.Handlers(HandlerFunc的数组)中添加一个中间件HandlerFunc。
接受新的连接
Engine的Run函数底层就是一个for循环,在循环内为每个新到来的连接创建一个goroutine(本质上是利用了net/http的ListenAndServe函数,实现对新连接的处理)
处理请求
Engine实现了ServeHTTP函数,从上一章1.3的分析中,我们知道ServeHTTP是统一的请求处理函数。ServeHTTP的主要作用就是在Engine.trees中找到跟请求路由对应的HandlersChain,并调用它们处理请求,回写结果(即路由匹配->请求处理->结果回写)。
2. Context
Context是gin的核心结构体之一,主要负责在同一个请求上下游之间传递request。我们都知道golang本身的Context,主要用来设置一次处理的deadline、同步信号,传递请求相关值,相关知识可以参考 《go语言设计与实现》Context。gin的Context就是golang原生Context的延续。官方的注释精确的描述了gin的Context的作用:
Context is the most important part of gin. It allows us to pass variables between middleware, manage the flow, validate the JSON of a request and render a JSON response for example.
type Context struct { ... Request *http.Request // 保存request请求 Writer ResponseWriter // 回写response handlers HandlersChain // 该次请求所有的中间件函数和处理函数 index int8 // HandlersChain的下标,用来调用某个具体的HandlerFunc fullPath string // 请求的url路径 engine *Engine // 对server Engine的引用 Keys map[string]any // 用于上下游之间传递参数 ... }
中间件函数
一次请求的所有中间件函数和请求处理函数都在Context.handlers中。因此,当请求到来时,只需要依次调用Context.handlers中的所有HandlerFunc即可。这就是调用中间件中的一个最重要的函数Next(),定义如下:
func (c *Context) Next() { c.index++
// 依次遍历所有的中间件函数,并调用他们 for c.index < int8(len(c.handlers)) { c.handlers[c.index](c) c.index++ } }
func (c *Context) Abort() { c.index = abortIndex // abortIndex是个常量=63 }
参数传递
Context中有成员Keys,上游需要传递的变量可以放在里面,下游处理时再从里面取出来,实现上下游参数传递。对应Set()和Get()方法。
func (c *Context) Set(key string, value any) { c.mu.Lock() // 用来保护c.Keys并发安全 if c.Keys == nil { c.Keys = make(map[string]any) } c.Keys[key] = value c.mu.Unlock() } func (c *Context) Get(key string) (value any, exists bool) { c.mu.RLock() value, exists = c.Keys[key] c.mu.RUnlock() return }
参数绑定
请求放到Context.Request里,需要解析成结构体或map,才好在业务代码里使用。这一部分是通过Bind()系列函数实现的。Bind()系列函数的作用就是根据request的数据类型,将其解析到结构体或map里。简单的看一个参数绑定的实现:
func (c *Context) ShouldBindWith(obj any, b binding.Binding) error { return b.Bind(c.Request, obj) // 底层调用的是binding相关的函数 }
3. Context和Engine合作完成一次HTTP请求的过程
在 request的流转 中我们分析过,每个新来的连接由一个专门的goroutine去服务,这个goroutine执行主要代码如下:
// 每个新到来的连接会由Server goroutine调用Accept函数,生成一个conn, 然后调用go conn.serve() func (c *conn) serve(ctx context.Context) { ... for { w, err := c.readRequest(ctx) // 读取req请求 if err != nil { // 错误处理 } req = w.req ... // 对req的前置校验 serverHandler{c.server}.ServeHTTP(w, w.req) // 本质上调用的是Engine.ServeHTTP函数 ... } ... }
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) { // 每次从Context的pool里取出一个Context结构体,先reset,然后将本次req的信息绑定到该结构体上 c := engine.pool.Get().(*Context) c.writermem.reset(w) c.Request = req c.reset() engine.handleHTTPRequest(c) engine.pool.Put(c) // 处理完成,回收Context结构体 } func (engine *Engine) handleHTTPRequest(c *Context) { httpMethod := c.Request.Method rPath := c.Request.URL.Path // 获取URL路径 ... // 对URL path进行清洗 t := engine.trees // 读取Engine上注册的路由树,每个HTTP method对应一个radix_tree for i, tl := 0, len(t); i < tl; i++ { if t[i].method != httpMethod { continue } root := t[i].root // 获取到对应http method对应的radix tree // 找到对应的路由上注册的节点 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() // 调用该路由下的所有HandlerFunc c.writermem.WriteHeaderNow() return } // 没有找到路由,执行修复后的path重定向 if httpMethod != http.MethodConnect && rPath != "/" { if value.tsr && engine.RedirectTrailingSlash { redirectTrailingSlash(c) return } if engine.RedirectFixedPath && redirectFixedPath(c, root, engine.RedirectFixedPath) { return } } break } ... }