Go 1.23 中的新包 unique

Gounique 包提供了一系列工具,用于通过称为 驻留(或 规范化)的过程来优化“可比较的值”的管理。驻留是一种将多个相同的值(例如内容相同的字符串或结构体)合并为唯一副本的机制。通过这种方式,所有相同的值都共享同一块内存,从而显著减少内存使用并加快相等性比较的速度。

Michael Knyszek 在 Go 官方博客 中详细介绍了这个包,并讨论了开发过程中引入的一些新概念,例如弱指针和终结器的替代方案。

简单的驻留实现

从宏观上看,驻留的实现非常简单。以下代码展示了如何使用 Go 的基本 Map 来对字符串进行去重:

var internPool map[string]string

// Intern 返回一个与 `s` 等价的字符串,该字符串可能与之前传递给 Intern 的字符串共享存储。
func Intern(s string) string {
    pooled, ok := internPool[s]
    if !ok {
        // 克隆字符串以防它是某个更大字符串的一部分。
        pooled = strings.Clone(s)
        internPool[pooled] = pooled
    }
    return pooled
}

当你处理可能重复的字符串(例如解析文本格式)时,这种方法非常有用。然而,它存在一些问题:

  1. 内存泄漏:字符串永远不会从池中移除。
  2. 并发问题:它不能在多个 goroutine 中安全地使用。
  3. 仅限于字符串:该思想实际上可以推广到其他类型。
  4. 优化不足:在比较字符串时,如果指针不相等,Go 需要比较字符串的内容。而驻留允许我们通过比较指针来快速判断相等性。

unique 包的引入

Go 1.23 引入的 unique 包提供了一个类似于 Intern 的函数 MakeMake 在内部使用一个全局 Map(一个快速的泛型并发 Map)来查找值。与之前的简单实现不同,Make 有两个显著的区别:

  1. 支持泛型:它可以接受任何可比较的类型,而不仅限于字符串。
  2. Handle[T]:它返回一个 Handle[T],可以通过它检索规范化的值。

Handle[T] 的关键作用

Handle[T] 是该包设计的核心。两个 Handle[T] 只有在用相等的值创建时才相等。其优势在于:两个 Handle[T] 的比较只需进行指针比较,而不用比较具体的内容,这大大提高了效率。

此外,只要 Map 中存在某个值的 Handle[T],该值的规范化副本就会被保留。一旦所有指向特定值的 Handle[T] 消失,该值就可以被垃圾回收器移除。

如果你曾接触过 Lisp,这可能会让你想起 Lisp 中的符号系统。Lisp 中的符号类似于驻留的字符串,但符号本身并不是字符串。Handle[string]string 的关系就像 Lisp 中符号与字符串的关系。

实际应用示例

Go 标准库中的 net/netip 包使用 unique 包对 addrDetail 类型的值进行了驻留。以下是该代码的简化版本:

type Addr struct {
    // 地址的详细信息,进行了规范化存储。
    z unique.Handle[addrDetail]
}

type addrDetail struct {
    isV6   bool   // 如果是 IPv6 则为 true,IPv4 则为 false。
    zoneV6 string // 如果是 IPv6,则可能不为空。
}

var z6noz = unique.Make(addrDetail{isV6: true})

// WithZone 返回一个带有指定 zone 的 IP,若 zone 为空则移除 zone。
func (ip Addr) WithZone(zone string) Addr {
    if !ip.Is6() {
        return ip
    }
    if zone == "" {
        ip.z = z6noz
        return ip
    }
    ip.z = unique.Make(addrDetail{isV6: true, zoneV6: zone})
    return ip
}

由于许多 IP 地址可能使用相同的 zone,且 zone 是地址标识的一部分,因此对其进行规范化有助于减少内存占用。驻留后,zone 名称的比较仅需指针比较,从而提升性能。

关于字符串驻留的说明

尽管 unique 包非常有用,但它在处理字符串驻留时存在一些不同之处。要防止字符串被从内部 Map 中移除,必须同时保留 Handle[T] 和字符串值。

字符串的特殊之处在于,它们虽然像值一样工作,但实际上是指针。因此,理论上可以只对字符串的底层存储进行驻留,而不暴露 Handle[T] 的细节。未来可能会实现所谓的透明字符串驻留,类似于 Intern 函数,但具有更灵活的语义。

目前,可以通过 unique.Make("my string").Value() 进行一定程度的字符串驻留。尽管没有保留 Handle[T],字符串仍会在下次垃圾回收后被清理,因此在短期内依然能达到驻留效果。

历史背景与未来展望

实际上,自 net/netip 包引入以来,它就已经对 zone 字符串进行了驻留。它使用了 go4.org/intern 包的一个内部副本,该包在 Go 支持泛型之前通过 Value 类型实现了类似于 Handle[T] 的功能。

为了实现类似行为,早期的 intern 包通过不安全的代码实现了弱指针。而弱指针现在是 unique 包的核心特性。弱指针不会阻止垃圾回收,当变量被回收时,弱指针会自动变为 nil

在开发 unique 包时,Go 团队添加了对弱指针的适当支持,并提出了 公开提案。此外,这项工作还促使我们重新审视终结器,并提出了一个 更高效的终结器替代方案。随着可比较值的哈希函数即将推出,Go 在内存高效缓存构建方面前景光明。

参考资料

  1. unique 包的介绍
  2. 泛型并发 Map
  3. go4.org/intern
  4. 弱指针公开提案
  5. 终结器替代方案
  6. 高效缓存

标签: Go

相关文章

在 Go 项目中使用 LevelDB 进行数据存储

LevelDB 是一个由 Google 开发的高性能键值存储库,广泛应用于需要快速读写操作的场景。本文将介绍如何在 Go 项目中使用 LevelDB 作为数据存储,并通过示例代码展示如何初始化数...

详解Go语言依赖注入工具wire最佳实践介绍与使用

wire是一个强大的依赖注入工具,通过代码生成的方式实现了高效的依赖注入。本文详细介绍了wire的入门级和高级使用技巧,并通过示例代码展示了其强大的功能。无论是简单的依赖注入,还是复杂的依赖图生...

Go语言中copy命令讲解 切片之间复制元素

在Go语言中,copy函数是一个非常常用的内置函数,用于在切片(slice)之间复制元素。理解copy函数的用法和机制对于高效处理数据操作至关重要1. copy函数的基本用法copy函数的基本语...

深入理解 Go 语言中的 goto:用法与最佳实践

在学习编程语言时,goto 一直是一个颇具争议的概念。它常常因为“跳跃式”的行为被认为会让代码混乱且难以维护,但在 Go 语言中,goto 被保留并提供了一些实际的应用场景。今天我们将深入探讨 ...

Go并发编程与调度器及并发模式详解

Go语言以其简洁的语法和强大的并发能力,成为现代网络编程和微服务架构的热门选择。本文将深入探讨Go的并发编程模型,调度器的工作机制,以及多种并发模式的实现和应用,帮助开发者更好地理解并发编程的设...

Go语言中sync.Pool详解

sync.Pool 是 Go 语言标准库中的一个数据结构,用于提供高效的对象池。它的主要作用是缓存临时对象,以减少内存分配和垃圾回收的开销。sync.Pool 特别适合用于存储短生命周期的对象,...

图片Base64编码

CSR生成

图片无损放大

图片占位符

Excel拆分文件