Featured image of post GO1.24: New Std-Lib weak

GO1.24: New Std-Lib weak

Go 1.24 introduces the `weak` package for managing memory more flexibly with weak pointers, preventing GC issues. Ideal for caching! πŸ’»βœ¨

 

Go 1.24 introduces a new std-lib package called weak, which allows you to create safe references to *T without preventing the garbage collector (GC) from reclaiming the memory associated with *T.

The weak package provides ways to safely reference memory weakly without preventing the garbage collector’s reclamation.

Much like OS.ROOT, weak is a feature that has existed in other programming languages for some time, including:

  • In Java, WeakReference and SoftReference are classic implementations primarily used for caching and object pools. These references allow automatic garbage collection when the JVM detects a memory shortage.
  • In Python, the weakref module allows the creation of weak references, commonly used to prevent circular reference issues or for caching.
  • In C++, std::weak_ptr was introduced alongside std::shared_ptr to solve circular dependency problems with shared pointers.
  • In Rust, Weak is the weak reference version of Rc and Arc, which helps prevent circular references and provides more flexible memory management.

Simple Definition of weak

The weak package is defined, with just a Make method and a Value method.
Pasted image 20241216144653

By using weak.Make, a weak.Pointer is created, and if the original *T has not been GC, we can access its address through weak.Pointer.Value. If the object has been collected, the Value will return nil.

You can see an example implementation of weak here: Example Code on Gist

Example Output:

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

In this example:

  • We create a string variable originalObject and use weak.Make to create a weak.Pointer called weakPtr.
  • During the first garbage collection (GC), since originalObject is still in use, weakPtr.Value returns the address of originalObject.
  • In the second GC, since originalObject is no longer used, it is collected by the GC, and weakPtr.Value returns nil.

Also, runtime.AddCleanup, a new feature in Go 1.24, works similarly to runtime.SetFinalizer by allowing you to execute code when an object is garbage collected. This feature will be covered in more detail in a later post.

Key Takeaways:

  1. weak.Make creates a weak.Pointer, which hides the real memory address but does not affect the GC.
  2. If the real object is garbage collected, weak.Pointer.Value will return nil. Since we don’t know when the real object will be reclaimed, always check the return value of weak.Pointer.Value.

The Practical Use of weak

Canonicalization Maps

You might remember the unique feature introduced in Go 1.23, which uses a single pointer (8 bytes) to represent multiple identical strings, saving memory. The weak package can achieve similar functionality (In fact, in Go 1.24, unique has been refactored using weak).

Implementing a Fixed-Size Cache

 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
35
36
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  
type 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  
}

In this example, we use a fixed-size list and a Map to store the position of each key in the list, where the value is a weak.Pointer to a list.Element. When we add a new cache item, we first check if the list is full. If so, the oldest item is evicted. When a key exists in the cache, Map[key].Value will return the address of the list element. If the item is evicted, Map[key].Value returns nil.

This design helps to create an efficient, fixed-size cache system. Using weak with a lock-free queue structure allows for even more efficient data handling.

I find weak to be quite helpful in specific scenarios. It’s easy to use, and I plan to incorporate it into my projects.

Further Reading on weak:

  1. Go 1.24 Weak Documentation
  2. Go GitHub Issue #67552
Licensed under CC BY-NC-SA 4.0
Last updated on Dec 16, 2024 19:06 CST
Built with Hugo
Theme Stack designed by Jimmy