Weak Pointers

Which is the most-wanted feature of Go 1.24? I have no idea! However, weak pointers are a hot candidate.

The Ultimate Guide to Debugging With Go
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.