浅析 Gin 框架设计
背景
net/http 虽然具有了路由注册功能,并且可以启动监听端口,实现一个简单的 http 服务,但是 net/http 本身提供的功能还是比较简单、原始,而且暴露的函数签名的参数是 (w http.ResponseWriter, req *http.Request),开发者解析请求和回写结果都不是很方便,因此产生了很多 http 框架。
gin 框架基于 httprouter 实现最重要的路由模块,采用类似字典树一样的数据结构来存储路由与 handle 方法的映射,HTTP 实现和标准库 net/http 是一致的。
其他改进了 net/http 框架的还有 fasthttp,采用了不同于 net/http 的 HTTP 实现,具有极快的运行性能。net/http 的实现是一个连接新建一个 goroutine;fasthttp 是利用一个 worker 复用 goroutine,减轻 runtime 调度 goroutine 的压力。
web 服务启动流程
- 核心概念:- Request:用户请求的信息:用来解析用户的请求信息,包括 post、get、cookie、url 等信息。
- Response:服务器需要反馈给客户端的信息。
- 用户的每次请求连接。
- Handler:处理请求和生成返回信息的处理函数。
- Router:路由根据- url找到对应的- Handler并执行
 
- 客户端与服务端的交互来自 client的request和server的response。- Router 实现一个 Multiplexer, Multiplexer 的目的是为了找到 path 对应的处理函数 handler
- handler 对 request 进行处理,同时构建 response
- 具体流程是: Client->Requests->Multiplexer(router)->handler->Response->Client
 
- Golang 中的 http 服务最重要的就是 Multiplexer和handler,Golang 中的Multiplexer基于ServeMux结构实现。
net/http 库中的 web 服务启动流程
核心结构
- Handler接口: 所有请求的处理器、路由- ServeMux都满足该接口。任何结构体,只要实现了- ServeHTTP方法,这个结构就可以称之为- handler对象。
- ServeMux结构体: HTTP 请求的- Multiplexer(路由)。负责将每一个接收到的请求的 URL 与一个注册模式的列表进行匹配,并调用和 URL 最匹配的模式的处理器,其内部用一个 map 来保存所有处理器- Handler。- muxEntry结构体:保存了- Handler请求处理器和匹配的模式字符串
- 包级别变量 DefaultServeMux:ServeMux的一个实例,表示默认路由,使用包级别的http.Handle()、http.HandleFunc()方法注册处理器时都是注册到该路由中。
- ServerMux结构体的- ServeHTTP()方法: 位于- ServerMux结构体中,满足- Handler接口。主要用于间接调用它所保存的- muxEntry中保存的- Handler处理器的- ServeHTTP()方法。
 
- http.HandlerFunc适配器:- 满足 Handler接口,这个类型默认就实现了ServeHTTP
- 只要函数的签名为 func(w http.ResponseWriter, r *http.Request),均可作为处理函数,因为它可以被转换为http.HandlerFunc函数类型
 
- 满足 
- Server结构体: 用户可以定义一个 struct,其中实现了- ServeHTTP方法,作为- handler对象传给- Server。
| 1 | // Handler接口 | 
HTTP 的使用模式
- 使用默认的路由来注册处理函数
- 使用自定义的路由来注册处理函数
- 直接自定义一个 Server实例:该模式可以很方便的管理服务端的行为
| 1 | func main() { | 
完整执行过程:监听端口、建立连接和处理 client 请求
- 通过 http.ListenAndServe(addr string, handler Handler)启动服务,实例化Server,调用Server对象的ListenAndServer方法,并将该方法的返回值error返回给调用方。
- server.ListenAndServe()内部调用- net.Listen("tcp", addr),该方法内部又调用- net.ListenTCP()创建并返回一个- net.Listener监听器 ln。
- 然后把监听器 ln 断言转换为 TCPListener类型,并根据它构造一个tcpKeepAliveListener对象并传递给server.Serve()方法。这是因为TCPListener实现了Listener接口,所以tcpKeepAliveListener也实现了Listener接口,并且它重写了Accept()方法,目的是为了调用SetKeepAlive(true),让操作系统为收到的每一个连接启动发送keepalive消息(心跳,为了保持连接不断开)。
- server.Serve()方法调用- tcpKeepAliveListener对象的- Accept()方法返回一个连接conn(该连接启动了心跳),并为每一个conn创建一个新的go协程执行- conn.server()方法
- conn.server()方法会读取请求,然后根据- conn内保存的- server来构造一个- serverHandler类型,并调用它的- ServeHTTP()方法:- serverHandler{c.server}.ServeHTTP(w, w.req)
- 路由 ServeMux的ServeHTTP方法则会根据当前请求提供的信息来查找最匹配的Handler
- 查找到的 Handler接口值 h 就是我们事先注册到路由中与请求匹配的Handler;而h的动态类型是HandlerFunc类型(它也满足Handler接口);所以,以上h.ServeHTTP(w, r)实际上调用的是接口值 h 中持有的动态值(也就是我们定义的处理函数,详见http.HandlerFunc适配器)
| 1 | // (1) 实例化 server | 
Gin 框架中的路由设置
Gin 核心结构
gin.Context
- 最重要的结构,贯穿一个 http 请求的所有流程,包含全部上下文信息。
- 提供了很多内置的数据绑定和响应形式,JSON、HTML、Protobuf 、MsgPack、Yaml 等,它会为每一种形式都单独定制一个渲染器
- engine的 ServeHTTP方法,在响应一个用户的请求时,都会先从临时对象池中取一个 context 对象。使用完之后再放回临时对象池。为了保证并发安全,如果在一次请求新起一个协程,那么一定要 copy 这个 context 进行参数传递
| 1 | type Context struct { | 
gin.Engine
- 框架的入口,通过 Engine对象来定义服务路由信息、组装插件、运行服务,是框架的核心发动机,整个Web服务的都是由它来驱动的。
- 用的是 Go 语言内置的 http server,Engine的本质只是对内置的 HTTP 服务器的包装。
- 关键字段- trees:Router Tree 基于基数树实现。每个节点都会挂接若干请求处理函数构成一个请求处理链 HandlersChain。当一个请求到来时,在这棵树上找到请求 URL 对应的节点,拿到对应的请求处理链来执行就完成了请求的处理。
- addRoute 方法:用于添加 URL 请求处理器,它会将对应的路径和处理器挂接到相应的请求树中。
- RouterGroup:- RouterGroup是对路由树的包装,所有的路由规则最终都是由它来进行管理。- Engine结构体继承了- RouterGroup,所以- Engine直接具备了 RouterGroup 所有的路由管理功能。同时- RouteGroup对象里面还会包含一个- Engine的指针,可以调用- engine的- addRoute方法。
- IRouter接口:- RouterGroup实现了- IRouter接口,暴露了一系列路由方法,这些方法最终都是通过调用 Engine.addRoute 方法将请求处理器挂接到路由树中
- RouterGroup内部有一个前缀路径属性,它会将所有的子路径都加上这个前缀再放进路由树中。有了这个前缀路径,就可以实现 URL 分组功能。
 
- trees:Router Tree 基于基数树实现。每个节点都会挂接若干请求处理函数构成一个请求处理链 
| 1 | func New() *Engine { | 
gin.RouterGroup
- 方便对路由进行管理,可以对组进行设置中间件。
- RouterGroup是对路由树的包装,所有的路由规则最终都是由它来进行管理。Engine 结构体继承了- RouterGroup,所以- Engine直接具备了- RouterGroup所有的路由管理功能。同时- RouteGroup对象里面还会包含一个- Engine的指针,可以调用 engine 的- addRoute方法。
- RouterGroup实现了 IRouter 接口,暴露了一系列路由方法,这些方法最终都是通过调用- Engine.addRoute方法将请求处理器挂接到路由树中。
- RouterGroup内部有一个前缀路径属性,它会将所有的子路径都加上这个前缀再放进路由树中。有了这个前缀路径,就可以实现 URL 分组功能。Engine 对象内嵌的- RouterGroup对象的前缀路径是 /,它表示根路径。
- RouterGroup支持分组嵌套,使用- Group方法就可以让分组下面再挂分组。
radix tree
前缀树是一个多叉树,广泛应用于字符串搜索,每个树节点存储一个字符,从根节点到任意一个叶子结点串起来就是一个字符串。
radix tree 是优化之后的前缀树,对空间进一步压缩,从上往下提取公共前缀,非公共部分存到子节点,这样既节省了空间,同时也提高了查询效率(左边字符串sleep查询需要5步, 右边只需要3步),Gin 的路由树就是用 radix tree 实现的。

Gin为每一种请求都维护了一个 radix tree,不同的请求会被解析并送到对应的 radix tree 进行处理。

Gin 为了更具扩展性,每一层的节点按照 priority 排序,一个节点的 priority 值表示他包含的所有子节点(子节点,孙节点等)的数量,这样做有两个好处:
- 被最多路径包含的节点会被最先评估。这样可以让尽量多的路由快速被定位。
- 有点像成本补偿。最长的路径可以被最先评估,补偿体现在最长的路径需要花费更长的时间来定位,如果最长路径的节点能被优先评估(即每次拿子节点都命中),那么所花时间不一定比短路径的路由长。
| 1 | //树节点 | 
中间件与请求链
- 每个路由节点都挂一个函数链。
- 只有函数链的尾部是业务处理,前面的部分都是插件函数。
- 在 Gin 中插件和业务处理函数形式是一样的,都是 func(*Context)。当我们定义路由时,Gin 会将插件函数和业务处理函数合并在一起形成一个链条结构。Gin 在接收到客户端请求时,找到相应的处理链,构造一个 Context 对象,再调用它的 Next() 方法就正式进入了请求处理的全流程。
- 4 种常用方法- (1) c.Next()
 利用函数调用栈后进先出的特点,巧妙的完成中间件在自定义处理函数完成的后处理的操作。
- (2) c.Abort()
 原理是将 Context.index 调整到一个比较大的数字,这样 Next() 方法中的调用循环就会立即结束(而不是中断执行流)。执行 Abort() 方法之后,当前函数内后面的代码逻辑还会继续执行
- (3) c.Set()
- (4) c.Get()
 
- (1) c.Next()
| 1 | // Next should be used only inside middleware. | 
完整启动流程
- Egnine 初始化:通过调用 gin.New() 方法来实例化 Engine。这个过程中同时会创建Context 对象池,减少频繁context实例化带来的资源消耗。
- 注册中间件:将路由处理函数(endpoint)和中间件函数(middleware) 注册到 radix tree 中,tree 中的每个节点可以认为是一个键值对,key 是路由,value 是[ ]HandlerFunc, 里面存储的就是按顺序执行的中间件和handle控制器方法。- 注册全局中间件
- 注册路由组中间件
- 注册路由以及中间件
 
- 启动:通过调用 net/http 来启动服务,由于 engine 实现了 ServeHTTP 方法,只需要直接传 engine 对象就可以完成初始化并启动
- 处理请求:通过请求方法和路由找到相对应的树节点,获取储存的 []HandlerFunc 列表,通过调用 c.Next() 处理请求,通过不停的移动下标递归,最后完成处理返回结果
