Golang 1.24 引入新的标准库: weak

使用 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 时执行代码。稍后将详细介绍这一特性。

收益

  1. weak.Make 会创建一个 weak.Pointer,它隐藏了实际内存地址,但不会影响 GC。
  2. 如果实际对象被垃圾回收,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]

参考资料

原文地址:https://mp.weixin.qq.com/s/CFjH2fBnIt9f48ZKut8PMg