`
January 21, 2020 本文阅读量

Gin源码简要分析

简单介绍gin源码,主要是路由和中间件的相关绑定使用流程,以及Context设计,但是不包括render

概述

通过日常对gin场景出发,深入源码,总结介绍gin的核心设计。包含:Engine / HandlerFunc / RouterGroup(Router) / Context。在日常使用中常见的就以上概念,汇总如下:

概念 解释 应用意义
Engine 引擎 web server的基础支持,也是服务的入口 和 根级数据结构
RouterGroup(Router) 路由 用于支持gin,REST路由绑定和路由匹配的基础,源于radix-tree数据结构支持
HandlerFunc 处理函数 逻辑处理器和中间件实现的函数签名
Context 上下文 封装了请求和响应的操作,为HandlerFunc的定义和中间件模式提供支持

从DEMO开始

type barForm struct {
    Foo string  `form:"foo" binding:"required"`
    Bar int     `form:"bar" binding:"required"`
}

func (fooHdl FooHdl) Bar(c *gin.Context) {
    var bform = new(barForm)
    if err := c.ShouldBind(bform); err != nil {
        // true: parse form error
        return
    }

    // handle biz logic and generate response structure
    // c (gin.Context) methods could called to support process-controling

    c.JSON(http.StatusOK, resp)
    // c.String() alse repsonse to client
}

// mountRouters .
func mountRouters(engi *gin.Engine) {
    // use middlewares
    engi.Use(gin.Logger())
    engi.Use(gin.Recovery())
    
    // mount routers
    group := engi.Group("/v1")
    {
        fooHdl := demohtp.New()
        group.GET("/foo", fooHdl.Bar)
        group.GET("/echo", fooHdl.Echo)
        // subGroup := group.Group("/subg")
        // subGroup.GET("/hdl1", fooHdl.SubGroupHdl1) // 最终路由:"targetURI = /v1/subg/hdl1"
    }
}

func main() {
    engi := gin.New()

    mountRouters(engi)

    if err := engi.Run(":8080"); err != nil {
        log.Fatalf("engi exit with err=%v", err)
    }
}

通过上述的代码就简单开启了一个gin server,其中就包括了常见的:路由注册,中间件注册,路由分组,服务启动。核心概念也就是刚刚在上文提到的那四个概念。概览流程如下图:

可以参照DEMO中的方法名在图中检索改流程。

Engine

// Engine is the framework's instance, it contains the muxer, middleware and configuration settings.
type Engine struct {
    // 路由管理
    RouterGroup

    // 省略非关心属性
    // ...

    // context poo,支持context复用,减少对象创建提高性能。
    pool             sync.Pool
    
    // []methodTree方法树根节点集合 这部分在路由部分会详细介绍
    trees            methodTrees 
}

Engine是根入口,它把RouterGroup结构图体嵌入自身,以获得了Group,GET,POST等路由管理方法。从 Run方法的代码:

因为直接复制代码太多了,推荐下载源码来跳转,更便捷高效。

func (engine *Engine) Run(addr ...string) (err error) {
    defer func() { debugPrintError(err) }()

    address := resolveAddress(addr)
    debugPrint("Listening and serving HTTP on %s\n", address)
    // 使用的是标准库的http服务启动方法
    // 如果看过http.ListenAndServe函数签名,就知道engine实现了http.Handler方法,
    // 也就是它一定有一个ServeHTTP方法
    err = http.ListenAndServe(address, engine)
    return
}

通过层层跳转我们可以找到图中淡紫色部分中的handlerHTTPRequest方法,在ServeHTTP函数时,engine会从pool中取得一个Context对象,并准备好Request和ResponseWriter的相关数据设置到Context中去,提供给方法链调用。

// 调用中间件和请求逻辑函数
func (engine *Engine) handleHTTPRequest(c *Context) {
    httpMethod := c.Request.Method
    rPath := c.Request.URL.Path
    unescape := false
    if engine.UseRawPath && len(c.Request.URL.RawPath) > 0 {
        rPath = c.Request.URL.RawPath
        unescape = engine.UnescapePathValues
    }

    if engine.RemoveExtraSlash {
        rPath = cleanPath(rPath)
    }

    // 这里必须知道gin使用的是 “基数树(Radix Tree)” 用于存储路由
    // https://michaelyou.github.io/2018/02/10/%E8%B7%AF%E7%94%B1%E6%9F%A5%E6%89%BE%E4%B9%8BRadix-Tree/
    t := engine.trees
    for i, tl := 0, len(t); i < tl; i++ {
        if t[i].method != httpMethod {
            // 从这里可以知道,engine中的methodTrees []methodTree是请求方法分组的路由匹配树。
            continue
        }
        root := t[i].root
        // root数据结构(radix-tree)?如何查找?
        value := root.getValue(rPath, c.Params, unescape) // 找到路由对应的处理函数
        if value.handlers != nil {
            c.handlers = value.handlers // 复制节点的所有handlers
            c.Params = value.params     // 路由参数
            c.fullPath = value.fullPath // 全路径
            c.Next()                    // 依次调用HandlerChain中的函数
            c.writermem.WriteHeaderNow()
            return
        }
        // 省略,这部分流程控制也自行阅读
    }
    
    // 省略...
}

从上述流程我们可以总结得到图中紫色部分调用链:

engine.Run -> http.ListenAndServe -> engine.handleHTTPRequest -> c.Next

但是其中还有不清楚的问题:

  • 路由是通过方法确定到该方法树的根节点,再查询到路由对应的节点,那么在radix-tree中是如何进行路由匹配的呢?
  • c.handlers 直接从匹配到的路由节点的handlers复制得到,那么中间件是如何挂在到其中的呢?
  • 获取到c.handlers之后,只调用了一次c.Next,那么该链路是如何继续调用下去的?
  • Path Param是如何获取到?root.getValue

RouterGroup & MethodTree

这部分是除了Context概念之外理解gin的第二核心部分,提供路由注册,调用链路函数链处理,路由分组,路由匹配功能。在开始之前,必须知道gin的路由是由httprouter提供,而httprouter包是使用了radix-tree来实现路由管理功能。

// IRouter defines all router handle interface includes single and group router.
type IRouter interface {
	IRoutes
	Group(string, ...HandlerFunc) *RouterGroup
}

// IRoutes defines all router handle interface.
type IRoutes interface {
	Use(...HandlerFunc) IRoutes

	Handle(string, string, ...HandlerFunc) IRoutes
	Any(string, ...HandlerFunc) IRoutes
	GET(string, ...HandlerFunc) IRoutes
	POST(string, ...HandlerFunc) IRoutes
	DELETE(string, ...HandlerFunc) IRoutes
	PATCH(string, ...HandlerFunc) IRoutes
	PUT(string, ...HandlerFunc) IRoutes
	OPTIONS(string, ...HandlerFunc) IRoutes
	HEAD(string, ...HandlerFunc) IRoutes

	StaticFile(string, string) IRoutes
	Static(string, string) IRoutes
	StaticFS(string, http.FileSystem) IRoutes
}

// RouterGroup is used internally to configure router, a RouterGroup is associated with
// a prefix and an array of handlers (middleware).
type RouterGroup struct {
	Handlers HandlersChain // 中间件
	basePath string        // 路由前缀
	engine   *Engine       // 指向engine入口的指针
	root     bool          // 是否是root
}

.路由注册

通过任意一个路由注册方法,都可以进入到 func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes 这个方法。

// 挂载路由的实际处理函数
func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes {
	// 计算最终的绝对路径 base + relativePath
	// 注意base也是绝对路径
	absolutePath := group.calculateAbsolutePath(relativePath)
	// 合并处理函数,将中间件和逻辑函数结合在一起
	// 一般来说这里传入的handlder是逻辑函数 len(handlers) = 1
	// 只有少数的handler会有自己的中间件处理函数 len(handlers) > 1
	handlers = group.combineHandlers(handlers)
	// 将处理好的 HandlersChain 加载到Radix Tree中去
	// 这也表明,这里的RouterGroup只会载处理路由时发挥作用
	group.engine.addRoute(httpMethod, absolutePath, handlers)
	return group.returnObj()
}

engine在初始化的时候会设置RouterGroup.basePath = “/” engine是将相同请求方法挂载到同一棵树下

看到这里回到engine.addRoute中去,通过深入发现如下调用链:

engine.GET -> routergroup.GET -> routergroup.handle -> engine.addRoute -> methodTree.addRoute -> node(radix-tree’s node).insertChild

.路由分组

RouterGroup通过Group函数来衍生下一级别的RouterGroup,会拷贝父级RouterGroup的中间件,重新计算basePath。

func (group *RouterGroup) Group(relativePath string, handlers ...HandlerFunc) *RouterGroup {
	return &RouterGroup{
		Handlers: group.combineHandlers(handlers),           // 初始化时,拷贝父亲节点的中间件
		basePath: group.calculateAbsolutePath(relativePath), // 初始化时,计算孩子group的绝对路径
		engine:   group.engine,
	}
}

.中间件挂载

有两个地方提供给中间件挂载,一个是RouterGroup.Use集中挂载中间件;另一个地方是挂载特定路由的时候(RouterGroup.GET / POST 等等),将中间件和逻辑处理函数一起挂载。第二种方式在路由注册的时候已经见过了,这里再说下Use方法,RouterGroup.Handlers = append(RouterGroup.Handlers, middlewareHandlers),这里只是简单的将后来的中间件复制到RouterGroup的中间件上,没有其他的逻辑。

.路由匹配

这部分全都是交由radix-tree来负责的部分。

HandlerFunc

type HandlerFunc func(*Context)  // 处理函数签名  
type HandlersChain []HandlerFunc // 处理函数链

这部分没什么好说的,就是定义了函数签名,指定Context作为上下文传递的关键数据。

Context

Context是上下文传递的核心,它包括了请求处理,响应处理,表单解析等重要工作。

// Context is the most important part of gin. It allows us to pass variables between middleware,
// manage the flow, validate the JSON of a request and render a JSON response for example.
type Context struct {
    writermem responseWriter // 实现了http.ResponseWriter 和 gin.ResponseWriter
    Request   *http.Request  // http.Request, 暴露给handler
    // gin.ResponseWriter 包含了:
    // http.ResponseWriter,http.Hijacker,http.Flusher,http.CloseNotifier和额外方法
    // 暴露给handler,是writermm的复制
    Writer    ResponseWriter 

    Params   Params        // 路径参数 
    handlers HandlersChain // 调用链
    index    int8          // 当前handler的索引
    fullPath string

    engine *Engine // 对engine的引用
    Keys map[string]interface{} // c.GET / c.SET 的支持,常用于session传递。

    // 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

    // query 参数缓存
    queryCache url.Values
    // 表单参数缓存, 跟queryCache作用类似
    formCache url.Values
}

.调用链流转和控制

从Engine部分已经知道,当路由被匹配到之后会执行一次c.Next,Next逻辑非常简单如下:

func (c *Context) Next() {
    // 从c.reset 可以知道 c.index == -1
    c.index++
    for c.index < int8(len(c.handlers)) {
        c.handlers[c.index](c) // 发生调用
        c.index++
    }
}

// 63,这意味这如果最大链路超过了63,那么通过index的流程控制就出问题了。
const abortIndex int8 = math.MaxInt8 / 2 

func (c *Context) Abort() {
    c.index = abortIndex
}

要注意的是,当你在调用链中的某一个handler中调用了c.Abort之类的函数,调用链会直接退出也是通过c.index来控制的。除了调用链流转,在Context这一部分还比较重要的是,参数的解析,响应处理。

.参数解析

参数传递一共有一下几种方式:

序号 参数类型 解释 Context支持
1 path param 在URI中将参数作为路径的一部分 c.Param(“key”) string
2 query param 在URI中以"?“开始的,“key=value"形式的部分 c.Query(“key”) string
3 body [form; json; xml等等] 根据请求头Content-Type判定或指定 c.Bind类似函数
// Content-Type MIME of the most common data formats.
const (
    MIMEJSON              = "application/json"
    MIMEHTML              = "text/html"
    MIMEXML               = "application/xml"
    MIMEXML2              = "text/xml"
    MIMEPlain             = "text/plain"
    MIMEPOSTForm          = "application/x-www-form-urlencoded"
    MIMEMultipartPOSTForm = "multipart/form-data"
    MIMEPROTOBUF          = "application/x-protobuf"
    MIMEMSGPACK           = "application/x-msgpack"
    MIMEMSGPACK2          = "application/msgpack"
    MIMEYAML              = "application/x-yaml"
)

关于第三种参数解析是最常用的,也是最复杂的部分包含的内容比较多,这里就不展开了。别忘了,binding的同时还有参数校验是基于“github.com/go-playground/validator”实现的。

.响应处理

常用的响应方法有c.String(http.Status, string), c.JSON(http.Status, interface{})

c.Render(code http.Status, r gin.Render) -> r.Render

// Render interface is to be implemented by JSON, XML, HTML, YAML and so on.
type Render interface {
	// Render writes data with custom ContentType.
	Render(http.ResponseWriter) error
	// WriteContentType writes custom ContentType.
	WriteContentType(w http.ResponseWriter)
}

这里以gin.render.JSON为例:

这里json包并不是直接使用的标准库json,而是经过了一层包装用于支持jsoniter替换json。 这一支持是基于golang选择性编译实现的。

// Render (JSON) writes data with custom ContentType.
func (r JSON) Render(w http.ResponseWriter) (err error) {
	if err = WriteJSON(w, r.Data); err != nil {
		panic(err)
	}
	return
}

// WriteContentType (JSON) writes JSON ContentType.
func (r JSON) WriteContentType(w http.ResponseWriter) {
	writeContentType(w, jsonContentType)
}

// WriteJSON marshals the given interface object and writes it with custom ContentType.
func WriteJSON(w http.ResponseWriter, obj interface{}) error {
	writeContentType(w, jsonContentType)
	encoder := json.NewEncoder(w)
	err := encoder.Encode(&obj)
	return err
}

总结

这里知识讲了把gin作为API Server开发时常用到的主要流程和数据结构,其中更多的流程控制部分被省略了,可以自行结合代码和源码阅读。这里主要解析了gin中函数调用链路和handlers流程控制,RadixTree的部分都被省略了,算法菜鸡不敢乱说,掌握后再结合gin的源码重读。

文中代码偏多,最好是结合源码和图片食用。

水平有限,如有错误,欢迎勘误指正🙏。

参考文献