4

Gin web框架

 2 years ago
source link: http://rivenzoo.github.io/2020/05/06/Gin-web%E6%A1%86%E6%9E%B6/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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

Engine

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

// 标准库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用于数据渲染。

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实现的。

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

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

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类型定义

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

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

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

// 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()获取。

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两个函数实现。

// 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读数据的接口

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的帮助函数

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等

type Render interface {
// Render writes data with custom ContentType.
Render(http.ResponseWriter) error
// WriteContentType writes custom ContentType.
WriteContentType(w http.ResponseWriter)
}

Context的帮助函数

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

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

// 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
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里定义了优化后的转换函数,用了黑魔法避免内存拷贝。

// 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的匹配算法等,希望以后有时间可以补上。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK