gin的路由
在前面的《gin是如何运行的》,我们讲解了简单的路由注册的一个过程。现在我们将详细的讲解路由注册的全过程,请系好安全带!马上开车!!!✈✈✈
前言
gin利用的前缀树方式注册路由。
HTTP请求的路径恰好是由/分隔的多段构成的,因此,每一段可以作为前缀树的一个节点。我们通过树结构查询,如果中间某一层的节点都不满足条件,那么就说明没有匹配到的路由,查询结束。
package main
import (
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
r.GET("/ping", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "pong",
})
})
r.GET("/ping/hello", func(c *gin.Context) {
c.JSON(200,gin.H{
"message": "hello",
})
})
r.GET("/about", func(c *gin.Context) {
c.JSON(200,gin.H{
"message": "about",
})
})
r.GET("/about/*work", func(c *gin.Context) {
c.JSON(200,gin.H{
"work":c.Param("work"),
})
})
r.GET("/:lang/intro", func(c *gin.Context) {
c.JSON(200,gin.H{
"lang":c.Param("lang"),
})
})
r.GET("/:lang/doc", func(c *gin.Context) {
c.JSON(200,gin.H{
"lang":c.Param("lang"),
})
})
err := r.Run()
if err != nil {
return
} // 监听并在 0.0.0.0:8080 上启动服务
}
我会根据上面的函数,一起Debug看看,gin是如何注册一个路由树,以及如何匹配的。
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)
//打印相关信息
//根据请求方式去构建了一个method树 ,都放在一样的请求方式的路由
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
}
}
上次我们在这里打了断点,了解到了gin注册一个GET函数的一具体过程。现在我们进入一探究竟,gin到底怎么构建一个路由树的。
路由树
前导知识
gin实现路由树的地方tree.go
进去后会看到一个tree的结构。
type methodTree struct {
method string //请求方式
root *node //子节点
}
type methodTrees []methodTree //多个请求方式字组成
func (trees methodTrees) get(method string) *node {
for _, tree := range trees {
if tree.method == method {
return tree.root
}
}
return nil
}
只要路由树不为空,那么他每次返回的节点都是根节点。
node的结构
type node struct {
path string // 当前节点相对路径(与祖先节点的 path 拼接可得到完整路径)
indices string // 所有孩子节点的path[0]组成的字符串
children []*node // 孩子节点
handlers HandlersChain // 当前节点的处理函数(包括中间件)
priority uint32 // 当前节点及子孙节点的实际路由数量(当前树的节点个数)
nType nodeType // 节点类型
maxParams uint8 // 子孙节点的最大参数数量
wildChild bool // 孩子节点是否有通配符(wildcard)
fullPath string // 路由全路径
}
nType的值
const (
static nodeType = iota // 普通节点,默认
root // 根节点
param // 参数路由,比如 /user/:id
catchAll // 匹配所有内容的路由,比如 /article/*key
)
执行情况
声明:执行代码里面,没有执行的我都删除了,不要抱着同一个函数前后代码不一样而感到疑惑!!
第一个GET路由注册
r.GET("/ping", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "pong",
})
})
他将执行的代码
//一开始就是一个空的根节点
func (n *node) addRoute(path string, handlers HandlersChain) {
fullPath := path // /ping
n.priority++ //实际路由的数量 1
// Empty tree
//判断当前的节点是否是一个空的节点
if len(n.path) == 0 && len(n.children) == 0 {
n.insertChild(path, fullPath, handlers)//处理路径的通配符以及插入路径和HandlerFunc
n.nType = root//设置当前节点类型
return
}
}
//主要处理路径的通配符,没有通配符相关的就会处理的很简单
func (n *node) insertChild(path string, fullPath string, handlers HandlersChain) {
for {
// Find prefix until first wildcard
//查询是否有通配符(":","*")
//没有回返回“”,-1,false
wildcard, i, valid := findWildcard(path)
if i < 0 { // No wildcard found
break
}
}
//没有通配符,就简单插入路径和HandlerFunc
n.path = path
n.handlers = handlers
n.fullPath = fullPath
}
第二个GET路由注册
r.GET("/ping/hello", func(c *gin.Context) {
c.JSON(200,gin.H{
"message": "hello",
})
})
他将执行的代码
func (n *node) addRoute(path string, handlers HandlersChain) {
fullPath := path // /ping/hello
n.priority++ // 2
//path的共同前缀位置
parentFullPathIndex := 0
walk:
for {
//获取公共前缀字符串的长度
i := longestCommonPrefix(path, n.path)
if i < len(path) {
//获取除去前缀字符串的路径
path = path[i:]
//获取路径的第一个字符
c := path[0]
// Otherwise insert it
if c != ':' && c != '*' && n.nType != catchAll {
//没有通配符的路径,插入节点
// []byte for proper unicode char conversion, see #65
n.indices += bytesconv.BytesToString([]byte{c})
child := &node{
fullPath: fullPath,
}
//将孩子节点添加进入之前的节点
n.addChild(child)
//给当前节点的孩子节点排序(由于现在的路由简单就没有排序)
n.incrementChildPrio(len(n.indices) - 1)
//将child引用到n,后面对n进行插入信息
n = child
}
n.insertChild(path, fullPath, handlers)
return
}
}
}
// addChild will add a child node, keeping wildcards at the end
//添加子节点,将通配符保留最后。
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 {
// Find prefix until first wildcard
//查询是否有通配符(":","*")
//没有回返回“”,-1,false
wildcard, i, valid := findWildcard(path)
if i < 0 { // No wildcard found
break
}
}
//没有通配符,就简单插入路径和HandlerFunc
n.path = path
n.handlers = handlers
n.fullPath = fullPath
}
第三个路由注册
r.GET("/about", func(c *gin.Context) {
c.JSON(200,gin.H{
"message": "about",
})
})
他将执行的代码
func (n *node) addRoute(path string, handlers HandlersChain) {
fullPath := path // /about
n.priority++ //3
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) //1 前缀的字符串是"/"
// Split edge
//当前缀字符串的长度,小于根节点路径长度。将当前节点作为一个子节点,添加到新建立的前缀节点
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}
// []byte for proper unicode char conversion, see #65
//获取子节点的首字符
n.indices = bytesconv.BytesToString([]byte{n.path[i]})
//当前节点的路径替换成前缀字符串
n.path = path[:i]
n.handlers = nil
n.wildChild = false
//当前节点的完整路径换成前缀字符串
n.fullPath = fullPath[:parentFullPathIndex+i]
}
// Make new node a child of this node
//添加新的节点
if i < len(path) {
//获取除去前缀字符串的路径
path = path[i:]
//获取路径的首字符
c := path[0]
// Check if a child with the next path byte exists
//检查当前路径是否存在与子节点首字母一致的
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
}
}
// Otherwise insert it
if c != ':' && c != '*' && n.nType != catchAll {
// []byte for proper unicode char conversion, see #65
//添加路径的首字符
n.indices += bytesconv.BytesToString([]byte{c})
//建立孩子节点
child := &node{
fullPath: fullPath,// ”/about“
}
//添加孩子节点
n.addChild(child)
//给当前节点的孩子节点排序(由于现在的路由简单就没有排序)
n.incrementChildPrio(len(n.indices) - 1)
//将孩子节点引用到n
n = child
}
//对孩子节点进行插入信息
n.insertChild(path, fullPath, handlers)
return
}
// Otherwise add handle to current node
//对于出现重复的路径的处理,覆盖之前的HandlerFunc
if n.handlers != nil {
panic("handlers are already registered for path '" + fullPath + "'")
}
n.handlers = handlers
n.fullPath = fullPath
return
}
}
//添加子节点,将通配符保留最后。
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)
}
}
当前构建的路由树
经过这三个简单的路由注册,了解到比较详细的构建路由树的过程。
动态路由
通配符*路由
r.GET("/about/*work", func(c *gin.Context) {
c.JSON(200,gin.H{
"work":c.Param("work"),
})
})
他将执行的代码
func (n *node) addRoute(path string, handlers HandlersChain) {
fullPath := path // "/about/*work"
n.priority++ //4
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) //1
// Make new node a child of this node
if i < len(path) {
path = path[i:] //"about/*work"
c := path[0] //a
// 检查下一个路径字节的子节点是否存在
//如果存在的话,当前的路由树就是子树了
for i, max := 0, len(n.indices); i < max; i++ {
//当前路径首字母与根节点的子节点首字母相同
if c == n.indices[i] {
//当前节点前缀的位置
parentFullPathIndex += len(n.path)
i = n.incrementChildPrio(i) //1
//将当前路由树换成匹配成功的子树
n = n.children[i]
//回到一开始,当前的walk的下面的代码没有再次进行了。
continue walk
}
}
}
//接下来的代码时 执行continue walk后的过程!!!!!请不要出现思维混乱的状况
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) //5 前缀字符串 "/about"
// Make new node a child of this node
if i < len(path) {
//"/*work"
path = path[i:]
//"/"
c := path[0]
// Otherwise insert it
if c != ':' && c != '*' && n.nType != catchAll {
// []byte for proper unicode char conversion, see #65
//"/"
n.indices += bytesconv.BytesToString([]byte{c})
//建立孩子节点
child := &node{
fullPath: fullPath, //"/about/*work"
}
//添加子节点
n.addChild(child)
//这里没有排序
n.incrementChildPrio(len(n.indices) - 1)
//将当前子树换成子节点
n = child
}
n.insertChild(path, fullPath, handlers)
return
}
}
}
// 增加给定孩子的优先级并在必要时重新排序
func (n *node) incrementChildPrio(pos int) int {
//获取当前节点的子节点
cs := n.children
//给对应的子树 节点数量+1
cs[pos].priority++
prio := cs[pos].priority //2
// Adjust position (move to front)
//会根据数量进行交换,把子树节点多的子树放到前面
newPos := pos // 1
for ; newPos > 0 && cs[newPos-1].priority < prio; newPos-- {
// Swap node positions
cs[newPos-1], cs[newPos] = cs[newPos], cs[newPos-1]
}
// 如果上面的顺序发生了变化,那么就需要构建新的索引字符串
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
}
//当前节点对应的是之前建立的孩子节点
func (n *node) insertChild(path string, fullPath string, handlers HandlersChain) {
for {
// Find prefix until first wildcard
wildcard, i, valid := findWildcard(path) //在当前的路径找到通配符"*" 返回 1 ,"*work" , "true"
//没有通配符会退出
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
}
// catchAll
//检测当前通配符* 路径是否在path的最后
//wildcard是通配符+名字
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, //"/about/*work"
}
//添加孩子节点
n.addChild(child)
//设置索引的字符串
n.indices = string('/')
//更换后续操作对象 ---上面的子节点
n = child
//节点数+1
n.priority++
//第二个节点是带有变量的。
// second node: node holding the variable
child = &node{
path: path[i:],//"/*work"
nType: catchAll,
handlers: handlers,
priority: 1,
fullPath: fullPath,
}
//添加刚才创建的节点
n.children = []*node{child}
return
}
}
子树构建情况:
路由构建情况
温馨提示:上面为空,是因为举例太特殊了。
如果是以下三个路由注册。
/about/ping/*work
/about/ping
/about/hello情况就会不一样了。
通配符:路由
r.GET("/:lang/intro", func(c *gin.Context) {
c.JSON(200,gin.H{
"lang":c.Param("lang"),
})
})
他将执行的代码
func (n *node) addRoute(path string, handlers HandlersChain) {
fullPath := path //"/:lang/intro"
n.priority++ //5
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) //1
// Make new node a child of this node
if i < len(path) {
path = path[i:] // ":lang/intro"
c := path[0] //":"
// Check if a child with the next path byte exists
//这里indices没有找到匹配的字符
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
}
}
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
//他的源码就会把通配符+名字都截取下来
//i是通配符的位置
wildcard, i, valid := findWildcard(path) //返回":lang" 0 true
if i < 0 { // No wildcard found
break
}
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, //":lang"
fullPath: fullPath, //":lang/intro"
}
//添加节点
n.addChild(child)
//设置为true 说明这个节点下面有通配符 :的节点
n.wildChild = true
//后续的操作节点换成了孩子节点
n = child
//节点数+1
n.priority++
// 如果路径不以通配符结尾,则有
// 将是另一个以“/”开头的非通配符子路径
if len(wildcard) < len(path) {
path = path[len(wildcard):] //获取通配符后面的子路径
//创建孩子节点
child := &node{
priority: 1,
fullPath: fullPath,
}
//添加孩子节点
n.addChild(child)
//更换后续操作的节点
n = child
continue
//continue之后他会为子路径建立一个新的节点,放到通配符节点下面。后面就不展示了
}
// Otherwise we're done. Insert the handle in the new leaf
n.handlers = handlers
return
}
}
// If no wildcard was found, simply insert the path and handle
n.path = path
n.handlers = handlers
n.fullPath = fullPath
}
上面所有路由注册情况:
总结:
希望你能通过上面的代码以及注释能看懂,路由构建的全过程!
当然其实因为路由比较简单,还有很多的代码没有讲解到,你可以自己探索!!!
声明:执行代码里面,没有执行的我都删除了,不要抱着同一个函数前后代码不一样而感到疑惑!!
路由组
其实路由组没有很特别的地方!别像太复杂了!!
核心代码:
// Group 创建一个新的路由器组。 您应该添加所有具有公共中间件或相同路径前缀的路由。
// 例如,所有使用公共中间件进行授权的路由都可以分组。
func (group *RouterGroup) Group(relativePath string, handlers ...HandlerFunc) *RouterGroup {
return &RouterGroup{
Handlers: group.combineHandlers(handlers),
basePath: group.calculateAbsolutePath(relativePath),
engine: group.engine,
}
}
当你们建立了路由组v1的话,其实后续都是在这个路由组基础上加一些信息(basepath,Handlers)。带着这个路由信息去构建路由树。
如何查找路由
我之前讲过所有的路由请求,都是交到engine实现的ServeHTTP 里面。
// ServeHTTP conforms to the http.Handler interface.
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
c := engine.pool.Get().(*Context)
c.writermem.reset(w)
c.Request = req
c.reset()
//这上面都是建立一个Context对象
//这才是重点,处理请求的逻辑函数
engine.handleHTTPRequest(c)
engine.pool.Put(c)
}
发起一个请求
以GET /ping 为例
func (engine *Engine) handleHTTPRequest(c *Context) {
httpMethod := c.Request.Method //获取请求方式 "GET"
rPath := c.Request.URL.Path //获取请求的路径 "/ping"
unescape := false
// Find root of the tree for the given HTTP method
t := engine.trees //获取engine的tree的结构
for i, tl := 0, len(t); i < tl; i++ {
//根据树查询请求方式
if t[i].method != httpMethod {
continue
}
//获取当前请求方式的根节点
root := t[i].root
// Find route in tree
//获得路由相关信息 handlerfunc 完整路径
value := root.getValue(rPath, c.params, unescape)
if value.params != nil {
c.Params = *value.params
}
if value.handlers != nil {
//给context对象赋值
//HandlerFunc
c.handlers = value.handlers
//完整路径
c.fullPath = value.fullPath
//执行HandlerFunc
c.Next()
//写入执行函数的内容
c.writermem.WriteHeaderNow()
return
}
}
c.handlers = engine.allNoRoute
serveError(c, http.StatusNotFound, default404Body)
}
查找路由核心代码
func (n *node) getValue(path string, params *Params, unescape bool) (value nodeValue) {
var (
skippedPath string
latestNode = n // 缓存最新节点
)
walk: // Outer loop for walking the tree
for {
prefix := n.path //获取根节点的path 也就是前缀路径 ---》"/"
//此时的请求路径长度大于根节点路径
if len(path) > len(prefix) {
//path路径的前缀等于根节点path
if path[:len(prefix)] == prefix {
//获取请求路径除去根节点的path后的路径 "ping"
path = path[len(prefix):]
// Try all the non-wildcard children first by matching the indices
//获取路径的首字母
idxc := path[0] // "p"
for i, c := range []byte(n.indices) {
//根据索引找到了对应的子树
if c == idxc {
// strings.HasPrefix(n.children[len(n.children)-1].path, ":") == n.wildChild
//如果当前的根节点子节点存在通配符的路由,那么更新lastnode=n
if n.wildChild {
skippedPath = prefix + path
latestNode = &node{
path: n.path,
wildChild: n.wildChild,
nType: n.nType,
priority: n.priority,
children: n.children,
handlers: n.handlers,
fullPath: n.fullPath,
}
}
//获取首字母匹配成功的子树
n = n.children[i]
continue walk
}
}
//温馨提示:因为执行了continue walk后所以后面执行就是后续的walk了
}
}
//这里接着上面的continue walk
walk: // Outer loop for walking the tree
for {
prefix := n.path //获取 “ping”
//当前路径与当前节点匹配上了
if path == prefix {
// 如果当前路径不等于 '/' 并且该节点没有注册的句柄并且最近匹配的节点有一个子节点
// 当前节点需要等于最新匹配的节点
//找的是通配符的路径,我们现在的请求没有就跳过
if latestNode.wildChild && n.handlers == nil && path != "/" {
n = latestNode.children[len(latestNode.children)-1]
}
// We should have reached the node containing the handle.
// Check if this node has a handle registered.
//这里就是找到了节点
if value.handlers = n.handlers; value.handlers != nil {
value.fullPath = n.fullPath
return
}
//其实后面的判断都是再找通配符的路由
//如果此路由没有句柄,但此路由有通配符子级,则必须有此路径的句柄,并带有附加的尾部斜杠
if path == "/" && n.wildChild && n.nType != root {
value.tsr = true
return
}
// 没有找到句柄。 检查此路径的句柄是否存在尾随斜杠推荐尾随斜杠
for i, c := range []byte(n.indices) {
if c == '/' {
n = n.children[i]
value.tsr = (len(n.path) == 1 && n.handlers != nil) ||
(n.nType == catchAll && n.children[0].handlers != nil)
return
}
}
return
}
}
以上就是简单的路由的全过程。
在这里我希望你能自己Debug,看看怎么获取动态参数的!
获取动态参数
这个过程其实不难,我简单说说。
举例:
当我发起GET /ssss/intro 这个请求时候。
- 他会先根据根节点的path,获取后续的节点,然后靠首字母查询子树,如果没有找到,就定义成是通配符的路由
- 因为通配符的路由都会放在最后,所以直接找根节点的最后一个子树(可以看
addChild这个函数) - 根据节点类型判断通配符,此时判断的是param
- 然后截取路由,根据“/” 截取到了 ”ssss“
- 他就会与当前子树的根节点“:leng”进行匹配,获得param的
- key:“lang”
- value:”ssss“
- 如果他还有子路径的话
- 将路径截取成“/intro”
- 在通配符路由子树下进行查询
- 工作其实上面发起的简单请求一致。
发起通配符路由,拿到的结果
总结
以上就是gin路由的全过程了。
我的文章的全过程都是去Debug源码,跑执行的代码,至于跳过的,我都会删除,因为这样才能准确的看到全过程,不会看的很混乱。
建议自己Debug跑一边简单的路由请求全过程,然后拿自己曾经写的,再去Debug一遍,验证自己的猜想!!