Weak Pointers
Which is the most-wanted feature of Go 1.24? I have no idea! However, weak pointers are a hot candidate.
Learn debugging with Matt, at 40% off
(and support AppliedGo this way)!
Use the coupon code APPLIEDGO40 at ByteSizeGo.com.
(Affiliate Link)
What is a weak pointer?
Go uses garbage collection to make memory management convenient. If you create a pointer to data, Go may create the data on the heap if required. Once no more pointers point to a piece of data on the heap, the garbage collector can reclaim the memory used for the data.
However, what if you need a pointer that doesn't keep the pointed-to object from being garbage-collected?
This doesn't seem to make sense. A pointer that survives the data it points to? After all, if the data gets collected, that pointer would be invalid!
At a second glance, you can indeed find valid use cases, such as:
- A cache that caches objects only as long as there is at least one pointer pointing to it. Such a cache helps to avoid duplicate data, similar to the
unique
package. - Observe objects to track specific metrics throughout their lifetime.
Weak pointers allow for a straightforward implementation of such use cases.
How to use weak pointers
Theory is dull and grey, so let's turn to an example.
Prompt: Act as a game developer. You want to track characters inside the game, without preventing garbage collection if the player exits the game.
You: No problem, here is my tracker!
The game defines a Character
with a Position
:
type Character struct {
name string
position Position
}
type Position struct {
x, y int
}
A map of weak pointers to Character
keeps track of the characters in the game. Method Track()
adds a character to the map. A Remove()
method is omitted for brevity.
Method Track()
calls weak.Make()
to create a weak pointer from a *Character
and adds it to the map:
type LastSeenTracker map[string]weak.Pointer[Character]
func (t LastSeenTracker) Track(c *Character) {
t[c.name] = weak.Make(c)
}
Method GetPosition()
first checks if the given character name exists in the map. Then, it attempts to turn the weak pointer from the map into a normal pointer by calling the weak pointer's Value()
method.
Value()
returns a valid pointer to the object until the object is scheduled for garbage-collecting. In this case, Value()
returns nil
.
GetPosition()
converts the result of Value()
into the classic “value, ok” idiom:
func (t *LastSeenTracker) GetPosition(name string) (Position, bool) {
if wp, exists := t.characters[name]; exists {
if c := wp.Value(); c != nil {
return c.position, true
}
}
return Position{}, false
}
Func main()
simulates a character that leaves the game by creating a new scope for the character variable hero
. A new tracker tracks the character's position once inside and once outside the scope.
A call to runtime.GC()
ensures the character variable is collected on time (to avoid any “demo effect”):
func main() {
tracker := NewLastSeenTracker()
// Create a new scope to limit character lifetime
{
// Create a character that will go out of scope
hero := &Character{
name: "Hero",
position: Position{x: 10, y: 20},
}
// Start tracking the character
tracker.Track(hero)
// We can get the position while the character exists
if pos, ok := tracker.GetPosition("Hero"); ok {
fmt.Printf("Hero is at position (%d, %d)\n", pos.x, pos.y)
}
// hero goes out of scope here
}
// Force garbage collection
runtime.GC()
// Try to get position after character is collected
if pos, ok := tracker.GetPosition("Hero"); ok {
fmt.Printf("Hero is at position (%d, %d)\n", pos.x, pos.y)
} else {
fmt.Println("Hero is no longer in the game")
}
`
Run the full code on the Go Playground, or—spoiler alert!—see the code's output here:
> go run .
Hero is at position (10, 20)
Hero is no longer in the game
Bottom line: It's ok to be weak.