Featured image of post go1.24: 新的标准库 weak

go1.24: 新的标准库 weak

 

weak 是什么?

Golang 在1.24 中带来了一个新的std-lib weak。 可以为*T 创建一个安全的引用,但是不会阻止 *T 被GC 回收。

Package weak provides ways to safely reference memory weakly, that is, without preventing its reclamation.

OS.ROOT 一样, weak 也是一个在其他语言中存在很久的功能,比如:

  • JavaWeakReferenceSoftReference 是经典实现,主要用于缓存和对象池。它们能够在 JVM 检测到内存不足时自动回收。
  • Python 提供了 weakref 模块,允许创建弱引用对象,常用于防止循环引用问题或缓存。
  • c++std::shared_ptr 中引入了 std::weak_ptr,用于解决共享指针的循环依赖问题。
  • Rust 提供 RcArc 的弱引用版本 Weak,也用于避免循环引用并提升内存管理的灵活性。

weak 的定义很简单,一个Make 方法还有一个Value 方法。
Pasted image 20241216144653
通过weak.Make 创建一个weak.Pointer ,如果 T 没有被回收的话,我们可以通过weak.Pointer.Value 获取T的地址。 否则就会返回 nil,很简单。
我们可以通过一个简单的例子来实践一下 weak。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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")
	}
}

https://gist.github.com/hxzhouh/abd6be9ed8860e506643031bb2d446ce

运行结果

1
2
3
4
5
6
7
8
➜  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

在上面的代码中,我们创建了一个 string 变量 originalObject ,然后使用weak.Make 创建了一个 weak.Pointer weakPtr

  • 在第一次GC 的时候,因为originalObject 在后面还有使用,所以 weakPtr.Value 返回了 originalObject 的地址。
  • 在第二次GC 的时候,originalObject 没有被使用,它被GC回收了, 所以 weakPtr.Value 返回了nil
  • runtime.AddCleanup 也是go 1.24 新增的功能,它的功能类似 runtime.SetFinalizer,也是在 对象呗垃圾回收的时候用于执行一段代码。我后面可能会详细介绍它
    通过上面的例子,我们可以知道
  1. weak.Make 通过创建一个中间地址(weak.Printer)将真实地址隐藏起来。
  2. weak.Printer 不会影响 真实地址的垃圾回收,如果真实地址被垃圾回收了,weak.Printer.Value 将会返回nil。由于不知道真实地址什么时候会被回收,所以需要仔细检查weak.Printer.Value 的返回值。

weak 有什么作用

canonicalization maps

相信您还记得在go 1.23 中添加的 unique 它可以将多个相同的字符串用一个指针(8个字节)来表示,达到节约内存的目的 , weak 也能实现类似的效果.(实际上 go 1.24unique 已经使用 weak 重构了)

实现一个固定大小的缓存

下面是一个使用weak+ list.List 实现 固定大小缓存的例子

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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 weaktype Storage struct {  
    capacity int // Maximum size of the cache  
    list     *list.List  
}
// Set
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  
}

完整的代码请参考: https://gist.github.com/hxzhouh/1945d4a1e5a6567f084628d60b63f125
我们可以创建一个固定大小的list ,然后使用一个Map记录keylist 中的位置,value 是一个指向 list.Elementweak.Pointer. 如果 key 存在于list 上,那么 Map[key].Value 会返回 list 的地址。 再给cache 添加数据的时候,会先判断list 的大小,如果已经list已经满了的话,就把队尾的数据淘汰。Map[key].Value返回nil。
这样,我们就能构建一个高效+固定大小的cache系统。 weak + 无锁队列 可以构建出更加高效的数据结构。

就我个人而言,weak 在特定场合下还是挺有用处的。使用起来也很简单,我会在项目中积极使用weak

更多关于 weak 的资料

  1. https://tip.golang.org/doc/go1.24#weak
  2. https://github.com/golang/go/issues/67552
Licensed under CC BY-NC-SA 4.0
最后更新于 Dec 14, 2024 19:40 CST
使用 Hugo 构建
主题 StackJimmy 设计