Gin web框架

gin是一个基于Golang标准库net/http封装的HTTPweb框架。
它提供了方便的路由注册功能,支持捕获URL参数,提供了中间件机制来串连请求处理流程,提供了方便的数据获取和输出方法,所有这些功能提升了开发web服务的效率。
本文将从以下六个方面介绍gin的实现。

Engine

基于Golang标准库net/http的web服务都需要实现http.Handler接口,而Engine就是gin实现该接口的类,它同时也实现了注册路由、run服务等方法。
ServeHTTP是HTTP请求的入口,而主要逻辑在handleHTTPRequest方法中实现。

1
2
3
4
5
6
7
// 标准库HTTP接口
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
// 接口实现
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request)
func (engine *Engine) handleHTTPRequest(c *Context)

以下是几个比较重要的成员。
RouterGroup是路由注册的类,收集路由信息和对应的HandlerChain,实际注册还是调用的Engine.addRoute方法。
trees是底层组织路由的结构。
pool是分配gin.Context的池,优化内存分配。
Render用于数据渲染。

1
2
3
4
5
6
7
8
9
type Engine struct {
RouterGroup // 路由,engine作为根路由

pool sync.Pool // context池
trees methodTrees // 路由

HTMLRender render.HTMLRender // 渲染数据
FuncMap template.FuncMap // 渲染数据
}

路由

gin定义了两个接口来处理路由信息
IRoutes包含设置中间件、设置HTTP处理函数以及静态文件等方法,应用程序通过它来注册应用的路由处理逻辑。
IRouter继承了IRoutes,另外添加了一个Group方法来按URL路径层次组织router。

RouterGroup实现了IRouter接口,字段包含路径以及请求处理链,保存了Engine的指针,注册路由是通过调用engine.addRoute实现的。

1
2
3
4
5
6
type RouterGroup struct {
Handlers HandlersChain // 处理链
basePath string
engine *Engine
root bool // 如果是根,那么返回的IRoutes接口对象为engine;否则返回自身
}

addRoute方法根据不同HTTP method获取路由的根节点,然后按路径加入到基数树中。

1
2
3
4
5
6
7
8
9
func (engine *Engine) addRoute(method, path string, handlers HandlersChain) {
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)
}

trees类型定义

1
2
3
// 路由组织成一个森林,不同HTTP的方法代表一颗路径树
// HTTP只有9个方法,所有使用数组保存
type methodTrees []methodTree

gin的路由实现似乎参考了httprouter这个项目,代码有点复杂先略过。
每个路由包含一个请求处理链Handlers HandlersChain

HandlersChain定义为处理函数切片,处理函数只接收*Context参数,所有的请求响应需要的字段、以及处理流程控制变量都包含在Context结构体中。

1
2
3
4
5
// HandlerFunc defines the handler used by gin middleware as return value.
type HandlerFunc func(*Context)

// HandlersChain defines a HandlerFunc array.
type HandlersChain []HandlerFunc

中间件

请求中间件和处理函数是同一个类型HandlerFunc
分为全局中间件和针对某个URL的中间件。Engine.Use设置全局中间件。

func (engine *Engine) Use(middleware ...HandlerFunc) IRoutes

RouterGroup.Use设置当前路由的中间件。

func (group *RouterGroup) Use(middleware ...HandlerFunc) IRoutes

gin提供了以下中间件,开发自定义中间件可以参考它们。

  • HTTP验证 BasicAuth
  • 日志 Logger
  • 捕获panic Recovery

Context

Context对象由Engine.pool分配,请求处理完又放回了池中。
主要字段包含请求、响应对象,捕获的参数,处理链,用来控制处理流程的索引,kv值存储等。
它实现了标准库context.Context接口,但是内部没有实现Deadline方法。请求的超时context在Request对象中,可以通过c.Request.Context()获取。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
type Context struct {
writermem responseWriter // HTTP writer的包装对象
Request *http.Request // 请求对象
Writer ResponseWriter // 指向writermem

Params Params // url参数
handlers HandlersChain // 处理链,最多63个处理函数
index int8 // handlers的索引,控制handlers调用。reset函数初始化为-1
fullPath string // 请求路径

engine *Engine

// This mutex protect Keys map
KeysMutex *sync.RWMutex

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

处理控制逻辑由NextAbort两个函数实现。

1
2
3
4
5
6
7
8
9
10
11
12
// Next可以在处理函数内部调用,相当于直接调用下一个处理函数,调用层级+1
func (c *Context) Next() {
c.index++ // 初始化为-1,从0开始执行
for c.index < int8(len(c.handlers)) {
c.handlers[c.index](c) // 调用当前函数
c.index++ // 移动到下一个函数
}
}
// Abort终止所有后续的函数
func (c *Context) Abort() {
c.index = abortIndex
}

其他API包括以下几类

  • 读请求参数,如Param,Query
  • 读写kv数据,如Get,Set
  • 解析请求数据,如带Bind的方法
  • 渲染数据,写响应,如JSON,Data,Render

数据输入

gin提供了binding接口来解码请求数据,接口和实现定义在binding目录下面。
为了方便,在Context上实现了许多数据格式解码的帮助函数,包括json、yaml、xml、protobuf等

最基础的是Binding接口,它定义了从request读数据并且解码的接口
BindingBody定义了从body读数据的接口
BindingUri定义了从URL读数据的接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
type Binding interface {
Name() string
Bind(*http.Request, interface{}) error
}
type BindingBody interface {
Binding
BindBody([]byte, interface{}) error
}
// BindingUri adds BindUri method to Binding. BindUri is similar with Bind,
// but it read the Params.
type BindingUri interface {
Name() string
BindUri(map[string][]string, interface{}) error
}

Context的帮助函数

1
2
func (c *Context) ShouldBindJSON(obj interface{}) error // 不设置状态码
func (c *Context) BindUri(obj interface{}) error // 错误设置400

数据输出

Context数据渲染方法

func (c *Context) Render(code int, r render.Render)

Render接口定义在render目录下,包括各种常用格式,如json、jsonp、HTML、protobuf等

1
2
3
4
5
6
type Render interface {
// Render writes data with custom ContentType.
Render(http.ResponseWriter) error
// WriteContentType writes custom ContentType.
WriteContentType(w http.ResponseWriter)
}

Context的帮助函数

1
2
func (c *Context) JSONP(code int, obj interface{})
func (c *Context) Data(code int, contentType string, data []byte)

有个小坑需要注意,Render函数内部会panic。如果渲染报错了,直接panic。
其他渲染函数都会调用它,所以都有可能panic。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Render writes the response headers and calls render.Render to render data.
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)
}
}

性能优化

  • 基数树高效组织路由
  • sync.pool分配Context
1
2
3
4
5
6
7
8
9
10
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)
}
  • 可替换的json库。
    internal/json封装了常用json函数,用// +build jsoniter来替换标准库json。

  • 字符串和slice转换优化。
    internal/bytesconv里定义了优化后的转换函数,用了黑魔法避免内存拷贝。

1
2
3
4
5
6
7
8
9
10
11
12
// StringToBytes converts string to byte slice without a memory allocation.
func StringToBytes(s string) (b []byte) {
sh := *(*reflect.StringHeader)(unsafe.Pointer(&s))
bh := (*reflect.SliceHeader)(unsafe.Pointer(&b))
bh.Data, bh.Len, bh.Cap = sh.Data, sh.Len, sh.Len
return b
}

// BytesToString converts byte slice to string without a memory allocation.
func BytesToString(b []byte) string {
return *(*string)(unsafe.Pointer(&b))
}

后记

这里只记录了gin框架的大体架构,涉及到细节的地方大多都略过了。例如HTTP请求处理逻辑、路由trees的匹配算法等,希望以后有时间可以补上。

打赏
  • 版权声明: 本博客所有文章除特别声明外,均采用 Apache License 2.0 许可协议。转载请注明出处!
  • © 2015-2024 RivenZoo
  • Powered by Hexo Theme Ayer
  • PV: UV:

请我喝杯咖啡吧~

支付宝
微信