[翻译]从 unique 到 cleanups 和 weak:高效的新底层工具
原文《From unique to cleanups and weak: new low-level tools for efficiency》 链接:https://go.dev/blog/cleanups-and-weak 以下翻译来自 DeepSeek
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
}
funcNewMemoryMappedFile(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 []byte
connErr := 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]funcNewCachedMemoryMappedFile(filename string)(*MemoryMappedFile, error) {
var newFile *MemoryMappedFile
for {
// 尝试从缓存加载
value, ok := cache.Load(filename)
if !ok {
// 创建新映射文件
if newFile == nil {
var err error
newFile, err = NewMemoryMappedFile(filename)
if err != nil {
returnnil, err
}
}
// 尝试安装新映射文件
wp := weak.Make(newFile)
var loaded bool
value, 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 团队在保持语言简洁性的同时,也在为高级使用场景提供必要的底层支持。正如文中所说:"这些是带有微妙语义的高级工具",但正确使用它们可以解决一些原本难以处理的问题。
文中链接
① https://go.dev/blog/unique ② https://go.dev/doc/gc-guide#Introduction ③ https://go.dev/doc/gc-guide