【GO基础学习】gin框架路由详解


gin框架路由详解

先创建一个项目,编写一个简单的demo,对这个demo进行讲解。

  1. 创建一个go的项目,采用GoLand:

在这里插入图片描述
2. 在该项目下创建一个main.go文件:

package main

import (
	"net/http"

	"github.com/gin-gonic/gin"
)

func main() {
	r := gin.Default()
	
	r.GET("/", func(c *gin.Context) {
		c.String(http.StatusOK, "hello word")
	})
	
	r.Run(":8000")
}

上面就是一个非常简单的gin的使用,逐行代码解读,在解读前,需要先下载gin库,写入main.go后,在terminal执行go mod tidy命令就会添加main里面的gin库。

(1)go mod tidy

  • 添加缺失的依赖

如果你的代码中引用了某些依赖(通过 import),但它们没有被记录在 go.mod 文件中,go mod tidy 会自动将这些缺失的依赖添加到 go.mod 文件中。

  • 移除未使用的依赖

如果你的代码中不再使用某些依赖(即没有通过 import 引用),go mod tidy 会从 go.mod 文件中移除这些无用的依赖。

  • 更新 go.sum 文件

go mod tidy 会检查 go.sum 文件(存储模块的校验和)是否与 go.mod 文件一致:

​ -如果某些依赖的校验和缺失,它会添加。

​ -如果某些校验和多余(对应的依赖已被移除),它会删除。

(2)r := gin.Default()

创建默认的Gin引擎,点进这个方法查看源码:

// Default returns an Engine instance with the Logger and Recovery middleware already attached.
func Default(opts ...OptionFunc) *Engine {
	debugPrintWARNINGDefault()
	engine := New()
	engine.Use(Logger(), Recovery())
	return engine.With(opts...)
}

返回了Engine的指针结构体,还包括一些日志和中断复原的操作。

关于Engine结构体:【是 Gin 框架的核心结构体,它既是路由表的管理器,也是 HTTP 服务的入口】

type Engine struct {
    RouterGroup
    trees       methodTrees  // 每种 HTTP 方法对应的路由树
    maxParams   uint16       // 路由参数最大数量
    maxSections uint16       // 路由路径最大分段数量
    handlers404 HandlersChain // 404 处理函数链
    // 其他字段...
}
  • trees: 存储路由表的核心字段,每种 HTTP 方法有一棵对应的 Radix 树。

  • RouterGroup: 用于管理路由组和中间件。

  • handlers404: 默认的 404 错误处理。

关于trees是路由规则的核心,存储路由表,每种 HTTP 方法有一棵对应的 Radix 树。
(1)Radix 树
公共前缀的树结构,是一种更节省空间的前缀树(Trie Tree)。对于基数树的每个节点,如果该节点是唯一的子树的话,就和父节点合并。下图为一个基数树示例:
在这里插入图片描述
(2)methodTrees

type methodTree struct {
	method string
	root   *node
}

type methodTrees []methodTree

method是http的类型,每个路由路径的片段都由一个node节点构成:

type node struct {
    path      string        // 当前节点的路径部分
    indices   string        // 子节点的索引,用于快速查找
    children  []*node       // 子节点
    handlers  HandlersChain // 当前节点的处理函数
    priority  uint32        // 优先级,用于优化匹配顺序
    wildChild bool          // 是否包含通配符子节点
    nType     nodeType      // 节点类型: static, param, catchAll
}

path: 存储路径片段。

indices: 子节点索引,表示每个子节点的第一个字符,用于快速查找。

handlers: 当前节点绑定的处理函数。

wildChild: 是否有动态或通配符子节点。

nType:

  • static: 静态路径节点。
  • param: 动态路径节点(如 :id)。
  • catchAll: 通配符节点(如 *filepath)。

(3)r.GET()

Gin 的路由实现主要分为路由注册路由匹配两部分。

路由注册

点进GET方法:

// GET is a shortcut for router.Handle("GET", path, handlers).
func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes {
	return group.handle(http.MethodGet, relativePath, handlers)
}

handle方法:

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()
}

addRoute方法:【注册路由的核心函数】

func (e *Engine) addRoute(method, path string, handlers HandlersChain) {
    root := e.trees.get(method) // 获取当前方法对应的路由树
    if root == nil {
        root = new(node)       // 如果路由树不存在,创建新的树
        e.trees = append(e.trees, methodTree{method: method, root: root})
    }
    root.addRoute(path, handlers) // 将路径插入到 Radix 树中
}

Radix 树节点插入逻辑:addRoute in node
在 node 中的 addRoute 方法负责将路径拆分并插入到树中:

// addRoute 将具有给定句柄的节点添加到路径中。
// 不是并发安全的
func (n *node) addRoute(path string, handlers HandlersChain) {
	fullPath := path
	n.priority++
	numParams := countParams(path)  // 数一下参数个数

	// 空树就直接插入当前节点
	if len(n.path) == 0 && len(n.children) == 0 {
		n.insertChild(numParams, path, fullPath, handlers)
		n.nType = root
		return
	}

	parentFullPathIndex := 0

walk:
	for {
		// 更新当前节点的最大参数个数
		if numParams > n.maxParams {
			n.maxParams = numParams
		}

		// 找到最长的通用前缀
		// 这也意味着公共前缀不包含“:”"或“*” /
		// 因为现有键不能包含这些字符。
		i := longestCommonPrefix(path, n.path)

		// 分裂边缘(此处分裂的是当前树节点)
		// 例如一开始path是search,新加入support,s是他们通用的最长前缀部分
		// 那么会将s拿出来作为parent节点,增加earch和upport作为child节点
		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, //子节点优先级-1
				fullPath:  n.fullPath,
			}

			// Update maxParams (max of all children)
			for _, v := range child.children {
				if v.maxParams > child.maxParams {
					child.maxParams = v.maxParams
				}
			}

			n.children = []*node{&child}
			// []byte for proper unicode char conversion, see #65
			n.indices = string([]byte{n.path[i]})
			n.path = path[:i]
			n.handlers = nil
			n.wildChild = false
			n.fullPath = fullPath[:parentFullPathIndex+i]
		}

		// 将新来的节点插入新的parent节点作为子节点
		if i < len(path) {
			path = path[i:]

			if n.wildChild {  // 如果是参数节点
				parentFullPathIndex += len(n.path)
				n = n.children[0]
				n.priority++

				// Update maxParams of the child node
				if numParams > n.maxParams {
					n.maxParams = numParams
				}
				numParams--

				// 检查通配符是否匹配
				if len(path) >= len(n.path) && n.path == path[:len(n.path)] {
					// 检查更长的通配符, 例如 :name and :names
					if 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 +
					"'")
			}
			// 取path首字母,用来与indices做比较
			c := path[0]

			// 处理参数后加斜线情况
			if n.nType == param && c == '/' && len(n.children) == 1 {
				parentFullPathIndex += len(n.path)
				n = n.children[0]
				n.priority++
				continue walk
			}

			// 检查路path下一个字节的子节点是否存在
			// 比如s的子节点现在是earch和upport,indices为eu
			// 如果新加一个路由为super,那么就是和upport有匹配的部分u,将继续分列现在的upport节点
			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 != '*' {
				// []byte for proper unicode char conversion, see #65
				// 注意这里是直接拼接第一个字符到n.indices
				n.indices += string([]byte{c})
				child := &node{
					maxParams: numParams,
					fullPath:  fullPath,
				}
				// 追加子节点
				n.children = append(n.children, child)
				n.incrementChildPrio(len(n.indices) - 1)
				n = child
			}
			n.insertChild(numParams, path, fullPath, handlers)
			return
		}

		// 已经注册过的节点
		if n.handlers != nil {
			panic("handlers are already registered for path '" + fullPath + "'")
		}
		n.handlers = handlers
		return
	}
}

整个路由树构造的详细过程:

(1)第一次注册路由,例如注册search
(2)继续注册一条没有公共前缀的路由,例如blog
(3)注册一条与先前注册的路由有公共前缀的路由,例如support

路由注册示例:

package main

import (
    "github.com/gin-gonic/gin"
)

func main() {
    r := gin.Default()

    // 注册静态路由
    r.GET("/hello", func(c *gin.Context) {
        c.String(200, "Hello, World!")
    })

    // 注册动态路由
    r.GET("/user/:id", func(c *gin.Context) {
        id := c.Param("id")
        c.String(200, "User ID: %s", id)
    })

    // 注册通配符路由
    r.GET("/static/*filepath", func(c *gin.Context) {
        filepath := c.Param("filepath")
        c.String(200, "Filepath: %s", filepath)
    })

    r.Run(":8080")
}

(4)r.Run()

路由匹配

路由匹配是根据请求路径在 Radix 树中查找对应节点并执行处理函数的过程。

核心代码:getValue

getValue 方法负责在 Radix 树中查找路径:
(1)Run()方法:

/ Run attaches the router to a http.Server and starts listening and serving HTTP requests.
// It is a shortcut for http.ListenAndServe(addr, router)
// Note: this method will block the calling goroutine indefinitely unless an error happens.
func (engine *Engine) Run(addr ...string) (err error) {
	defer func() { debugPrintError(err) }()

	if engine.isUnsafeTrustedProxies() {
		debugPrint("[WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.\n" +
			"Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.")
	}

	address := resolveAddress(addr)
	debugPrint("Listening and serving HTTP on %s\n", address)
	err = http.ListenAndServe(address, engine.Handler())
	return
}

(2)Handler()方法处理类:

func (engine *Engine) Handler() http.Handler {
	if !engine.UseH2C {
		return engine
	}

	h2s := &http2.Server{}
	return h2c.NewHandler(engine, h2s)
}

(3)http.Handler

type Handler interface {
	ServeHTTP(ResponseWriter, *Request)
}

(4)ServeHTTP实现:

// gin.go
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
  // 这里使用了对象池
	c := engine.pool.Get().(*Context)
  // 这里有一个细节就是Get对象后做初始化
	c.writermem.reset(w)
	c.Request = req
	c.reset()

	engine.handleHTTPRequest(c)  // 我们要找的处理HTTP请求的函数

	engine.pool.Put(c)  // 处理完请求后将对象放回池子
}

(5)handleHTTPRequest方法

// gin.go
func (engine *Engine) handleHTTPRequest(c *Context) {
	// 根据请求方法找到对应的路由树
	t := engine.trees
	for i, tl := 0, len(t); i < tl; i++ {
		if t[i].method != httpMethod {
			continue
		}
		root := t[i].root
		// 在路由树中根据path查找
		value := root.getValue(rPath, c.Params, unescape)
		if value.handlers != nil {
			c.handlers = value.handlers
			c.Params = value.params
			c.fullPath = value.fullPath
			c.Next()  // 执行函数链条
			c.writermem.WriteHeaderNow()
			return
		}
	
	c.handlers = engine.allNoRoute
	serveError(c, http.StatusNotFound, default404Body)
}

(6)getValue方法
路由匹配是由节点的 getValue方法实现的。getValue根据给定的路径(键)返回nodeValue值,保存注册的处理函数和匹配到的路径参数数据。
如果找不到任何处理函数,则会尝试TSR(尾随斜杠重定向)。

// tree.go

type nodeValue struct {
	handlers HandlersChain
	params   Params  // []Param
	tsr      bool
	fullPath string
}

func (n *node) getValue(path string, po Params, unescape bool) (value nodeValue) {
	value.params = po
walk: // Outer loop for walking the tree
	for {
		prefix := n.path
		if path == prefix {
			// 我们应该已经到达包含处理函数的节点。
			// 检查该节点是否注册有处理函数
			if value.handlers = n.handlers; value.handlers != nil {
				value.fullPath = n.fullPath
				return
			}

			if path == "/" && n.wildChild && n.nType != root {
				value.tsr = true
				return
			}

			// 没有找到处理函数 检查这个路径末尾+/ 是否存在注册函数
			indices := n.indices
			for i, max := 0, len(indices); i < max; i++ {
				if indices[i] == '/' {
					n = n.children[i]
					value.tsr = (len(n.path) == 1 && n.handlers != nil) ||
						(n.nType == catchAll && n.children[0].handlers != nil)
					return
				}
			}

			return
		}

		if len(path) > len(prefix) && path[:len(prefix)] == prefix {
			path = path[len(prefix):]
			// 如果该节点没有通配符(param或catchAll)子节点
			// 我们可以继续查找下一个子节点
			if !n.wildChild {
				c := path[0]
				indices := n.indices
				for i, max := 0, len(indices); i < max; i++ {
					if c == indices[i] {
						n = n.children[i] // 遍历树
						continue walk
					}
				}

				// 没找到
				// 如果存在一个相同的URL但没有末尾/的叶子节点
				// 我们可以建议重定向到那里
				value.tsr = path == "/" && n.handlers != nil
				return
			}

			// 根据节点类型处理通配符子节点
			n = n.children[0]
			switch n.nType {
			case param:
				// find param end (either '/' or path end)
				end := 0
				for end < len(path) && path[end] != '/' {
					end++
				}

				// 保存通配符的值
				if cap(value.params) < int(n.maxParams) {
					value.params = make(Params, 0, n.maxParams)
				}
				i := len(value.params)
				value.params = value.params[:i+1] // 在预先分配的容量内扩展slice
				value.params[i].Key = n.path[1:]
				val := path[:end]
				if unescape {
					var err error
					if value.params[i].Value, err = url.QueryUnescape(val); err != nil {
						value.params[i].Value = val // fallback, in case of error
					}
				} else {
					value.params[i].Value = val
				}

				// 继续向下查询
				if end < len(path) {
					if len(n.children) > 0 {
						path = path[end:]
						n = n.children[0]
						continue walk
					}

					// ... but we can't
					value.tsr = len(path) == end+1
					return
				}

				if value.handlers = n.handlers; value.handlers != nil {
					value.fullPath = n.fullPath
					return
				}
				if len(n.children) == 1 {
					// 没有找到处理函数. 检查此路径末尾加/的路由是否存在注册函数
					// 用于 TSR 推荐
					n = n.children[0]
					value.tsr = n.path == "/" && n.handlers != nil
				}
				return

			case catchAll:
				// 保存通配符的值
				if cap(value.params) < int(n.maxParams) {
					value.params = make(Params, 0, n.maxParams)
				}
				i := len(value.params)
				value.params = value.params[:i+1] // 在预先分配的容量内扩展slice
				value.params[i].Key = n.path[2:]
				if unescape {
					var err error
					if value.params[i].Value, err = url.QueryUnescape(path); err != nil {
						value.params[i].Value = path // fallback, in case of error
					}
				} else {
					value.params[i].Value = path
				}

				value.handlers = n.handlers
				value.fullPath = n.fullPath
				return

			default:
				panic("invalid node type")
			}
		}

		// 找不到,如果存在一个在当前路径最后添加/的路由
		// 我们会建议重定向到那里
		value.tsr = (path == "/") ||
			(len(prefix) == len(path)+1 && prefix[len(path)] == '/' &&
				path == prefix[:len(prefix)-1] && n.handlers != nil)
		return
	}
}

Radix 树的路径匹配过程:

package main

import (
    "fmt"
)

type node struct {
    path     string
    children []*node
    handlers func()
}

func (n *node) addRoute(path string, handler func()) {
    child := &node{path: path, handlers: handler}
    n.children = append(n.children, child)
}

func (n *node) getRoute(path string) func() {
    for _, child := range n.children {
        if child.path == path {
            return child.handlers
        }
    }
    return nil
}

func main() {
    root := &node{}
    root.addRoute("/hello", func() {
        fmt.Println("Hello, World!")
    })

    handler := root.getRoute("/hello")
    if handler != nil {
        handler() // 输出:Hello, World!
    } else {
        fmt.Println("Route not found!")
    }
}

总结

  1. 创建路由表
    • 每种 HTTP 方法有独立的 Radix 树。
    • 路由通过 addRoute 插入到对应的树中。
  2. 处理 HTTP 请求
    • Gin 的入口是 EngineServeHTTP 方法。
    • 根据请求方法和路径查找路由节点:
      • 如果找到,执行绑定的处理函数。
      • 如果未找到,执行 404 处理函数。
  3. 分发请求
    • 匹配成功的路由节点的处理函数会被依次执行,支持中间件链。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值