浅析 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 的实现是一个连接新建一个 goroutinefasthttp 是利用一个 worker 复用 goroutine,减轻 runtime 调度 goroutine 的压力。

web 服务启动流程

  1. 核心概念:
    • Request:用户请求的信息:用来解析用户的请求信息,包括 post、get、cookie、url 等信息。
    • Response:服务器需要反馈给客户端的信息。
    • 用户的每次请求连接。
    • Handler:处理请求和生成返回信息的处理函数。
    • Router:路由根据 url 找到对应的 Handler 并执行
  2. 客户端与服务端的交互来自 clientrequestserverresponse
    • Router 实现一个 Multiplexer, Multiplexer 的目的是为了找到 path 对应的处理函数 handler
    • handler 对 request 进行处理,同时构建 response
    • 具体流程是: Client -> Requests -> Multiplexer(router) -> handler -> Response -> Client
  3. Golang 中的 http 服务最重要的就是 Multiplexerhandler,Golang 中的 Multiplexer 基于 ServeMux 结构实现。

net/http 库中的 web 服务启动流程

核心结构

  1. Handler 接口: 所有请求的处理器、路由 ServeMux 都满足该接口。任何结构体,只要实现了 ServeHTTP 方法,这个结构就可以称之为 handler 对象。
  2. ServeMux 结构体: HTTP 请求的 Multiplexer(路由)。负责将每一个接收到的请求的 URL 与一个注册模式的列表进行匹配,并调用和 URL 最匹配的模式的处理器,其内部用一个 map 来保存所有处理器 Handler
    • muxEntry 结构体:保存了 Handler 请求处理器和匹配的模式字符串
    • 包级别变量 DefaultServeMux:ServeMux 的一个实例,表示默认路由,使用包级别的 http.Handle()http.HandleFunc() 方法注册处理器时都是注册到该路由中。
    • ServerMux 结构体的 ServeHTTP() 方法: 位于 ServerMux 结构体中,满足 Handler 接口。主要用于间接调用它所保存的 muxEntry 中保存的 Handler 处理器的 ServeHTTP() 方法。
  3. http.HandlerFunc 适配器:
    • 满足 Handler 接口,这个类型默认就实现了 ServeHTTP
    • 只要函数的签名为 func(w http.ResponseWriter, r *http.Request),均可作为处理函数,因为它可以被转换为 http.HandlerFunc 函数类型
  4. Server 结构体: 用户可以定义一个 struct,其中实现了 ServeHTTP 方法,作为 handler 对象传给 Server
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// Handler接口
type Handler interface {
ServeHTTP(ResponseWriter, *Request) //路由具体实现
}

// ServeMux结构体
type ServeMux struct {
mu sync.RWMutex //锁,由于请求涉及到并发处理,因此这里需要一个锁机制
m map[string] muxEntry // 路由规则,一个string对应一个mux实体,这里的string就是注册的路由表达式
hosts bool // 是否在任意的规则中带有host信息
}

// muxEntry 结构体
type muxEntry struct {
h Handler // 控制器
pattern string // 匹配字符串
}

// 包级别变量 DefaultServeMux
func NewServeMux() *ServeMux { return new(ServeMux) }
var DefaultServeMux = NewServeMux()

// HandlerFunc 适配器
type HandlerFunc func(ResponseWriter, *Request)
// ServeHTTP calls f(w, r).
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
f(w, r)
}

// Server 结构体
type Server struct {
Addr string // TCP address to listen on, ":http" if empty
Handler Handler // Handler是一个interface
...
}

HTTP 的使用模式

  1. 使用默认的路由来注册处理函数
  2. 使用自定义的路由来注册处理函数
  3. 直接自定义一个 Server 实例:该模式可以很方便的管理服务端的行为
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
func main() {
...

// 1. 向默认路由注册处理器函数
http.HandleFunc("/", serveHome) //或http.Handle("/", http.HandlerFunc(serveHome))
http.Handle("/file", myHandler("somefile"))

err := http.ListenAndServe(*addr, nil) //启动监听,第二个参数nil表示使用默认路由DefaultServeMux中注册的处理器
if err != nil {
log.Fatalln("ListenAndServe: ", err)
}

// 2. 使用自定义的路由来注册处理函数
mux := http.NewServeMux() //新建一个自定义的路由
mux.Handle("/file", myHandler("somefile"))
mux.HandleFunc("/", serveHome)

err := http.ListenAndServe(*addr, mux) //启动监听
if err != nil {
log.Fatalln("ListenAndServe: ", err)
}

// 3. 直接自定义一个Server实例
mux := http.NewServeMux()
mux.Handle("/file", myHandler("somefile"))
mux.HandleFunc("/", serveHome)

s := &http.Server{
Addr: ":8080",
Handler: mux, //指定路由或处理器,不指定时为nil,表示使用默认的路由DefaultServeMux
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
MaxHeaderBytes: 1 << 20,
ConnState: //指定连接conn的状态改变时的处理函数
//....
}
log.Fatal(s.ListenAndServe())
}

完整执行过程:监听端口、建立连接和处理 client 请求

  1. 通过 http.ListenAndServe(addr string, handler Handler) 启动服务,实例化 Server,调用 Server 对象的 ListenAndServer 方法,并将该方法的返回值 error 返回给调用方。
  2. server.ListenAndServe() 内部调用 net.Listen("tcp", addr),该方法内部又调用 net.ListenTCP() 创建并返回一个 net.Listener 监听器 ln。
  3. 然后把监听器 ln 断言转换为 TCPListener 类型,并根据它构造一个 tcpKeepAliveListener 对象并传递给 server.Serve() 方法。这是因为 TCPListener 实现了 Listener 接口,所以 tcpKeepAliveListener 也实现了 Listener 接口,并且它重写了 Accept() 方法,目的是为了调用 SetKeepAlive(true),让操作系统为收到的每一个连接启动发送 keepalive 消息(心跳,为了保持连接不断开)。
  4. server.Serve() 方法调用 tcpKeepAliveListener 对象的 Accept() 方法返回一个连接conn(该连接启动了心跳),并为每一个conn创建一个新的go协程执行 conn.server() 方法
  5. conn.server() 方法会读取请求,然后根据 conn 内保存的 server 来构造一个 serverHandler 类型,并调用它的 ServeHTTP() 方法:serverHandler{c.server}.ServeHTTP(w, w.req)
  6. 路由 ServeMuxServeHTTP 方法则会根据当前请求提供的信息来查找最匹配的 Handler
  7. 查找到的 Handler 接口值 h 就是我们事先注册到路由中与请求匹配的 Handler;而h的动态类型是 HandlerFunc 类型(它也满足 Handler 接口);所以,以上 h.ServeHTTP(w, r) 实际上调用的是接口值 h 中持有的动态值(也就是我们定义的处理函数,详见 http.HandlerFunc 适配器)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
// (1) 实例化 server
// The handler is typically nil, in which case the DefaultServeMux is used.
func ListenAndServe(addr string, handler Handler) error {
server := &Server{Addr: addr, Handler: handler}
return server.ListenAndServe()
}
// (2) 返回监听器
func (srv *Server) ListenAndServe() error {
if srv.shuttingDown() {
return ErrServerClosed
}
addr := srv.Addr
if addr == "" {
addr = ":http"
}
ln, err := net.Listen("tcp", addr)
if err != nil {
return err
}
// Accepted connections are configured to enable TCP keep-alives.
return srv.Serve(tcpKeepAliveListener{ln.(*net.TCPListener)})
}
// (3) 启动发送keepalive消息
type tcpKeepAliveListener struct {
*net.TCPListener
}
func (ln tcpKeepAliveListener) Accept() (c net.Conn, err error) {
tc, err := ln.AcceptTCP()
if err != nil {
return
}
tc.SetKeepAlive(true) //发送心跳
tc.SetKeepAlivePeriod(3 * time.Minute) //发送周期
return tc, nil
}
// (4) 启用 go 协程执行 conn.server() 方法
func (srv *Server) Serve(l net.Listener) error {
defer l.Close()
if fn := testHookServerServe; fn != nil {
fn(srv, l)
}
var tempDelay time.Duration //重试间隔

if err := srv.setupHTTP2_Serve(); err != nil {
return err
}

srv.trackListener(l, true) //缓存该监听器
defer srv.trackListener(l, false) //从缓存中删除当前监听器

baseCtx := context.Background()
ctx := context.WithValue(baseCtx, ServerContextKey, srv) //新建一个context用来管理每个连接conn的Go协程
for {
rw, e := l.Accept() //调用tcpKeepAliveListener对象的 Accept() 方法
if e != nil {
select {
case <-srv.getDoneChan():
return ErrServerClosed //退出Serve方法,并执行延迟调用(从缓存中删除当前监听器)
default:
}
//如果发生了net.Error错误,则隔一段时间就重试一次,间隔时间每次翻倍,最大为1秒
if ne, ok := e.(net.Error); ok && ne.Temporary() {
if tempDelay == 0 {
tempDelay = 5 * time.Millisecond
} else {
tempDelay *= 2
}
if max := 1 * time.Second; tempDelay > max {
tempDelay = max
}
srv.logf("http: Accept error: %v; retrying in %v", e, tempDelay)
time.Sleep(tempDelay)
continue
}
return e
}
tempDelay = 0
c := srv.newConn(rw) //该方法根据net.Conn、srv构造了一个新的http.conn类型
c.setState(c.rwc, StateNew) //缓存该连接的状态,如果方法:Server.ConnState(net.Conn, ConnState)不为nil,就根据当前连接的状态执行它
go c.serve(ctx)
}
}
// (5) 调用ServeHTTP()方法
func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {
handler := sh.srv.Handler
// handler == nil 时使用默认的DefaultServeMux路由
// 否则使用在第1步中为Serve指定了的Handler
if handler == nil {
handler = DefaultServeMux
}
if req.RequestURI == "*" && req.Method == "OPTIONS" {
handler = globalOptionsHandler{}
}
handler.ServeHTTP(rw, req)
}
// (6) 查找最匹配的Handler
func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) {
if r.RequestURI == "*" {
if r.ProtoAtLeast(1, 1) {
w.Header().Set("Connection", "close")
}
w.WriteHeader(StatusBadRequest)
return
}
h, _ := mux.Handler(r) //规范化请求的路径格式,查找最匹配的Handler
h.ServeHTTP(w, r)
}

Gin 框架中的路由设置

Gin 核心结构

gin.Context

  1. 最重要的结构,贯穿一个 http 请求的所有流程,包含全部上下文信息。
  2. 提供了很多内置的数据绑定和响应形式,JSON、HTML、Protobuf 、MsgPack、Yaml 等,它会为每一种形式都单独定制一个渲染器
  3. engine的 ServeHTTP 方法,在响应一个用户的请求时,都会先从临时对象池中取一个 context 对象。使用完之后再放回临时对象池。为了保证并发安全,如果在一次请求新起一个协程,那么一定要 copy 这个 context 进行参数传递
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
type Context struct {
writermem responseWriter
Request *http.Request // 请求对象
Writer ResponseWriter // 响应对象

Params Params // URL 匹配参数
handlers HandlersChain // 请求处理链
index int8
engine *Engine

// This mutex protect Keys map
mu sync.RWMutex

// Keys is a key/value pair exclusively for the context of each request.
Keys map[string]interface{}

// Errors is a list of errors attached to all the handlers/middlewares who used this context.
Errors errorMsgs

// Accepted defines a list of manually accepted formats for content negotiation.
Accepted []string
...
}

gin.Engine

  1. 框架的入口,通过 Engine 对象来定义服务路由信息、组装插件、运行服务,是框架的核心发动机,整个 Web 服务的都是由它来驱动的。
  2. 用的是 Go 语言内置的 http serverEngine 的本质只是对内置的 HTTP 服务器的包装。
  3. 关键字段
    • trees:Router Tree 基于基数树实现。每个节点都会挂接若干请求处理函数构成一个请求处理链 HandlersChain。当一个请求到来时,在这棵树上找到请求 URL 对应的节点,拿到对应的请求处理链来执行就完成了请求的处理。
    • addRoute 方法:用于添加 URL 请求处理器,它会将对应的路径和处理器挂接到相应的请求树中。
    • RouterGroupRouterGroup 是对路由树的包装,所有的路由规则最终都是由它来进行管理。Engine 结构体继承了 RouterGroup ,所以 Engine 直接具备了 RouterGroup 所有的路由管理功能。同时 RouteGroup 对象里面还会包含一个 Engine 的指针,可以调用 engineaddRoute 方法。
    • IRouter 接口:RouterGroup 实现了 IRouter 接口,暴露了一系列路由方法,这些方法最终都是通过调用 Engine.addRoute 方法将请求处理器挂接到路由树中
    • RouterGroup 内部有一个前缀路径属性,它会将所有的子路径都加上这个前缀再放进路由树中。有了这个前缀路径,就可以实现 URL 分组功能。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
func New() *Engine {
debugPrintWARNINGNew()
engine := &Engine{
RouterGroup: RouterGroup{
Handlers: nil,
basePath: "/",
root: true,
},
FuncMap: template.FuncMap{},
RedirectTrailingSlash: true,
RedirectFixedPath: false,
HandleMethodNotAllowed: false,
ForwardedByClientIP: true,
RemoteIPHeaders: []string{"X-Forwarded-For", "X-Real-IP"},
TrustedProxies: []string{"0.0.0.0/0"},
AppEngine: defaultAppEngine,
UseRawPath: false,
RemoveExtraSlash: false,
UnescapePathValues: true,
MaxMultipartMemory: defaultMultipartMemory,
trees: make(methodTrees, 0, 9),
delims: render.Delims{Left: "{{", Right: "}}"},
secureJSONPrefix: "while(1);",
}
engine.RouterGroup.engine = engine
engine.pool.New = func() interface{} {
return engine.allocateContext()
}
return engine
}

gin.RouterGroup

  1. 方便对路由进行管理,可以对组进行设置中间件。
  2. RouterGroup 是对路由树的包装,所有的路由规则最终都是由它来进行管理。Engine 结构体继承了 RouterGroup ,所以 Engine 直接具备了 RouterGroup 所有的路由管理功能。同时 RouteGroup 对象里面还会包含一个 Engine 的指针,可以调用 engine 的 addRoute 方法。
  3. RouterGroup 实现了 IRouter 接口,暴露了一系列路由方法,这些方法最终都是通过调用 Engine.addRoute 方法将请求处理器挂接到路由树中。
  4. RouterGroup 内部有一个前缀路径属性,它会将所有的子路径都加上这个前缀再放进路由树中。有了这个前缀路径,就可以实现 URL 分组功能。Engine 对象内嵌的 RouterGroup 对象的前缀路径是 /,它表示根路径。
  5. RouterGroup 支持分组嵌套,使用 Group 方法就可以让分组下面再挂分组。

radix tree

前缀树是一个多叉树,广泛应用于字符串搜索,每个树节点存储一个字符,从根节点到任意一个叶子结点串起来就是一个字符串。

radix tree 是优化之后的前缀树,对空间进一步压缩,从上往下提取公共前缀,非公共部分存到子节点,这样既节省了空间,同时也提高了查询效率(左边字符串sleep查询需要5步, 右边只需要3步),Gin 的路由树就是用 radix tree 实现的。

前缀树和 radix tree

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

method trees

Gin 为了更具扩展性,每一层的节点按照 priority 排序,一个节点的 priority 值表示他包含的所有子节点(子节点,孙节点等)的数量,这样做有两个好处:

  1. 被最多路径包含的节点会被最先评估。这样可以让尽量多的路由快速被定位。
  2. 有点像成本补偿。最长的路径可以被最先评估,补偿体现在最长的路径需要花费更长的时间来定位,如果最长路径的节点能被优先评估(即每次拿子节点都命中),那么所花时间不一定比短路径的路由长。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//树节点
type node struct {
path string //保存节点的路径,像上面radix tree图中节点里面的值
indices string //子节点的首字符根据priority排列组成的字符串,为了方便遍历
wildChild bool //标识孩子节点是否有通配符
nType nodeType //节点类型
priority uint32 //优先级
children []*node
handlers HandlersChain //处理函数链
fullPath string //保存完整路径
}

// node结构中nodeType节点类型
const (
static nodeType = iota // default 普通节点,默认
root // 根节点
param // 参数路由,比如 /user/:id
catchAll // 匹配所有内容的路由,比如 /article/*key
)

中间件与请求链

  1. 每个路由节点都挂一个函数链。
  2. 只有函数链的尾部是业务处理,前面的部分都是插件函数。
  3. 在 Gin 中插件和业务处理函数形式是一样的,都是 func(*Context)。当我们定义路由时,Gin 会将插件函数和业务处理函数合并在一起形成一个链条结构。Gin 在接收到客户端请求时,找到相应的处理链,构造一个 Context 对象,再调用它的 Next() 方法就正式进入了请求处理的全流程。
  4. 4 种常用方法
    • (1) c.Next()
      利用函数调用栈后进先出的特点,巧妙的完成中间件在自定义处理函数完成的后处理的操作。
    • (2) c.Abort()
      原理是将 Context.index 调整到一个比较大的数字,这样 Next() 方法中的调用循环就会立即结束(而不是中断执行流)。执行 Abort() 方法之后,当前函数内后面的代码逻辑还会继续执行
    • (3) c.Set()
    • (4) c.Get()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Next should be used only inside middleware.
// It executes the pending handlers in the chain inside the calling handler.
func (c *Context) Next() {
c.index++
for c.index < int8(len(c.handlers)) {
c.handlers[c.index](c)
c.index++
}
}

const abortIndex int8 = math.MaxInt8 / 2
// Abort prevents pending handlers from being called. Note that this will not stop the current handler.
func (c *Context) Abort() {
c.index = abortIndex
}

完整启动流程

  1. Egnine 初始化:通过调用 gin.New() 方法来实例化 Engine。这个过程中同时会创建Context 对象池,减少频繁context实例化带来的资源消耗。
  2. 注册中间件:将路由处理函数(endpoint)和中间件函数(middleware) 注册到 radix tree 中,tree 中的每个节点可以认为是一个键值对,key 是路由,value 是[ ]HandlerFunc, 里面存储的就是按顺序执行的中间件和handle控制器方法
    • 注册全局中间件
    • 注册路由组中间件
    • 注册路由以及中间件
  3. 启动:通过调用 net/http 来启动服务,由于 engine 实现了 ServeHTTP 方法,只需要直接传 engine 对象就可以完成初始化并启动
  4. 处理请求:通过请求方法和路由找到相对应的树节点,获取储存的 []HandlerFunc 列表,通过调用 c.Next() 处理请求,通过不停的移动下标递归,最后完成处理返回结果

Gin 完整启动流程

Reference