2

官方简介

Gin is a web framework written in Go (Golang).
It features a martini-like API with performance that is up to 40 times faster thanks to httprouter.
If you need performance and good productivity, you will love Gin.

一些核心的结构

* Engine        gin实例结构
* HandlerFunc   gin中间件函数类型
* RouterGroup   路由组结构
* methodTree    路由树结构
* node          路由树的节点

源码学习 (gin版本:v1.7.0)

Engine结构

源码:

type Engine struct {
    // 内嵌的匿名路由组结构
    RouterGroup

    // 结尾斜线重定向开关
    // eg: 请求'/foo/',但是服务端只有'/foo',那么开关打开后就会重定向到该路由
    RedirectTrailingSlash bool

    // 路径修复开关
    // 如果请求路径匹配不成功,并且开关打开,
    // 首先尝试删除请求路径中的'../' 和 '//',
    // 然后再进行一次不区分大小写的匹配,
    // eg: /FOO 和 /..//Foo 会匹配到 /foo
    RedirectFixedPath bool

    // 如果该值为true,并且路由未匹配成功,
    // 那么会去其他方法的路由树中去匹配,
    // 如果匹配成功,会返回 “Method Not Allowed”,http状态码 405
    // 如果未匹配成功,该请求会扔给NotFound handler 处理
    HandleMethodNotAllowed bool

    // 是否使用客户端的IP进行转发
    ForwardedByClientIP    bool
    // 如果为true,则会推送一些以'X-AppEngine...'开始的请求头
    AppEngine bool
    // 如果为true,将使用url.RawPath查找参数
    UseRawPath bool
    // 如果为true,url.Paht将不会被转义
    UnescapePathValues bool
    // http.Request的ParseMultipartForm方法中 maxMemory参数的值
    MaxMultipartMemory int64

    // 分隔符
    delims           render.Delims
    secureJsonPrefix string
    HTMLRender       render.HTMLRender
    FuncMap          template.FuncMap

    // 不同情况的处理函数链
    allNoRoute       HandlersChain
    allNoMethod      HandlersChain
    noRoute          HandlersChain
    noMethod         HandlersChain

    // context对象池
    pool             sync.Pool
    // 路由树
    trees            methodTrees
}
主要是对gin实例的一些基础配置,比较核心的是RounterGroup,pool和trees,下边会介绍到。

看一下引擎实例的创建代码,也非常简单:

func New() *Engine {
    // 打印信息
    debugPrintWARNINGNew()
    // 初始化实例,返回指针类型
    engine := &Engine{
        // root路由组
        RouterGroup: RouterGroup{
            Handlers: nil,
            basePath: "/",
            root:     true,
        },
        FuncMap:                template.FuncMap{},
        RedirectTrailingSlash:  true,
        RedirectFixedPath:      false,
        HandleMethodNotAllowed: false,
        ForwardedByClientIP:    true,
        AppEngine:              defaultAppEngine,
        UseRawPath:             false,
        UnescapePathValues:     true,
        MaxMultipartMemory:     defaultMultipartMemory,
        // 路由树,一个方法对应一颗树
        trees:                  make(methodTrees, 0, 9),
        delims:                 render.Delims{Left: "{{", Right: "}}"},
        secureJsonPrefix:       "while(1);",
    }
    engine.RouterGroup.engine = engine
    // 定义对象池的New方法
    engine.pool.New = func() interface{} {
        return engine.allocateContext()
    }
    return engine
}

RouterGroup结构

源码:

type RouterGroup struct {
    // 路由组里所有路由公共的处理函数链
    Handlers HandlersChain
    // 路由组的公共路径
    basePath string
    // gin实例指针,指向程序创建的gin实例
    engine   *Engine
    // 是否时root路由组
    root     bool
}


// 看一下HandlersChain和HandlerFunc的定义
type HandlerFunc func(*Context)
type HandlersChain []HandlerFunc
路由组极大的方便了我们根据业务进行路由拆分,
同一个路由组拥有公共的中间处理函数和公共路径前缀,
不同的路由组之间则可以根据实际业务实现差异化。

看一下路由组的创建源码,非常简单:

func (group *RouterGroup) Group(relativePath string, handlers ...HandlerFunc) *RouterGroup {
    return &RouterGroup{
        // 合并处理函数链,其实就是个数组,上边有定义
        Handlers: group.combineHandlers(handlers),
        basePath: group.calculateAbsolutePath(relativePath),
        // 看,所有的路由组都指向了同一个gin实例,非常巧妙的抽象设计
        engine:   group.engine,
    }
}

在看一下添加中间件和路由的方法:

// 通过RouterGroup的方法添加中间件
// 返回IRoutes接口类型,方便后续给路由组添加路由
func (group *RouterGroup) Use(middleware ...HandlerFunc) IRoutes {
    // 就是往数组后边append
    group.Handlers = append(group.Handlers, middleware...)
    return group.returnObj()
}

// 通过Engine的方法添加全局中间件,这里加入的中间件是所有路由共有的
func (engine *Engine) Use(middleware ...HandlerFunc) IRoutes {
    engine.RouterGroup.Use(middleware...)
    engine.rebuild404Handlers()
    engine.rebuild405Handlers()
    return engine
}

// 添加路由,以GET方法为例,这里就会往对应的路由树中添加路由,下文会介绍到
func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes {
    return group.handle("GET", relativePath, handlers)
}

methodTree结构

源码:

type methodTree struct {
    // 方法名:GET,POST,PUT,DELETE,PATCH,HEAD,OPTIONS,CONNECT,TRACE
    method string
    // 树节点
    root   *node
}

type node struct {
    // 当前节点的相对路径
    path      string
    // 所有孩子节点的path[0]组成的字符串,方便查找
    indices   string
    // 所有的孩子节点
    children  []*node
    // 当前节点的处理函数链
    handlers  HandlersChain
    // 当前节点的路由数量
    priority  uint32
    // 节点类型
    nType     nodeType
    // 孩子节点是否包含通配符
    wildChild bool
    // 完整路径
    fullPath  string
}

// 路由树数组
type methodTrees []methodTree
gin的路由设计是一大亮点,每一种HTTP的请求方法对应一颗路由树,
每棵路由树的实现采用了Radix tree,使得路由查找非常的高效。

来看看路由树的插入过程:

func (engine *Engine) addRoute(method, path string, handlers HandlersChain) {
    assert1(path[0] == '/', "path must begin with '/'")
    assert1(method != "", "HTTP method can not be empty")
    assert1(len(handlers) > 0, "there must be at least one handler")

    debugPrintRoute(method, path, handlers)
    
    // 通过请求的方法获取对应的路由树
    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)

    // Update maxParams
    if paramsCount := countParams(path); paramsCount > engine.maxParams {
        engine.maxParams = paramsCount
    }
}

func (n *node) addRoute(path string, handlers HandlersChain) {
    fullPath := path
    // 节点路由数量加一(因为每次调用,肯定有一个路由路过这个节点)
    n.priority++
    // 计算路径中包含的参数个数
    numParams := countParams(path)

    // Empty tree
    if len(n.path) == 0 && len(n.children) == 0 {
        n.insertChild(path, fullPath, handlers)
        n.nType = root
        return
    }

    parentFullPathIndex := 0

walk:
    for {
        // Find the longest common prefix.
        // This also implies that the common prefix contains no ':' or '*'
        // since the existing key can't contain those chars.
        // 将新加入的路径跟当前节点路径进行比较,寻找最长公共前缀
        i := longestCommonPrefix(path, n.path)


        // 这种情况就需要拆分当前节点了,因为存在更短的公共前缀
        if i < len(n.path) {
            // 将当前节点的差异部分拆分出来,放到一个新节点中
            child := node{
                // 当前节点的差异部分拆分出来
                path:      n.path[i:],
                wildChild: n.wildChild,
                indices:   n.indices,
                // 转移孩子信息
                children:  n.children,
                handlers:  n.handlers,
                // 当前节点路由数减一,因为函数开头加一了
                priority:  n.priority - 1,
                fullPath:  n.fullPath,
            }

           
            // 更新当前节点的孩子节点,指向新创建的孩子节点
            n.children = []*node{&child}
            // 当前节点的孩子变成了一个,就是上面分割出来的孩子
            n.indices = bytesconv.BytesToString([]byte{n.path[i]})
            // 更新路径,因为进行了拆分
            n.path = path[:i]
            // 中间处理函数置为nil
            n.handlers = nil
            n.wildChild = false
            // 更新当前节点的完整路径,因为拆出去了一部分
            n.fullPath = fullPath[:parentFullPathIndex+i]
        }
        // 新加入的路径的非公共部分也需要处理,插入当前节点的孩子中
        if i < len(path) {
            // 更新路径,取差异部分
            path = path[i:]
            c := path[0]

            // 如果当前是参数节点,且只有一个孩子,且 path的剩余部分是以/开头的
            if n.nType == param && c == '/' && len(n.children) == 1 {
                parentFullPathIndex += len(n.path)
                   // 更新n的指向,指向孩子节点
                n = n.children[0]
                // 路由数自增
                n.priority++
                // 返回继续匹配
                continue walk
            }

            // 从所有孩子中查找是否有首字母匹配的孩子,如果找到了,那么继续向下查找
            for i, max := 0, len(n.indices); i < max; i++ {
                if c == n.indices[i] {
                    parentFullPathIndex += len(n.path)
                    i = n.incrementChildPrio(i)
                    n = n.children[i]
                    continue walk
                }
            }

            // 走到这里的话,那就插入节点吧
            if c != ':' && c != '*' && n.nType != catchAll {
                // 更新当前节点的孩子首字母集合
                n.indices += string([]byte{c})
                // 创建一个新节点
                child := &node{
                    fullPath: fullPath,
                }
                // 插入到孩子集合中,这个函数保证具有通配符的孩子永远处于最后一个孩子的位置
                n.addChild(child)
                n.incrementChildPrio(len(n.indices) - 1)
                // 指向新创建的节点
                n = child
            } else if n.wildChild { // 如果孩子节点有通配符
                // inserting a wildcard node, need to check if it conflicts with the existing wildcard
                // 通配符孩子处于最后一个位置,n指向孩子
                n = n.children[len(n.children)-1]
                // 路由数加一
                n.priority++

                // Check if the wildcard matches
                if len(path) >= len(n.path) && n.path == path[:len(n.path)] &&
                    // Adding a child to a catchAll is not possible
                    n.nType != catchAll &&
                    // Check for longer wildcard, e.g. :name and :names
                    (len(n.path) >= len(path) || path[len(n.path)] == '/') {
                    continue walk
                }

                // 走到这里,表示插入失败了
                pathSeg := path
                if n.nType != catchAll {
                    pathSeg = strings.SplitN(path, "/", 2)[0]
                }
                prefix := fullPath[:strings.Index(fullPath, pathSeg)] + n.path
                panic("'" + pathSeg +
                    "' in new path '" + fullPath +
                    "' conflicts with existing wildcard '" + n.path +
                    "' in existing prefix '" + prefix +
                    "'")
            }
            // 插入孩子
            n.insertChild(path, fullPath, handlers)
            return
        }
        // Otherwise add handle to current node
        if n.handlers != nil {
            panic("handlers are already registered for path '" + fullPath + "'")
        }
        n.handlers = handlers
        n.fullPath = fullPath
        return
    }
}

func (n *node) insertChild(path string, fullPath string, handlers HandlersChain) {
    for {
        // Find prefix until first wildcard
        wildcard, i, valid := findWildcard(path)
        if i < 0 { // No wildcard found,如果没有通配符,跳出循环,直接更新当前节点的值
            break
        }

        // The wildcard name must not contain ':' and '*'
        // 非法路径,报错
        if !valid {
            panic("only one wildcard per path segment is allowed, has: '" +
                wildcard + "' in path '" + fullPath + "'")
        }

        // check if the wildcard has a name
        if len(wildcard) < 2 {
            panic("wildcards must be named with a non-empty name in path '" + fullPath + "'")
        }

        if wildcard[0] == ':' { // param 参数类型
            if i > 0 {
                // Insert prefix before the current wildcard
                n.path = path[:i]
                path = path[i:]
            }

            // 创建一个孩子节点,参数类型
            child := &node{
                nType:    param,
                path:     wildcard,
                fullPath: fullPath,
            }
            n.addChild(child)
            n.wildChild = true
            n = child
            n.priority++

            // if the path doesn't end with the wildcard, then there
            // will be another non-wildcard subpath starting with '/'
            // 如果通配符后边还有路径
            if len(wildcard) < len(path) {
                path = path[len(wildcard):]

                child := &node{
                    priority: 1,
                    fullPath: fullPath,
                }
                n.addChild(child)
                n = child
                continue
            }

            // Otherwise we're done. Insert the handle in the new leaf
            // 否则直接结束
            n.handlers = handlers
            return
        }

        // catchAll 能走下来的话,只能是一种情况:*通配符,匹配所有路径
        if i+len(wildcard) != len(path) {
            panic("catch-all routes are only allowed at the end of the path in path '" + fullPath + "'")
        }

        if len(n.path) > 0 && n.path[len(n.path)-1] == '/' {
            panic("catch-all conflicts with existing handle for the path segment root in path '" + fullPath + "'")
        }

        // currently fixed width 1 for '/'
        i--
        if path[i] != '/' {
            panic("no / before catch-all in path '" + fullPath + "'")
        }

        n.path = path[:i]

        // First node: catchAll node with empty path
        child := &node{
            wildChild: true,
            nType:     catchAll,
            fullPath:  fullPath,
        }

        n.addChild(child)
        n.indices = string('/')
        n = child
        n.priority++

        // second node: node holding the variable
        child = &node{
            path:     path[i:],
            nType:    catchAll,
            handlers: handlers,
            priority: 1,
            fullPath: fullPath,
        }
        n.children = []*node{child}

        return
    }

    // If no wildcard was found, simply insert the path and handle
    n.path = path
    n.handlers = handlers
    n.fullPath = fullPath
}
路由树其实采用了Radix tree的思想,先去了解下Radix tree有助于大家理解这快的插入逻辑

总结

  1. gin服务启动前的工作,主要也就下面几块:

    • 创建实例
    • 添加中间件
    • 创建路由组添加路由
  2. Engine和RounterGroup的结构设计非常巧妙,值得花时间思考一下。
  3. 路由树的数据结构选型也是非常考究的,Radix tree的思想本身在保证时间效率的基础上,也实现了空间的优化。

下一期会介绍服务启动的逻辑。


satan
16 声望1 粉丝

深呼吸,然后静下来。