Gin源码分析一:引擎 Engine

HTTP 标准库 中解释了 go 的标准库是如何处理请求的,但是通过源码的分析,可以发现,标准库对于这部分的处理比较简单,例如对 url 中携带参数就不支持,面对这种情况,社区中出现了大量的框架,对原有的 http 进行补充。

大部分的 Go 的 HTTP 框架都是在重写路由部分,已实现更快的更准确的路由查找,减少路由解析过程中消耗的时间,来提高框架的处理速度。

先来回顾一下标准库的 HTTP 处理流程。

  1. 启动 HTTP 服务器:使用 http.ListenAndServe 或 http.ListenAndServeTLS 函数启动 HTTP 服务器。

  2. 处理请求:当服务器接收到 HTTP 请求时,它会使用与路径相对应的 http.Handler 实现处理请求。

  3. 调用处理程序:服务器会调用 ServeHTTP 方法,并将请求相关的信息作为参数传递给该方法。

  4. 路由匹配:在 ServeHTTP 方法中,通过比较请求的路径和已注册的路由,找到与请求匹配的路由。

  5. 调用处理函数:如果找到了匹配的路由,则调用与该路由相关的处理函数。

  6. 写入响应:处理函数通过 ResponseWriter 接口写入响应数据,以返回给客户端。

根据上述的处理方式,目前需要关注两个函数:ServeHTTPResponseWriter 。这是两个主要的处理方式。

http.ListenAndServehttp.ListenAndServeTLS 用于启动 HTTP 服务器;以及 http.ResponseWriterhttp.Request 分别用于写入响应和处理请求。

同时通过源码分析,如果在 Listen 的时候不传入 handler, 那么就会采用 http 默认的 Handler,也就是 ServeMux ,所以只要我们按照标准实现一个 Handler,那么就会替换原有的处理逻辑,而且 Handler 的实现也是非常简单,只要实现 ServeHTTP 这个接口即可。

采用 gin 实现一个最简单的 ping/pong 服务。这是官方 README 提供的例子。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
package main

import (
  "net/http"

  "github.com/gin-gonic/gin"
)

func main() {
  r := gin.Default()
  r.GET("/ping", func(c *gin.Context) {
    c.JSON(http.StatusOK, gin.H{
      "message": "pong",
    })
  })
  r.Run() // listen and serve on 0.0.0.0:8080 (for windows "localhost:8080")
}

例子中实现也是非常简单,先通过 gin.Default() 生成一个 engine。 之后添加相关的处理方式,最后用 r.Run() 启动。下面将会从最开始的方法开始剖析 gin 框架。

Default 是 gin 使用的第一个函数。这个函数看起来很简单,一共就五行代码。

1
2
3
4
5
6
func Default() *Engine {
	debugPrintWARNINGDefault()
	engine := New()
	engine.Use(Logger(), Recovery())
	return engine
}

其中主要通过 New() 函数初始化 Engine 结构体。在 gin 中,通过 Engine 这个结构体进行管理,这个结构体其实是实现了 ServeHTTP 这个方法。

在这里 Engine 的功能和标准库中的 ServeMux 的地位其实是一模一样的,那么他的主要功能也就是用来保存注册的 handler,等到使用的时候进行查找调用。那么就先看看路由和 handler 是如何注册的。

在上面的例子中, 路由和 handler 是通过 r.GET() 方法进行注册。从源码来看,可以发现不仅仅是 GET 方法,其他的请求方法也一样,都是直接调用了 group.handler 方法。

1
2
3
4
5
6
7
8
9
func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes {
	// 计算绝对路径
	absolutePath := group.calculateAbsolutePath(relativePath)
	// 合并 handler
	handlers = group.combineHandlers(handlers)
	// 添加相关路由
	group.engine.addRoute(httpMethod, absolutePath, handlers)
	return group.returnObj()
}

从代码中可以看到这个方法,其实是比较简单,从代码上的命名来看,基本进行了以下操作:先进行绝对路径的计算,之后对 handler 进行合并,最后添加路由。

combineHandlers方法中,首先计算了当前handlers的长度,并判断是否超过了最大长度(当前最大长度为63)。如果超过最大长度,则会引发panic异常。在这里,源代码采用了两次复制(copy)操作。第一次是将group.Handlers中的数据复制到mergedHandlers中,第二次是将handlers的数据传入mergedHandlers中。

1
2
3
4
5
6
7
8
func (group *RouterGroup) combineHandlers(handlers HandlersChain) HandlersChain {
	finalSize := len(group.Handlers) + len(handlers)
	// ...
	mergedHandlers := make(HandlersChain, finalSize)
	copy(mergedHandlers, group.Handlers)
	copy(mergedHandlers[len(group.Handlers):], handlers)
	// ...
}

这样 mergeHandler 中就存在两部分数据, handlers 可以通过源码看出来是在项目中注册的 handler,那么 group.Handlers 中又是什么呢?

其实就是在项目启动的时候注册的中间件。在 Engine 中的 Default 方法可以看到,engine.Use(Logger(), Recovery()) 项目在初始化的时候注册了两个中间件,而这个 Use 方法,其实就是将中间件添加到上面的 group.Handlers 中,这里不多赘述,只是简单的说明一下,具体的中间件的流程会在 中间件(Middleware) 章节讲述。

确切地说,mergeHandlers的目的是将中间件和用户定义的处理函数合并为一个处理函数切片,并按照一定的顺序注册到路由中。

路由添加方法也比较简单,核心代码一共就 6 行。

1
2
3
4
5
6
7
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)

先通过请求方法获取树的根节点,如果根节点不存在就创建一个,最后添加相关的路由和 handlers。这里关于路由如何添加,路由数据结构在路由(Router)章节会进行讲解。

直到目前,路由的注册工作已经完成。

在之前详细的了解过 go 的 net/http 包中是如何启动一个 http 服务之后,其实现在回过头来看 gin,其实一切变得很简单。

在 gin 的 Run 方法中,主要通过标准库的 http.ListenAndServe 方法启动,而这个方法在 HTTP 标准库中有过详细的分析 ,剩下的方法流程和标准库中的流程基本一致,唯独不同的一点是将原有的默认 Handler 换成了 gin.Handler。

在之前说过,要想成为一个 Handler,只要实现 ServeHTTP 方法即可,而 gin 的 engine 就实现了这个方法。

根据之前对 http 了解的处理流程来看,在 gin 收到相关的请求,都会统一调用 ServeHTTP 方法,该方法会将接收到的参数等进行处理,例如寻找合适的处理器(Handler),最后返回统一的处理结果。

1
2
3
4
5
6
7
8
9
// 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)
}

首先使用到的变量有 pool。这里的 pool 使用的 sync.Pool 这个类型,主要是用来重复使用的 Context。这里直接从 pool 中取出 Context,并对 Context 的一些参数进行设置,最后调用 engine.handleHTTPRequest 方法。

这也是目前常常使用

engine.handleHTTPRequest 这个方法主要处理用户的 HTTP 请求,确定该请求的处理方法。简单的来说就:首先,获取请求的 HTTP 方法(如 GET 或 POST)和 URL 路径,并在需要时解码路径。然后,它搜索匹配该请求的处理树。如果找到了一个匹配的节点,它会将处理程序分配给请求的上下文(c),并写入 HTTP 响应头。如果未找到匹配的节点,则会通过 serverError 写入 “405 Method Not Allowed” 或 “404 Not Found” 的错误响应。

这样基本就是一个简单的 http 请求的处理过程。在代码中可以看到很多事情其实是由上下文 (Context) 进行处理的。

相关内容