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

相关文章

从入门到放弃:使用 spf13/viper 管理 Go 应用配置

在现代软件开发中,配置管理是一个至关重要的环节。随着应用的复杂性增加,配置管理的需求也变得更加多样化和复杂化。Go 语言社区中,spf13/viper 是一个非常流行的配置管理库,它提供了一种强...

使用 spf13/cobra 构建强大的 Go 命令行应用

spf13/cobra 是 Go 语言中非常流行的一个库,用于创建命令行应用(CLI)。它提供了一种强大且易于使用的框架来开发支持复杂命令结构的应用程序。Cobra 库主要用于创建像 kubec...

Go语言Web框架 Fiber入门教程

Fiber 是一个基于 Go 语言的 Web 框架,灵感来源于 Express.js,旨在提供快速、简单且轻量级的开发体验。Fiber 的设计目标是让开发者能够快速构建高性能的 Web 应用,同...

Go语言跨平台GUI工具包tk9.0

不得不说,这名字起的.....tk9.0是一个用Go语言编写的跨平台GUI工具包,它使用Tcl/Tk作为底层图形库,无需CGo,这意味着您可以使用它来创建原生跨平台应用程序,而无需依赖于C语言编...

基于Go语言开源免费轻量级网站防火墙SamWaf

SamWaf网站防火墙是一款适用于小公司、工作室和个人网站的免费轻量级网站防火墙,完全私有化部署,数据加密且仅保存本地,一键启动,支持Linux,Windows 64位主要功能:完全独立引擎,防...

图片Base64编码

CSR生成

图片无损放大

图片占位符

Excel拆分文件