golang学习笔记03——gin框架的核心数据结构

文章目录

上期文章我们讲到了golang中gin框架的基本原理和底层请求、渲染的流程,还不知道的小伙伴查看golang学习笔记02------gin框架及基本原理

那么,本章节我们来更进一步,深入学习gin框架的核心数据结构gin.Context的讲解前缀树压缩前缀树代码实现

1.核心数据结构

1.1 gin.Context

gin.Context是我们基于gin框架业务开发时最常接触到的结构。

该结构是一个context.Context实现,因此可以将该结构传递到所有接收context.Context的方法或函数中。

golang 复制代码
type Context struct {
    writermem responseWriter
    Request   *http.Request  // http请求
    Writer    ResponseWriter // http响应输出流

    Params   Params // URL路径参数
    handlers HandlersChain   // 处理器链
    index    int8 // 当前的处理进度,即处理链路处于函数链的索引位置
    fullPath string

    engine       *Engine
  ...
    mu sync.RWMutex // 用于保护 map 的读写互斥锁

    // 提供对外暴露的 Get 和 Set 接口向用户提供了共享数据的存取服务,相关操作都在读写锁的保护之下,能够保证并发安全
    Keys map[string]any // 缓存 handlers 链上共享数据的 map,由于使用的map,避免了设置多个值时context形成链表

  ...
    queryCache url.Values // 查询参数缓存,使用时调用`Request.URL.Query()`,该方法每次都会对原始的查询字符串进行解析,所以这里设置缓存避免冗余的解析操作

    formCache url.Values // 表单参数缓存,作用同上
  ...
}

由于封装了http.Request和ResponseWriter(内部是http.ResponseWriter)对象,因此可以通过context对http请求响应进行操作。

context中还封装了处理器链HandlersChain和当前处理位置索引,因此可以很方便地访问处理器。

另外,我们知道,context能够以链表形式存储值(也就是说每个k-v会对应一个context,这些context之间之间以链表形式连接),当存在大量值时,访问效率比较低。因此gin.context在内部有一个map[string]any结构专门用于保存这些值,并且提供了线程安全访问方法。

golang 复制代码
func (c *Context) Set(key string, value any) {
    c.mu.Lock()
    defer c.mu.Unlock()
    if c.Keys == nil {
        c.Keys = make(map[string]any)
    }

    c.Keys[key] = value
}

// Get returns the value for the given key, ie: (value, true).
// If the value does not exist it returns (nil, false)
func (c *Context) Get(key string) (value any, exists bool) {
    c.mu.RLock()
    defer c.mu.RUnlock()
    value, exists = c.Keys[key]
    return
}

针对需要用到表单参数和查询字符串参数的场景,gin.Context进行了优化,设计了两个缓存结构(即queryCache和formCache)来提高重复访问时的效率。以表单参数为例:

golang 复制代码
func (c *Context) PostForm(key string) (value string) {
    value, _ = c.GetPostForm(key)
    return
}

func (c *Context) GetPostForm(key string) (string, bool) {
    if values, ok := c.GetPostFormArray(key); ok {
        return values[0], ok
    }
    return "", false
}

func (c *Context) PostFormArray(key string) (values []string) {
    values, _ = c.GetPostFormArray(key)
    return
}

func (c *Context) GetPostFormArray(key string) (values []string, ok bool) {
    c.initFormCache()
    values, ok = c.formCache[key]
    return
}

func (c *Context) initFormCache() {
    if c.formCache == nil {
        c.formCache = make(url.Values)
        req := c.Request
        // 从这里可以看出,如果不使用缓存,则每次都会解析请求,效率较低
        if err := req.ParseMultipartForm(c.engine.MaxMultipartMemory); err != nil {
            if !errors.Is(err, http.ErrNotMultipart) {
                debugPrint("error on parse multipart form array: %v", err)
            }
        }
        c.formCache = req.PostForm
    }
}

通过这样两个缓存结构,避免每次请求时都调用net/http库的方法。

1.2 前缀树

(1)前缀树

前缀树也称Trie树或字典树,是一种基于字符串公共前缀构建树形结构,来降低查询时间和提高效率的目的。前缀树一般用于统计和排序大量的字符串,其核心思想是空间换时间。

前缀树有三个重要特性:

  • 根节点不包含字符,除根节点外每一个节点都只包含一个字符。
  • 从根节点到某一节点路径上所有字符连接起来,就是该节点对应的字符串。
  • 每个节点任意子节点包含的字符都不相同。

如下是普通前缀树的结构:

(2)压缩前缀树

上述前缀树实现起来比较简单,但是在空间利用上并不高效,因此有压缩前缀树。不同之处在于,压缩前缀树会对节点进行压缩,可以简单认为如果某一个节点是其父节点的唯一子节点,则会与父节点合并。

gin框架就采用的是压缩前缀树实现。

我们一般会将前缀树与哈希表结构进行对比,实际上标准库采用的就是哈希表实现。哈希表实现简单粗暴,但是有一些缺点,不太适合作为通用的路由结构。如:

  • 哈希表实现只支持简单的路径,不支持路径参数和通配
  • 路由的数量一般是有限的,使用map的优势并不明显
  • 哈希表需要存储完整的路径,相比较而言前缀树存储公共前缀只需要一个节点,空间效率更高
(3)代码实现

前面说过,gin针对每一个http请求方法,都构造了一棵前缀树,即:

golang 复制代码
type methodTree struct {
    method string
    root   *node // 该方法对应的路由树的根节点
}

其中method即http请求方法,root则是指向对应前缀树根节点的指针,node结构是前缀树的节点。

golang 复制代码
type node struct {
    path string // 节点路径(不包含父节点)
    indices string // 子节点数组中每个节点path的首字母
    wildChild bool // 是否存在通配类型的子节点
    nType nodeType // 节点类型,包括root(根节点)、static(静态节点)、catchAll(通配符*匹配的节点)、param(参数节点,即带:的节点)
    priority uint32 // 根据经过节点的路由数确定的节点优先级。同一个节点下的子节点会按照节点优先级降序排序,匹配时按序遍历children。优先级越高,越先被匹配。
    handlers HandlersChain // 处理器链
    fullPath string // 完整路径(路由树结构中根节点到当前节点的路径上的全部path的完整拼接)
}

如下是有关优先级的一部分代码:

golang 复制代码
func (n *node) incrementChildPrio(pos int) int {
    // 子节点数组
    cs := n.children
    // 增加对应的子节点的优先级
    cs[pos].priority++
    prio := cs[pos].priority

    // 调整节点位置,确保整个子节点数组是按照优先级倒序排列的,从而优先级更大的节点会被优先匹配
    newPos := pos
    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
}

压缩前缀树部分是gin框架中最复杂的代码。

本人技术水平有限,文章中可能存在不足和遗漏,如果有同学愿意一起学习golang和gin的代码,也可以留言补充,一起学习共同成长!

关注我,带你发现更多有意思的技术和应用~👉👉

相关推荐
计算机学姐37 分钟前
基于python+django+vue的在线学习资源推送系统
开发语言·vue.js·python·学习·django·pip·web3.py
吃什么芹菜卷1 小时前
2024.9最新:CUDA安装,pytorch库安装
人工智能·pytorch·笔记·python·深度学习
云边有个稻草人1 小时前
【刷题】Day5--数字在升序数组中出现的次数
开发语言·笔记·算法
月夕花晨3741 小时前
C++学习笔记(26)
c++·笔记·学习
向往风的男子1 小时前
【从问题中去学习k8s】k8s中的常见面试题(夯实理论基础)(三十一)
学习·容器·kubernetes
蜡笔小新星2 小时前
切换淘宝最新镜像源npm
vue.js·经验分享·学习·npm·node.js
zhangrelay3 小时前
Arduino IDE离线配置第三方库文件-ESP32开发板
笔记·学习·持续学习
我叫啥都行3 小时前
计算机基础知识复习9.13
linux·笔记·后端·系统架构
limengshi1383923 小时前
通信工程学习:什么是AN-SMF接入网系统管理功能
服务器·网络·网络协议·学习·信息与通信