[翻译]从 unique 到 cleanups 和 weak:高效的新底层工具
Michael Knyszek
2025年3月6日
在去年的关于 unique 包的博文①中,我们提到了当时处于提案审查阶段的一些新特性。现在很高兴与大家分享,从 Go 1.24 开始,所有开发者都可以使用这些新特性了!这些新特性包括 runtime.AddCleanup
函数(用于在对象不可达时排队运行函数)和 weak.Pointer
类型(安全指向对象而不阻止其被垃圾回收)。这两大特性结合起来足以构建你自己的 unique 包!让我们深入探讨这些特性的实用场景和使用方法。
注意:这些新特性是垃圾回收器的高级功能。如果还不熟悉基本的垃圾回收概念,强烈建议先阅读垃圾回收指南的简介部分②。
Cleanups(清理函数)
如果使用过终结器(finalizer),那么对 cleanup 的概念应该不陌生。终结器是通过调用 runtime.SetFinalizer
与已分配对象关联的函数,在对象变得不可达后由垃圾回收器调用。从高层次看,cleanup 的工作原理类似。
我们通过一个使用内存映射文件的应用示例来说明 cleanup 的用法:
//go:build unixtype MemoryMappedFile struct {data []byte
}func NewMemoryMappedFile(filename string) (*MemoryMappedFile, error) {f, err := os.Open(filename)if err != nil {returnnil, err}defer f.Close()// 获取文件信息(需要文件大小)fi, err := f.Stat()if err != nil {returnnil, err}// 提取文件描述符conn, err := f.SyscallConn()if err != nil {returnnil, err}var data []byteconnErr := conn.Control(func(fd uintptr) {// 创建由该文件支持的内存映射data, err = syscall.Mmap(int(fd), 0, int(fi.Size()), syscall.PROT_READ, syscall.MAP_SHARED)})if connErr != nil {returnnil, connErr}if err != nil {returnnil, err}mf := &MemoryMappedFile{data: data}cleanup := func(data []byte) {syscall.Munmap(data) // 忽略错误}runtime.AddCleanup(mf, cleanup, data)return mf, nil
}
内存映射文件的内容直接映射到内存中。通过这段代码,当 *MemoryMappedFile
不再被引用时,内存映射会被自动清理。
注意 runtime.AddCleanup
的三个参数:
-
要附加 cleanup 的变量地址
-
cleanup 函数本身
-
传给 cleanup 函数的参数
与 runtime.SetFinalizer
的关键区别在于:cleanup 函数的参数独立于附加对象。这个改变修复了终结器的若干问题。
终结器的痛点包括:
-
涉及引用循环时会导致内存泄漏
-
至少需要两次完整 GC 周期才能回收内存
-
对象复活(resurrection)问题
cleanup 通过不传递原始对象解决了这些问题:
-
对象涉及循环引用仍可被回收
-
内存可以立即回收
弱指针(Weak Pointers)
假设我们需要通过文件名去重内存映射文件。使用 weak.Pointer
类型可以安全地实现缓存:
var cache sync.Map // map[string]weak.Pointer[MemoryMappedFile]func NewCachedMemoryMappedFile(filename string) (*MemoryMappedFile, error) {var newFile *MemoryMappedFilefor {// 尝试从缓存加载value, ok := cache.Load(filename)if !ok {// 创建新映射文件if newFile == nil {var err errornewFile, err = NewMemoryMappedFile(filename)if err != nil {returnnil, err}}// 尝试安装新映射文件wp := weak.Make(newFile)var loaded boolvalue, loaded = cache.LoadOrStore(filename, wp)if !loaded {runtime.AddCleanup(newFile, func(filename string) {cache.CompareAndDelete(filename, wp)}, filename)return newFile, nil}}// 检查缓存条目有效性if mf := value.(weak.Pointer[MemoryMappedFile]).Value(); mf != nil {return mf, nil}// 发现待清理的空条目cache.CompareAndDelete(filename, value)}
}
该示例展示的关键特性:
-
弱指针可比较且具有稳定标识
-
支持为单个对象添加多个独立 cleanup
-
可实现通用缓存结构(见原文通用
Cache
结构示例)
注意事项与未来工作
使用这些特性时需注意:
-
cleanup 关联对象不可被 cleanup 函数或其参数引用
-
弱指针作为 map 键时,值不能引用键对象
-
非确定性行为依赖 GC 实现细节
-
测试具有挑战性
未来可能改进方向:
-
Ephemeron(短命对象)支持
-
直接追踪映射内存区域的 API
总结
runtime.AddCleanup
和 weak.Pointer
为 Go 带来了更精细的内存管理能力,但需要谨慎使用。大多数场景应通过标准库间接使用这些特性,而非直接操作。建议开发者仔细阅读更新后的垃圾回收指南③中的使用建议。
这些特性的加入体现了 Go 团队在保持语言简洁性的同时,也在为高级使用场景提供必要的底层支持。正如文中所说:"这些是带有微妙语义的高级工具",但正确使用它们可以解决一些原本难以处理的问题。