Weak pointers in Go are references that do not keep an object alive for the garbage collector. Go 1.24 added the weak package to the standard library in February 2025, giving you weak.Pointer[T], a type you create with weak.Make(p) and read with Value(). A strong pointer stops collection; a weak pointer lets the runtime free the object and then reports the value is gone.
Strong vs weak pointers in Go
A strong reference stops the garbage collector from reclaiming an object. A weak reference lets collection proceed and then reads back as nil.
The weak package that adds weak pointers shipped with Go 1.24, released in February 2025. Reachability, not the pointer variable itself, decides whether memory is freed.
A pointer in Go is an address. Writing &a takes the address of a, and writing *p dereferences it, meaning you read or write the value stored at that address. When you assign that address to another variable, both names refer to the same memory location, so a write through either one is visible through the other.
The garbage collector traces references from roots such as goroutine stacks and globals. Any object it can reach stays in memory. A strong pointer counts as a reachable path, so the collector leaves the target alone even if nothing else in the program uses it. A weak pointer contributes no such path.
That single difference changes what you can build. A weak pointer gives you a handle you can test and discard, which suits caches, intern tables, and observer lists where holding the object permanently would defeat the purpose of storing it at all.
The trade-off is real: a weak pointer can become nil at any point where the collector runs. Code that reads it must handle that case every time, because there is no way to pin the value without creating a strong reference first.
Reading the pointer operators out loud
The pointer syntax trips people up mainly because three symbols look similar. They are not interchangeable:
| Syntax | Name | What it does |
|---|---|---|
&a | address-of | Returns the memory address where a is stored |
*p | dereference | Reads or writes the value at the address held in p |
*T | pointer type | Declares a variable that holds an address of a T |
One consequence catches people out. After p := &a, a second assignment b := &a makes b point at the same mailbox, so *b = 30 also changes a. Go infers the pointer type from the address you pass, so you rarely write the asterisk in that line yourself.
Go value semantics vs pointer semantics
Go passes function arguments by value, so a function receives a copy unless you pass a pointer; mutating the copy has no effect on the caller's variable. This rule explains most of the confusion around pointers in Go. Structs, slices, maps, and arrays all follow it.
A struct passed to a method is copied field by field. A method with a pointer receiver operates on the original, which is why methods that change state usually take a pointer receiver. Slices and maps behave differently because they are descriptors: the copy shares the same backing array or hash table, so element changes inside them are visible to the caller even without a pointer.
This is why the following snippet prints the old name. ChangeName receives a copy of the Person value, sets the copy's field, and returns. The original is untouched:
type Person struct {
Name string
}
func (p Person) ChangeName(name string) {
p.Name = name
}
func main() {
p := Person{Name: "Lucas"}
p.ChangeName("Joao")
fmt.Println(p.Name) // Lucas
}
Changing the receiver to *Person makes the same call mutate the original, because the method body now writes through the address rather than into a copy. The compiler takes the address of an addressable value automatically, so you can still call a pointer-receiver method on a local variable.
How to add the weak package in Go 1.24 and later
To use weak pointers in Go, add the weak package to your imports and wrap the pointer you want to track: w := weak.Make(data). That call returns a weak.Pointer[T], and w.Value() returns the original *T while the object is still reachable from somewhere else. Go 1.24, released in February 2025, is the first version that includes it.
At the time the recording was made, the speaker noted that Go was on version 1.23 and 1.24 was still at release candidate 2, so the API described here was not yet in a stable release.
The wrapper does not keep the object alive by itself. You still need at least one strong pointer somewhere, or the value disappears immediately. A typical cache stores the strong pointer in the map and hands out weak pointers to callers who must not extend the lifetime.
Reading a weak pointer is not atomic with the decision to use it, and that is the part most tutorials skip. Value() can return the pointer in one call and nil in the next if the collector ran in between, so the returned pointer has to be used immediately or converted into a strong reference that the caller holds for the duration of the work.
A short example shows the mechanics, with the runtime forced to collect so the transition is visible:
package main
import (
"fmt"
"runtime"
"time"
"weak"
)
type Data struct {
Key string
Value string
}
func main() {
obj := &Data{Key: "profile", Value: "cached"}
w := weak.Make(obj)
fmt.Println(w.Value().Value) // cached
obj = nil
runtime.GC()
time.Sleep(time.Second)
if w.Value() == nil {
fmt.Println("value was collected")
}
}
The order of the steps is the whole lesson:
- Allocate the object and keep the strong pointer
objon it. - Call
weak.Make(obj)to get aweak.Pointer, which adds no strong path. - Read
w.Value()whileobjis alive, and use the result. - Drop the strong pointer by setting
obj = nil. - Force
runtime.GC()so the collection happens at a predictable moment, then callw.Value()again and expect nil.
The time.Sleep(time.Second) is there only to make the collection observable before the check runs.
What weak.Pointer guarantees over uintptr tricks
weak.Pointer guarantees that if Value() returns non-nil, it returns the same pointer that was passed to Make; a raw uintptr offers no such guarantee. Go's garbage collector is moving-capable in design terms, and the runtime needs to know about references. Storing an address as an integer hides that reference from the collector and can produce a dangling value.
Before Go 1.24, codebases simulated weak references in ways that mostly did not work. Common attempts included storing addresses in map[uintptr]*T, keeping a finalizer that deleted the entry, or relying on runtime.SetFinalizer timing. Each approach has failure modes: addresses get reused after collection, and finalizers run late and only once.
The weak package removes the guesswork by making the weak relationship a first-class type the runtime understands. Collection clears the weak pointer; the runtime does not need a finalizer, and the pointer does not linger as a stale address that a future allocation might occupy.
The package does not offer resurrection protection beyond what Value() provides, does not pin objects, and does not act as a cache eviction policy. It is a reference type, not a memory solution by itself.
Where weak pointers earn their keep
Weak pointers in Go fit situations where a value is worth keeping only if someone else still needs it. The three patterns below appear most often in production code and in the Go issue tracker discussions that led to the package. None of them requires changing the garbage collector's behavior.
Caches with a strong owner
The cache holds weak references and the owning subsystem holds strong ones. When the owner drops the object, the cache entry becomes nil on the next read and can be pruned lazily. Memory returns to the runtime without a background eviction goroutine sweeping on a timer, though a sweep is still useful to shrink the map itself.
Canonicalization and intern tables
Two identical large values can share one allocation if the intern table stores a weak pointer to the canonical copy. When the last strong reference disappears, the canonical copy goes away and the entry is dropped. This keeps deduplication from turning into a permanent leak, which is what happens with a plain map of strong pointers.
Observer and listener registries
A registry that holds weak pointers to observers lets an observer be collected when the code that created it stops using it, instead of requiring an explicit deregistration call that someone will eventually forget. The registry still has to skip nil entries when it iterates.
Benchmarks and the limits of the network effect
No peer-reviewed benchmark shows that weak pointers in Go reduce memory by a fixed percentage; the payoff depends on the object graph and the collection cadence. What can be stated precisely is the version attribution: the weak package entered the standard library with Go 1.24 in February 2025, as recorded in the official Go 1.24 release notes. Earlier versions have no equivalent.
The relevant algorithmic work is the Go team's concurrent mark-and-sweep garbage collector, which has been the default since the Go 1.5 release in 2015 and has been refined in every release since. Weak-reference support is a runtime capability layered on that collector, not a change to the algorithm's core.
Reporting practice matters here. A cache that stops growing after a weak-pointer rewrite is a measurement of that cache on that workload. It is not evidence that weak pointers shrink all Go programs, and it is not evidence about other languages whose runtimes implemented weak references earlier, such as Java or Python.
For anyone deciding whether to adopt the feature, the honest summary is that it removes one specific category of leak at the cost of nil checks at every read site. That trade is favorable in caches and registries, and unnecessary in code where the strong reference is already released promptly.
The stack, the heap, and why they decide everything
The choice between stack and heap allocation is what makes pointers worth learning in the first place. Go keeps function-local data on the stack, which grows and shrinks as calls are pushed and popped. When a function returns, its frame disappears and the memory is reclaimed for free. A deep call chain that never unwinds far enough is what people mean by stack overflow.
Heap allocation is for values that outlive the function that created them or that need to be shared with other parts of the program. Those values get an address, and pointers are how the rest of the program reaches that address.
This is why the fair collection decision is described as "reachability": the collector walks the graph from stacks and globals, and anything it can follow stays. A weak pointer is simply an edge the walk does not follow, which is what lets the value be freed on schedule while the caller still holds a handle.
FAQ
What are weak pointers in Go?
A weak pointer is a reference that does not prevent the garbage collector from reclaiming its target. Go exposes the type as weak.Pointer[T] in the weak package, added in Go 1.24. Reading it through Value() returns the pointer while the object is reachable and nil afterwards.
When did Go add weak pointers?
The weak package shipped with Go 1.24, released in February 2025. Versions before that had no supported weak-reference type in the standard library. Third-party attempts relied on raw addresses or finalizers and were unreliable.
How do I use weak.Make in Go?
Call weak.Make(p) with a pointer, keep at least one strong pointer to the same object alive elsewhere, and check w.Value() before every use. The call itself does not extend the object's lifetime, so a program with no remaining strong pointer can lose the value immediately.
Why does weak.Pointer.Value() return nil?
The garbage collector reclaimed the object because no strong reference reached it. That is the intended behavior, not an error, and every read site has to handle the nil case. There is no way to reserve the object without creating a strong reference first.
Can weak pointers replace caching libraries?
No. They govern reachability, not eviction policy, expiration, or size limits. A production cache still needs maximum size, TTL, and metrics, and weak references only solve the case where the owning subsystem already signals when a value is no longer needed.
What happens if I store a pointer address as uintptr?
The collector cannot see the reference, so it may free the object while your integer still holds the old address. If a later allocation lands at the same address, you read unrelated data. weak.Pointer exists precisely to avoid that class of bug.
Does the garbage collector run at a predictable time?
No. Collection is triggered by allocation pressure and other runtime heuristics, so a weak pointer can change state between two consecutive statements. Tests that need determinism call runtime.GC() explicitly, as the example above does.
Are weak pointers available in other languages?
Yes, and their runtimes shipped the capability earlier. JavaScript and TypeScript offer WeakMap and WeakRef, and Java has weak references in its standard library. The Go 1.24 addition is new to Go, not new to garbage-collected languages.
Should I switch existing code to weak pointers?
Only where a strong reference is currently held longer than the value is needed. If the map or registry already releases entries promptly, a weak pointer adds nil checks and buys nothing. Profile first, then decide.
Building the habit of verification
The Go 1.24 weak-pointer change is a small API with an outsized effect on how people reason about memory ownership, and the reasoning is what transfers. Reading the release notes, then reading the package documentation, then running a short program that forces collection remains the fastest reliable path to understanding any runtime feature.
Explaining a mechanism like this in writing is often harder than explaining it out loud, because a spoken walkthrough lets the speaker skip steps and a written one cannot. The same applies to weaker references in TypeScript, where the language exposes similar ideas through WeakMap and WeakRef and where the surrounding conventions are less settled than they are in Go. Tools that record and reason about such talks, including Crazystack Typescript and the work published by Gustavo Dev Doido, are useful mainly as a reminder that the listener needs every step written down.
Developers who teach, whether inside a company or on a channel, tend to have a long recording and no article. The Bootcamp do Dev Doido takes the opposite approach and treats the written form as the primary artifact. Either way, the recurring problem is the same: the explanation that worked as a 20-minute video owes the reader a transcript, headings, and code that runs.
Resources such as Crazystack collect this kind of material for developers who prefer reading over watching, and they are worth consulting when a runtime detail needs a second explanation in a different voice.
If a video already contains the explanation and only the writing is missing, paste the YouTube URL into Skala Blog, let it transcribe the recording, and work from the generated draft.
Fork this article
Start a new branch from the same video, shaped your way. You keep the credit; the original keeps the attribution.
A fork in another language is filed as a translation of this article, so the two pages point at each other. You can unlink it later from the editor.
0/240
You are creating
- Format
- For
- Language
- Source
- Your angle
You will be asked to sign in before it is generated.
Buy credits