官方简介
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有助于大家理解这快的插入逻辑
总结
gin服务启动前的工作,主要也就下面几块:
- 创建实例
- 添加中间件
- 创建路由组添加路由
- Engine和RounterGroup的结构设计非常巧妙,值得花时间思考一下。
- 路由树的数据结构选型也是非常考究的,Radix tree的思想本身在保证时间效率的基础上,也实现了空间的优化。
下一期会介绍服务启动的逻辑。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。