使用 weak.Pointer
灵活管理对象
Go 1.24 引入了一个名为 weak 的新标准库[^1],它允许创建对 *T 的安全引用,而不会阻止垃圾回收器 (GC) 回收与 *T 相关的内存。
The weak package provides ways to safely reference memory weakly, meaning without preventing its reclamation by the garbage collector.
与 OS.ROOT 一样, weak
功能在其他编程语言中已经支持:
在 Java 中,WeakReference 和 SoftReference 是主要用于缓存和对象池的经典实现。这些引用允许在 JVM 检测到内存不足时自动进行垃圾回收。
在 Python 中,weakref 模块允许创建弱引用,通常用于防止循环引用问题或缓存。
在 C++ 中,std::weak_ptr 与 std::shared_ptr 一起被引入,以解决共享指针的循环依赖问题。
在 Rust 中,Weak 是 Rc 和 Arc 的弱引用版本,有助于防止循环引用并提供更灵活的内存管理。
weak 标准库的定义
weak
的定义只有一个 Make 方法和一个 Value 方法。
通过使用 weak.Make,我们创建了一个 weak.Pointer,如果原始 *T 没有被 GC,我们可以通过 weak.Pointer.Value 访问它的地址。如果对象已被收集,Value 将返回 nil。
可以在这里看到 weak
执行的示例:
package main
import (
"fmt"
"runtime"
"strings"
"time"
"unsafe"
"weak"
)
func main() {
originalObject := "Hello, World!"
runtime.AddCleanup(&originalObject, func(s int64) {
fmt.Println("originalObject clean at: ", s)
}, time.Now().Unix())
weakPtr := weak.Make(&originalObject)
fmt.Println(fmt.Sprintf("originalObject:addr %x", &originalObject))
fmt.Println(fmt.Sprintf("weakPtr addr:%x,size:%d", weakPtr, unsafe.Sizeof(weakPtr)))
runtime.GC()
time.Sleep(1 * time.Millisecond)
value := weakPtr.Value()
if value != nil && strings.Contains(*value, originalObject) {
fmt.Println("First GC :value: ", *value)
} else {
fmt.Println("first gc. Weak reference value is nil")
}
runtime.GC()
time.Sleep(1 * time.Millisecond)
value = weakPtr.Value()
if value != nil {
fmt.Println("Second GC", *value)
} else {
fmt.Println("Second GC: Weak reference value is nil")
}
}
输出:
➜ weak git:(main) ✗ gotip version
go version devel go1.24-18b5435 Sun Dec 15 21:41:28 2024 -0800 darwin/arm64
➜ weak git:(main) ✗ gotip run main.go
originalObject:addr 14000010050
weakPtr addr:{1400000e0d0},size:8
First GC :value: Hello, World!
originalObject clean at: 1734340907
Second GC: Weak reference value is nil
在这个例子中: 我们创建了一个字符串变量 originalObject,并使用 weak.Make 创建了一个名为 weakPtr 的 weak.Pointer。
在第一次垃圾回收 (GC) 期间,由于 originalObject 仍在使用,weakPtr.Value 会返回 originalObject 的地址。
在第二次 GC 中,由于 originalObject 不再被使用,它将被 GC 收集,而 weakPtr.Value 返回 nil。
此外,runtime.AddCleanup 是 Go 1.24 中的一项新功能,其作用与 runtime.SetFinalizer 类似,允许您在对象 GC 时执行代码。稍后将详细介绍这一特性。
收益
- weak.Make 会创建一个 weak.Pointer,它隐藏了实际内存地址,但不会影响 GC。
- 如果实际对象被垃圾回收,weak.Pointer.Value 将返回 nil。由于我们不知道实际对象何时会被回收,所以要经常检查 weak.Pointer.Value 的返回值。
weak 的实际应用
看过之前文章的用户,可能还记得 Go 1.23 中引入的 unique 功能,它使用一个指针(8 字节)来表示多个相同的字符串,从而节省了内存。weak 包也能实现类似的功能(事实上,在 Go 1.24 中,unique 已使用 weak 进行了重构)。
实现固定大小的高速缓存
下面是一个使用 weak+list 的固定大小缓存的示例:
import (
"container/list"
"fmt"
"runtime"
"sync"
"time"
"weak"
)
type CacheItem struct {
key string
value any
}
type WeakCache struct {
cache map[string]weak.Pointer[list.Element] // Use weak references to store values
mu sync.Mutex
storage Storage
}
// Storage is a fixed-length cache based on doubly linked tables and weak
type Storage struct {
capacity int // Maximum size of the cache
list *list.List
}
// NewWeakCache creates a fixed-length weak reference cache.
func NewWeakCache(capacity int) *WeakCache {
return &WeakCache{
cache: make(map[string]weak.Pointer[list.Element]),
storage: Storage{capacity: capacity, list: list.New()},
}
}
// Set adds or updates cache entries
func (c *WeakCache) Set(key string, value any) {
// If the element already exists, update the value and move it to the head of the chain table
if elem, exists := c.cache[key]; exists {
if elemValue := elem.Value(); elemValue != nil {
elemValue.Value = &CacheItem{key: key, value: value}
c.storage.list.MoveToFront(elemValue)
elemWeak := weak.Make(elemValue)
c.cache[key] = elemWeak
return
} else {
c.removeElement(key)
}
}
// remove the oldest unused element if capacity is full
if c.storage.list.Len() >= c.storage.capacity {
c.evict()
}
// Add new element
elem := c.storage.list.PushFront(&CacheItem{key: key, value: value})
elemWeak := weak.Make(elem)
c.cache[key] = elemWeak
}
// Get gets the value of the cached item
func (c *WeakCache) Get(key string) (any, bool) {
if elem, exists := c.cache[key]; exists {
// Check if the weak reference is still valid
if elemValue := elem.Value(); elemValue != nil {
// Moving to the head of the chain indicates the most recent visit
c.storage.list.MoveToFront(elemValue)
return elemValue.Value.(*CacheItem).value, true
} else {
c.removeElement(key)
}
}
return nil, false
}
// evict removes the cache item that has not been used for the longest time
func (c *WeakCache) evict() {
if elem := c.storage.list.Back(); elem != nil {
item := elem.Value.(*CacheItem)
c.removeElement(item.key)
}
}
// removeElement removes elements from chains and dictionaries.
func (c *WeakCache) removeElement(key string) {
c.mu.Lock()
defer c.mu.Unlock()
if elem, exists := c.cache[key]; exists {
// Check if the weak reference is still valid
if elemValue := elem.Value(); elemValue != nil {
c.storage.list.Remove(elemValue)
}
delete(c.cache, key)
}
}
// Debug prints the contents of the cache
func (c *WeakCache) Debug() {
fmt.Println("Cache content:")
for k, v := range c.cache {
if v.Value() != nil {
fmt.Printf("Key: %s, Value: %v\n", k, v.Value().Value.(*CacheItem).value)
}
}
}
func (c *WeakCache) CleanCache() {
c.storage.list.Init()
c.storage.capacity = 0
}
func main() {
cache := NewWeakCache(3)
cache.Set("a", "value1")
cache.Set("b", "value2")
cache.Set("c", "value3")
runtime.GC()
time.Sleep(1 * time.Millisecond)
cache.Debug()
// Access "a" to make it the most recently used
_, _ = cache.Get("a")
runtime.GC()
time.Sleep(1 * time.Millisecond)
cache.Debug()
// Add new element "d", triggering the elimination of the oldest unused "b".
cache.Set("d", "value4")
runtime.GC()
time.Sleep(1 * time.Millisecond)
cache.Debug()
cache.CleanCache()
runtime.GC()
time.Sleep(1 * time.Millisecond)
cache.Debug()
}
在这个例子中:
我们使用固定大小的列表和 Map 来存储列表中每个键的位置,其中键值是指向列表元素的弱指针。
当我们添加一个新的缓存项时,首先要检查列表是否完整。如果是,则驱逐最旧的项目。
当缓存中存在键时,Map[key].Value 将返回列表元素的地址。如果项目被驱逐,Map[key].Value 将返回 nil。
这种设计有助于创建一个高效、固定大小的缓存系统。使用 weak 无锁定队列结构可以更高效地处理数据。 我发现它在特定场景下非常有用。并且使用起来非常简单,我打算将它融入新的项目中。
更多关于 weak 标准库的讨论可以参考issue 67552[2]
参考资料
- [1] go 1.24 pre-release: https://tip.golang.org/doc/go1.24#weak
- [2] weak issue: https://github.com/golang/go/issues/67552