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在内部有一个mapstringany结构专门用于保存这些值,并且提供了线程安全访问方法。

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的代码,也可以留言补充,一起学习共同成长!

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

相关推荐
知识分享小能手27 分钟前
C学习教程,从入门到精通,C语言单片机基础 — 完整知识点手册(19)
c语言·单片机·学习
小黄人软件44 分钟前
Cluade 桌面端配置(ccswitch配置或文件直接配置)
学习
念何架构之路1 小时前
Go reflect包值操作 Value 的读写、可设置性与 interface 布局
开发语言·后端·golang
李永奉1 小时前
杰理可视化SDK开发-音频录制功能使用教程
笔记·单片机·嵌入式硬件·mcu·物联网·音视频
努力努力再努力FFF1 小时前
普通人适合考什么AI证书?不同基础和学习目标怎么选
人工智能·学习
ysu_03141 小时前
【C++ 进阶之路】(二)从内联函数到类与对象:深挖内存对齐、this 指针与三大默认成员函数
开发语言·数据结构·c++·笔记·面向对象·this
叼烟扛炮1 小时前
Linux 学习笔记(三十二):线程控制——创建、等待与分离
linux·运维·服务器·笔记·学习·1024程序员节
瑞克-桑切斯1 小时前
PostgreSQL 14.4 源码部署 & 运维手册(CentOS7)
数据库·学习·postgresql
Nuanyt1 小时前
LeetCode Hot 100 · 双指针 3 题 Java
java·笔记·算法·leetcode·双指针
深圳老胡2 小时前
FDC2212 + STM32G030 液面探测:IIC 读数跳动导致下探深度不一致的排查与解决
笔记·stm32·单片机·代码规范·1024程序员节