文章目录
gin路由原理
原理:Radix Tree - 基数树
啥是基数树
基数树(Radix Tree)又称为PAT位树(Patricia Trie or crit bit tree),是一种更节省空间的前缀树(Trie Tree),
共同的前缀,形成一个父节点,然后往下分。
具体看下图:
路由和前缀树的联系
我们注册路由的过程就是构造前缀树的过程,具有公共前缀的节点也共享一个公共父节点。
假设我们现在注册以下路由信息:
r := gin.Default()
r.GET("/", func1)
r.GET("/search/", func2)
r.GET("/support/", func3)
r.GET("/blog/", func4)
r.GET("/blog/:post/", func5)
r.GET("/about-us/", func6)
r.GET("/about-us/team/", func7)
r.GET("/contact/", func8)
那么我们会得到一个GET方法对应的路由树(每种请求类型,都会有一棵对应的路由树),具体结构如下:
Priority Path Handle
9 \ *<1>
3 ├s nil
2 |├earch\ *<2>
1 |└upport\ *<3>
2 ├blog\ *<4>
1 | └:post nil
1 | └\ *<5>
2 ├about-us\ *<6>
1 | └team\ *<7>
1 └contact\ *<8>
-
Priority: 表示这个节点下的子节点个数,子节点个数越多,代表这个前缀包含的路由越多。
每棵树都按照Priority的数量排序,多的靠前,优先匹配,类似这样:9 3 2 2 2 1 1 1 1 1
这样做的好处:优先匹配被大多数路由路径包含的节点,让尽可能多的路由快速被定位。我们要找的路由可能就包含在其中,算是一种成本补偿。
-
Path: 表示路由前缀树的结构。
-
Handle: 表示这个路由对应处理函数的指针。
使用前缀树的优势
- 由于URL路劲具有层次结构,所以很可能有许多常见的前缀,可以很好的划分前缀树。
- 路由器为每种请求方法管理一棵单独的树,它比在每个节点中都保存一个method-> handle map节省空间。
- 我们可以根据请求方法,去对应的前缀树查找,提升查询路由的效率。
简单看下源码
func main() {
r := gin.Default()
err := r.Run()
if err != nil {
// log error
}
}
1.进入 r.Run函数内部,
看到下面这些代码,可以看到实现Run函数的结构体是 Engine,我们细看下这个结构
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.看Engine结构实现的3个接口
3.点击看 Engine 实现 ServeHTTP(ResponseWriter, *Request) 方法
4.ServeHTTP(ResponseWriter, *Request)
// ServeHTTP conforms to the http.Handler interface.
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
// 每进来一个请求,都从Engine的对象池中申请对象
c := engine.pool.Get().(*Context)
c.writermem.reset(w)
c.Request = req
c.reset()
// 处理http请求
engine.handleHTTPRequest(c)
// 使用完成回收
engine.pool.Put(c)
}
5.engine.handleHTTPRequest©, 路由树节点
在函数中找到这个代码,这里就是我们上面说的前缀树了, 顺着找到树的节点,node
// Find root of the tree for the given HTTP method
t := engine.trees
type methodTree struct {
method string // 请求方法, get,post,put
root *node // 这个请求前缀树的根节点
}
// 路由方法树,为啥用切片存不用map存?
// 因为路由方法的个数不多,也就get,post,put,delete等这几种
// map占用更多的空间
// 并且在元素数量不大(不超过1000)的情况下,遍历数组的效率和hash查找差不多
type methodTrees []methodTree
// 根据请求方法,找到对应的路由树
func (trees methodTrees) get(method string) *node {
for _, tree := range trees {
if tree.method == method {
return tree.root
}
}
return nil
}
// tree.go
type node struct {
// 节点路径,比如上面的s,earch,和upport
path string
// 和children字段对应, 保存的是分裂的分支的第一个字符
// 例如search和support, 那么s节点的indices对应的"eu"
// 代表有两个分支, 分支的首字母分别是e和u
indices string
// 儿子节点
children []*node
// 处理函数切片
handlers HandlersChain
// 优先级,这个根节点下子节点的数量
priority uint32
// 节点类型,包括static, root, param, catchAll
// static: 静态节点(默认),比如上面的s,earch等节点
// root: 树的根节点
// catchAll: 有*匹配的节点
// param: 参数节点, 类似这种 r.GET("/blog/:post/", func5)
nType nodeType
// 路径上最大参数个数
maxParams uint8
// 节点是否是参数节点,比如上面的:post
wildChild bool
// 完整路径
fullPath string
}
6.Engine
Engine是一个重要的结构体,它包含路由, 中间件, 相关配置信息等, 在 gin.go中
type Engine struct {
RouterGroup // 包含路由结构
pool sync.Pool // context pool
trees methodTrees // 路由树
// html template及其他相关属性先暂时忽略
}
// 这行的意思是创建一个匿名的IRouter类型变量,赋一个空的Engine指针
// 这样做的目的是确保 Engine 结构实现 IRouter 接口
// 因为这里赋值了,如果没有实现接口的话,编译就挂了....
var _ IRouter = &Engine{}
Engine的初始化
r := gin.Default() -> engine := New()
func New() *Engine {
debugPrintWARNINGNew()
engine := &Engine{
RouterGroup: RouterGroup{
Handlers: nil,
basePath: "/",
root: true,
},
FuncMap: template.FuncMap{},
RedirectTrailingSlash: true,
RedirectFixedPath: false,
HandleMethodNotAllowed: false,
ForwardedByClientIP: true,
RemoteIPHeaders: []string{"X-Forwarded-For", "X-Real-IP"},
TrustedPlatform: defaultPlatform,
UseRawPath: false,
RemoveExtraSlash: false,
UnescapePathValues: true,
MaxMultipartMemory: defaultMultipartMemory,
// 可以看到,路由树初始化时候,设置的容量是9个
trees: make(methodTrees, 0, 9),
delims: render.Delims{Left: "{{", Right: "}}"},
secureJSONPrefix: "while(1);",
trustedProxies: []string{"0.0.0.0/0", "::/0"},
trustedCIDRs: defaultTrustedCIDRs,
}
engine.RouterGroup.engine = engine
engine.pool.New = func() any {
return engine.allocateContext()
}
return engine
}
7.路由注册
每一个GET,POST…这些函数中,都会返回这个 group.handle()方法
// POST is a shortcut for router.Handle("POST", path, handle).
func (group *RouterGroup) POST(relativePath string, handlers ...HandlerFunc) IRoutes {
return group.handle(http.MethodPost, relativePath, handlers)
}
// GET is a shortcut for router.Handle("GET", path, handle).
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()
}
8.构建请求树
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
}
if sectionsCount := countSections(path); sectionsCount > engine.maxSections {
engine.maxSections = sectionsCount
}
}
// tree.go
// addRoute 将具有给定句柄的节点添加到路径中。
// 不是并发安全的
func (n *node) addRoute(path string, handlers HandlersChain) {
fullPath := path
n.priority++ // 优先级 + 1
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循环中一层一层的构造前缀树
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
}
}
9.路由匹配
看gin框架处理请求的入口函数ServeHTTP:
// gin.go
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
c := engine.pool.Get().(*Context) // 对象池取对象
c.writermem.reset(w) // 这里有一个细节就是Get对象后做初始化
c.Request = req
c.reset()
engine.handleHTTPRequest(c) // 我们要找的处理HTTP请求的函数
engine.pool.Put(c) // 处理完请求后将对象放回池子
}
处理请求
// gin.go
func (engine *Engine) handleHTTPRequest(c *Context) {
// liwenzhou.com...
// 根据请求方法找到对应的路由树
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
}
// liwenzhou.com...
c.handlers = engine.allNoRoute
serveError(c, http.StatusNotFound, default404Body)
}
getValue根据给定的路径(键)返回nodeValue值
// tree.go
type nodeValue struct {
handlers HandlersChain
params Params // []Param
tsr bool
fullPath string
}
// liwenzhou.com...
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
}
}
中间件
中间件其实就是一个处理函数,在路由处理之前执行
中间件常用函数有 c.Next()、c.Abort()、c.Set()、c.Get()
1.中间件注册
r := gin.Default() // 创建gin的engine
func Default() *Engine {
debugPrintWARNINGDefault()
engine := New()
engine.Use(Logger(), Recovery()) // 默认注册的两个中间件
return engine
}
func (engine *Engine) Use(middleware ...HandlerFunc) IRoutes {
// 实际上还是调用的RouterGroup的Use函数
engine.RouterGroup.Use(middleware...)
engine.rebuild404Handlers()
engine.rebuild405Handlers()
return engine
}
注册中间件其实就是将中间件函数追加到group.Handlers中:
func (group *RouterGroup) Use(middleware ...HandlerFunc) IRoutes {
group.Handlers = append(group.Handlers, middleware...)
return group.returnObj()
}
注册路由时会将对应路由的函数和之前的中间件函数结合到一起, 拼接到路由处理函数切片中
// 路由处理函数切片,中间件函数也放到这里面
type HandlersChain []HandlerFunc
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()
}
2.中间件常用函数
c 是指 *gin.Context
*gin.Context贯穿这个请求调用的
c.Next(), 根据索引调用处理函数切片中的处理函数, 其实是调用下一个
func (c *Context) Next() {
c.index++
for c.index < int8(len(c.handlers)) {
c.handlers[c.index](c)
c.index++
}
}
c.Abort() :切断调用链,这个请求就不会再调用其他处理函数了
func (c *Context) Abort() {
c.index = abortIndex // 直接将索引置为最大限制值,从而退出循环
}
c.Set()/c.Get() : 这两个方法多用于在多个函数之间通过c传递数据的
总结:
- gin框架路由使用前缀树,路由注册的过程是构造前缀树的过程,路由匹配的过程就是查找前缀树的过程。
- gin框架的中间件函数和处理函数是以切片形式的调用链条存在的,我们可以顺序调用也可以借助c.Next()方法实现嵌套调用。
- 借助c.Set()和c.Get()方法我们能够在不同的中间件函数中传递数据。
gin框架代码学习点
1.for循环优化
学习下for循环的优化,看 handleHTTPRequest() 方法中, for 循环这样写
for i, arrLen := 0, len(arr); i < arrLen; i++ {
// process
}
这样一次性把切片或数组的长度算出来保存起来,不用每一次循环都调用 len() 函数,提升效率.
2.切片拷贝
func Copy(a1, a2 []int) []int {
finalSize = len(a1) + len(a2) //计算两个切片总长度
mergedArr := make([]int, finalSize) //申请合并之后的切片
copy(mergedArr, a1) //拷贝第一个切片
copy(mergedArr[len(a1):], a2) //拷贝第二个切片到第一个切片的尾部
return mergedArr
}