Gin 是一个轻量级的 Go 语言 web 框架,支持中间件,同时具备快速、可扩展等特点。相较于其他 web 框架,拥有更高的性能和更快的路由。
Gin 框架的路由采用树形结构,同时支持包含参数和通配符的路由。下文将据此对 Gin 框架的路由机制展开介绍。
下文中将会用到的路由:
- /users
- /user/add
- /user/:name
- /user/:name/*action66^
⒈初始化电脑时出现问题 相关的初始化电脑的后果数据结构
Gin 采用基数树的形式存储路由信息。基数树(appleradix tree)在每个节点存储各子节点的Go公共前缀字符串,相较于前缀树(prefix tree)在每个节点只存储一个字符的方式初始化失败是怎么解决,显著的提高了空间利用率,同时也提高了查询效率。
Gin 框架在启动时首先需要实例json解析化一个引擎 gin.Engine
,Engine
是一个结构体,其中包括了框架的路由、中间件、配置等信息,Engine.trees
即存储了路由信息。
package gin
func New() *Engine {
engine := &Engine{
RouterGroup: RouterGroup{
Handlers: nil,
basePath: "/",
root: true,
},
/* ... ... */
trees: make(methodTrees, 0, 9),
/* ... ... */
}
engine.RouterGroup.engine = engine
/* ... ... */
return engine
}
func Default() *Engine {
debugPrintWARNINGDefault()
engine := New()
engine.Use(Logger(), Recovery())
return engine
}
通过调用 gin.Default()
方法创建 *Engine
实例APP,而 Default()
方法则是通过调用 New()
方法来实现该功能。通过以上代码可知approach,在实例化 *Engine
的过程中初始化,En工资超过5000怎么扣税ginjson格式e.trees
被初始化初始化电脑为一个长度为 0 容量为json 9 的切片,切片元素的类型为 methodTree
。之所以将容量设置为 9 ,是因为 HTTP请求总共支持 9 种方法,分别为 GET
、HEAD
、POST
、PUT初始化sdk什么意思
、PATCH
、DELETE
、CONNECT
、OPTIONapplicationS
、TRACE
,针对每一种请求方法所设置的路由,都会有一棵单独的初始化电脑树来存储相应的路由信初始化息。
package gin
type methodTrees []methodTree
type methodTree struct {
method string
root *node
}
type node struct {
path string
indices string
wildChild bool
nType nodeType
priority uint32
children []*node // child nodes, at most 1 :param style node at the end of the array
handlers HandlersChain
fullPath string
}
type HandlersChain []HandlerFunc
type HandlerFunc func(*Context)
路由树中的节点信息通过结构体 node
来存储,该结构体存储了相应节点对应的路由完整路径、节点类型、优先级、子节点、处理函数等信息。
Gin 框架中的路由appetite可以分为两种类型:精准路由、包含参数和通配符的路由。所谓appstore精准路由,即是指approach路由中不包含任何参数以及通配符,这种路由的优先级最高。所谓包含参数和通配符的路由,则是指路由中包含诸如 :param
或 *action
的jsonobject部分。其中,:param
为路由中的参数,且一个路由的同级子路由中只能有一个参数,例如 /user/:name
和 /user/:age
不可同时出现;*action
为路由中的通配符json格式,通配符会适配任意的同级子路由。另外,一个路由中可以包含多个参数,但这些参数不可以与通配符冲突,例如,/user/:name/:age
和 /user/:name/*action初始化磁盘
不可同时出现。
另外,Gin 框架在启动时会默认往 Engine
引擎中注册两个中间件: Logger
和 Recovery
,分别用来记录日志以及从恐慌中恢复。在将这两个中间件注册google到 Engine
中的同时,相应的处理函数也会注册到 RouterGroup
的 Handlers
字段中。
⒉ 精准路由
Gin
框架的 Engine
结构体内嵌了一个 RoujsonobjectterGroup
结构体,所以 Gin
框架在启动之后即可以通过调工商银行用 RouterGroup
的方法来添加路由。在 Gin
框架中,所有与路由相关的操作都通过调用 RouterGroup
的方法来实现。
package main
import (
"github.com/gin-gonic/gin"
)
func main () {
r := gin.Default()
/* ... ... */
r.Run(":8080")
}
jsonp Gin
框架在初始化时已经将路由的基础路径设github汤姆置为 /
,框架在启动后如果不添加任何路由,则此时 Engine.tregithub永久回家地址es
为一个长度为 0 的空的 slice
。之后,我们以 POST
方法为例添加路由。
⓵ /users
r.POST("/users", func (context *gin.Context) {
context.JSON(http.StatusOK, "users")
})
func (group *RouterGroup) POST(relativePath string, handlers ...HandlerFunc) IRoutes {
return group.handle(http.MethodPost, relativePath, handlers)
}
func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes {
absolutePath := group.calculateAbsolutePath(relativePath)
handlers = group.combineHandlers(handlers)
group.engine.addRoute(httpMethod, absolutePath, handlers)
return group.returnObj()
}
由上述代码可知,在向框架中添加初始化游戏启动器失败路由时,Go首先要计算得到该路由的绝对路径,然后计算得到作用于该路由的处理函数。/users
路由的绝对路径即为其本身,而作用于该路由的处理函数,除在定义该路由JSON时设置的处理函数之外,还包括在初始化sdk什么意思框架启动时注册的处理函数。
待路由的绝对路径和处理函数都确定之后,则着手构造路由的树状结构。
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)
/* ... ... */
}
func (trees methodTrees) get(method string) *node {
for _, tree := range trees {
if tree.method == method {
return tree.root
}
}
return nil
}
首先要做的是根据路由设置的请求方法获取对应的路由树的根节点。这里我们设置的请求方法json怎么读为 POST
,由于之工龄差一年工资差多少前我们并没有添加任何的路由,所以这里返回的根节点为空初始化失败是怎么解决的 node
,即 root == nil
。
由于根节点为空,所以我们首先需要创建根节点,即创建 node
对象。这里使用 new()
方法创建 node
对象,只是为其分配了相应的内存空间并返回指向所分配的内存空间的指针,并没有对其进行初始化,Go所以,root
此时为一个指向 node
的零值的指针。
创建完根节点之后,将根节点的 fullPath
字段设置为工龄差一年工资差多少 /
,然后向 engine.trees
中追加相应的路由树,此刻我们添加的是 POjson文件是干什么的ST
方法对应的路由树。这之后,我们将向路由树application中工龄差一年工资差多少添加 /users
路由。
func (n *node) addRoute(path string, handlers HandlersChain) {
fullPath := path
n.priority++
// Empty tree
if len(n.path) == 0 && len(n.children) == 0 {
n.insertChild(path, fullPath, handlers)
n.nType = root
return
}
/* ... ... */
}
调用 addRoute
方法添加路由,此时接收者 n
为刚创建的 root
节点,参数 path
和 handlerjsonps
为路由apple /users
的github直播平台永久回家绝对路径和处理函数。由于之前在创建 root
节点时只是分配了内存空间,并没有赋值,所以 n.path
此时为空字符串,n.children
的长度亦为 0,所以此时会直接调用 insertChild()
方法向根节点中添加子节点。
// 搜索路由中的通配符 : 和 *
func findWildcard(path string) (wildcard string, i int, valid bool) {
// Find start
for start, c := range []byte(path) {
// A wildcard starts with ':' (param) or '*' (catch-all)
if c != ':' && c != '*' {
continue
}
// Find end and check for invalid characters
valid = true
for end, c := range []byte(path[start+1:]) {
switch c {
case '/':
return path[start : start+1+end], start, valid
case ':', '*':
valid = false
}
}
return path[start:], start, valid
}
return "", -1, false
}
// 更新 node 的 children 字段
func (n *node) addChild(child *node) {
if n.wildChild && len(n.children) > 0 {
wildcardChild := n.children[len(n.children)-1]
n.children = append(n.children[:len(n.children)-1], child, wildcardChild)
} else {
n.children = append(n.children, child)
}
}
// 添加子节点
func (n *node) insertChild(path string, fullPath string, handlers HandlersChain) {
for {
// 查找通配符之前的路由前缀
wildcard, i, valid := findWildcard(path)
if i < 0 { // No wildcard found
break
}
// 通配符中不得包含 ':' 和 '*'
if !valid {
panic("only one wildcard per path segment is allowed, has: '" +
wildcard + "' in path '" + fullPath + "'")
}
// 通配符必须命名
if len(wildcard) < 2 {
panic("wildcards must be named with a non-empty name in path '" + fullPath + "'")
}
if wildcard[0] == ':' { // 路由中包含参数
if i > 0 {
// 将前缀设置为父节点的 path 并更新 path 参数值
n.path = path[:i]
path = path[i:]
}
// 创建子节点(子节点 nType 为参数类型,path 为参数,fullPath 为路由全路径),并将其加入父节点的 children
child := &node{
nType: param,
path: wildcard,
fullPath: fullPath,
}
n.addChild(child)
n.wildChild = true
n = child // 将 n 的值设置为新创建的子节点
n.priority++
// 路由参数后还有子路径
if len(wildcard) < len(path) {
path = path[len(wildcard):] // 更新路径信息
// 创建子节点
child := &node{
priority: 1,
fullPath: fullPath,
}
n.addChild(child)
n = child
continue
}
// 设置处理函数
n.handlers = handlers
return
}
// 路由中包含通配符
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]
// 创建子节点(节点 path 为空)
child := &node{
wildChild: true,
nType: catchAll,
fullPath: fullPath,
}
// 更新父节点的 children 字段
n.addChild(child)
n.indices = string('/')
n = child
n.priority++
// 创建子节点(节点 path 为通配符)
child = &node{
path: path[i:],
nType: catchAll,
handlers: handlers,
priority: 1,
fullPath: fullPath,
}
n.children = []*node{child}
return
}
// 路由中不含通配符,直接更新节点信息
n.path = path
n.handlers = handlers
n.fullPath = fullPath
}
根据代码,向根节点中添加子节点分为三种情况:
ⅰ 路由中不包含任何通配符。这种情况最json是什么意思为简单,初始化英文我们即将添加的路由 /users
就属于这种情况。由于刚创建的 root
节点为空,我们只需要直接将要添加的路由的相应信息直接更新写入 root
节点即可。更新完成后,路由树的结appstore构如下图所示:
ⅱ 路由中包含参数的情况。对于这种情况,首github官网登陆入口先需要找到路由中的参数部分以及相应的起始位置。如果参数的起始位置不是从 0 开始,那么需要将参数部分之前的部分路由设置为父节点的 path
。之后会创建新的 param
类型的节点,并将路由中的参数部分作为该节点的 path
,并且这个新创建json是什么意思的节点会被添加到父公司让员工下班发手机电量截图节点的 children
切片中。如果在参数部分之后还有子路由,那么还会创建新的节点,然后循环进行上述操作,具体流初始化sdk什么意思程将在下文中包含参数和通配符的路由部分介绍。
ⅲ 路由中包含通配符 *
的情况。对于这种情况,同样是需要先找到路由中包含通配符的部分以及相应的起始位置。需要特别注意的是,这种通配符只能出现在路由的最后,即github中文社区通配符之后不可以再有任何子路由,甚至不可以包含字符 /
。之后创建、添加节点的过程与上述参数部分的情况类似,只不过此时创建的节json数据点类型为 catchAll
,并且,由于通配符只能出现在路由的最后,所以也不会有上述参数github永久回家地址部分可google能出现的循环操作,具体流程将在approve下文中介绍。
⓶ /user/a龚俊dd
工龄差一年工资差多少 下面继续添加 /user/add
路由,其绝对路径仍然为其自身,处理函数仍然是三个:框架启动时注册的日志、恐慌恢复函龚俊数,以及路由处理函数。在添加完 /users
路由之后,此时 POST
请求路由树的根节点不再为空,所以添加 /宫颈癌user/add
的逻辑与上述添加 /users
的逻辑也不尽相同。
func (n *node) addRoute(path string, handlers HandlersChain) {
fullPath := path
n.priority++
/* ... ... */
parentFullPathIndex := 0
walk:
for {
// 找到当前将要添加的路由与节点 n 的 path 字段的公有前缀
i := longestCommonPrefix(path, n.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]
n.handlers = nil
n.wildChild = false
n.fullPath = fullPath[:parentFullPathIndex+i]
}
// 为新添加的路由创建子节点
if i < len(path) {
path = path[i:]
c := path[0]
// n 为参数类型节点,并且参数部分后还存在子路由
if n.nType == param && c == '/' && len(n.children) == 1 {
parentFullPathIndex += len(n.path)
n = n.children[0]
n.priority++
continue walk
}
// n 的子节点中存在某个子节点,其 path 与当前路由的 path 有公共前缀
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 不是通配符节点,并且即将插入的路由也不是参数或通配符
n.indices += bytesconv.BytesToString([]byte{c})
child := &node{
fullPath: fullPath,
}
n.addChild(child)
n.incrementChildPrio(len(n.indices) - 1)
n = child
} else if n.wildChild {
// 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)] &&
// 路由中的通配符部分必须处在最后的位置
n.nType != catchAll &&
// Check for longer wildcard, e.g. :name and :names
(len(n.path) >= len(path) || path[len(n.path)] == '/') {
continue walk
}
// Wildcard conflict
pathSeg := path
if n.nType != catchAll {
pathSeg = strings.SplitN(pathSeg, "/", 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
}
// 为节点添加处理函数
if n.handlers != nil {
panic("handlers are already registered for path '" + fullPath + "'")
}
n.handlers = handlers
n.fullPath = fullPath
return
}
}
在添json解析加 /user/add
之前,首先要找到即将添加的路由与根节点路径之间的公有前缀,即 /user
。由于公有前缀 /us初始化电脑时出现问题er
与根节点路径 /users
并不完全相等公积金,所以这里首github中文官网网页先需要对根节点进行拆分。
拆分之后的子节点中,path
为根节点与公有前缀json数据的差集,即 s
;优先级 priority
在根节点的基础上减一;节点类型为默认值 stjson文件是干什么的atic
;其他信息则完全继json解析承自根节点。拆分之后的根节点中,子节点和索引字段信息不再为空;path
和 fullPath
字段内容则变成了公有前缀;处理函数也公司让员工下班发手机电量截图变为空。github直播平台永久回家根节github是什么点拆分之后的路由树结构如下图所示:
根节点拆分完成之后开始处理新添加的路由 /user/add
。在去掉与根节点之github汤姆间的公有前缀之后,即将添加的路由的 path
变成了 /add
。由于根节点既不是参数类型节点,也不是通配符类型的节点,同时,其子节点当中也不存在与当前 path
有公共部分的节点;并且,当前 path
也不是以参数或通配符开头,所以,我们直接生成一个普通节点,JSON并将其作为根节点的子节点插入路由树中。在这个新生成的节点当中,除 fullPath
字段外,其他字段全部为默认值。除此之外,我们还appear需要更新根节点的 igithub永久回家地址ndices
字段。github永久回家地址
待上述操作完成之后,此时我们需要设置新增子节点的优先级,同时根据子节点的优先级将子节点按照优先级倒序排序。
func (n *node) incrementChildPrio(pos int) int {
cs := n.children
// 首先将对应子节点的优先级加 1
cs[pos].priority++
prio := cs[pos].priority
newPos := pos
for ; newPos > 0 && cs[newPos-1].priority < prio; newPos-- {
// 将优先级低于对应子节点但位置处于对应子节点之前的节点移动到新增子节点之后
cs[newPos-1], cs[newPos] = cs[newPos], cs[newPos-1]
}
// 根据新的子节点的顺序重新排列 indices 字段中的字符顺序
if newPos != pos {
n.indices = n.indices[:newPos] + // Unchanged prefix, might be empty
n.indices[pos:pos+1] + // The index char we move
n.indices[newPos:pos] + n.indices[pos+1:] // Rest without char at 'pos'
}
return newPos
}
根据代码,新增子节点在插入父节点的 children
切片时,总是处于末尾的位置(除非子节点切片github开放私库中有参数类型或通配符类型的节点)。此时,我们再将新增子节点的优先级设为 1,然后按照 children
切片中子节点的优先级倒序重新排列子节点的顺序,之工龄越长退休金越多吗后再按照新的application子节点的顺序重新排列 indices
字段中的字符顺序。
就当前添加的子节点json怎么读而言,其优先级为 1JSON,而之前从根节点拆分出来的子节点的优先级亦为 1,所以子节点的顺序不会发生变化,相应的,indices
字段中的字符顺序亦不会发生变化。
在上述操作approach完成之后,新增节点除 fullPath
和 priority
字段之外,其他字段全部为默认值。此时,我们将参数 n
的值github中文社区更新为新增节点,然后调用 insertChild
进行后续的操作。此时,在 insertChjson数据ild
的参数列表中,appearancepath
的值为 /a公司让员工下班发手机电量截图dd
,其他参数json是什么意思没有发生变化。由于 /add
中既没有参数部分,也不包含通配符部分,所以,只需要直接更新json解析新增节点的 path
、handlers
、fullPajson数据th
。这些操作都json完成之后,路由 /user/add
的添加操作完成。此时,路由树的结构变成如下图所示:
以上就是 Gin 框架中精准路由的路由树结构构建过程appointment。由于精准路由中不存在参数或通配符,所以整个构建过程相对比较简单,简而言之,就是节点拆分以及子节点的添加。另外,由于两个路由中都有共同的前缀 /user
,所以路由树根节点的 p初始化电脑ath
为 /user
,如果是两个完全不同的路由,则路由树的根节点的 path工资超过5000怎么扣税
为 /
。
⒊ 包含参数和通配符的路由
介绍完了精准路由的树结构构建过程,接着来看包含参数以及通配符的路由如何添加到路由树中。
⓵ /user/:name
继续向路由树中添加包含参数的路由,仍然是先找到路由树的根节点,路由前缀 /user
与路由树根节点的 path
一致,所以根节点不需要再做拆分,只需要直接向根节点中插入子节点。
当前路由中,除去公有前缀,剩余部分为 /:name
,其首字符存在于根节点的 indJSONices
中,亦即根节点中存在某个子节点,其 pathjson是什么意思
字段与当前路由的剩余部分有公共前缀。根据当前路由剩余部分的首字符在根节点 i工龄差一年工资差多少ndices
字段中的索引位置,找到相应的子节点,然后将该子节点的approach priority
字json数据段值加一,然后根据修改后的 priority
重排子节点的顺序。待这些操作完成之后,path
字段值为 /add
的子节点排在了子节点列表的首位,同时该子节点的 priority
值变为 2 。
上述操作完成之后,重新开始添加路由的逻辑,由于 /:name
与 /add
存在公有前缀 /
,所以 /add
子节点需要进行拆分。拆分完成之后,/
成json文件是干什么的为新的父节点的 path
,而 add
成为新github开放私库的子节点的 path
。
当前路由在初始化是什么意思去除appearance公有前缀之后,剩余部分变成了 :name
,这是一个参数部分github开放私库,所以我们将调用 insertChild()
方法进行后续的操作:首先创建新的参数类型的子节点Go,然后将创建好的子节点挂到新的父节点之下。待操作完成之后,路由树的结构如下图:
一个父节点的所有子节点中,只允许有一个参数类型的子节点,并且这个参数类型的子节点只能位于整个子节点切片的最后json数据
⓶ /user/:name/*acgithub开放私库tion
继续添加包初始化电脑时出现问题含通配符的路由,在当初始化电脑时出现问题未进行更改前路由中,/user/:name
在已经构建好的路由树中都能找到相对应的节点,所以这里不再赘述github中文官网网页此过程。但需要注意的是,在查找节点的过程中,所经过的每一工龄差一年工资差多少个节点,其优先级都要加一。
在到达路由树的 :name
节点后,当前路由的剩余部分变json格式成了 /*action
,此时继续构建路由树。根据 addRoute
方法的逻辑,:name
节点此时会新增一个子节点,该子节点的优先级被设置为 1,fullPath
则为当前路由的完整路appstore径,同时该 :name
节点的 indices
的值会被approve设置为 /
。
随后,这个新增的子节点会调用 insertCjsonobjecthild
方法继续构建路apple由树,将当前路由的剩余部分 /*action
添加到路由树中。根据代码逻辑,首先会从当前路由的剩余部分中appointment找到完整的json格式怎么打开通配符部分以及其起始位置。这之后会进行一些必要的校验,待这些校验通过之后,接着就会创建新的子application节点继续构建路由树。首先,刚新增的子节点,其 path
会被设置为空,indices
则会被设置为 /
,同时还会新增一个类型为 catchAll
的子节点,这个子节点的 wildChild
为 true
,fullPath
为当前路由的完整路径,优先级为 1。其次,又有一个类型为 catchAll
的子节点被创建,这个新创建的子节点则会存储当前路由的完整信息,包括路径、处理函数等。至此github开放私库,整个路由树构建完成,完整的路由树结构如下图所示: