← Back to list

Weak Pointers Are in Go Now. Most Teams Shouldn’t Use Them.

The use cases are real but narrow. Outside cache invalidation and canonicalization, they add lifecycle complexity that will outlive the…

syarif in Stackademic · 2026-07-11 07:30 · 10 claps · 7.1 min read paywalled
#programming #golang #software-development #backend-development #software-engineering
Open on Medium ↗
Wiki topics: 💻 · Programming 🌐 · Web Development

Weak Pointers Are in Go Now. Most Teams Shouldn’t Use Them.

The use cases are real but narrow. Outside cache invalidation and canonicalization, they add lifecycle complexity that will outlive the team that introduced them.

Photo by Pedro Domingos on Unsplash

Photo by Pedro Domingos on Unsplash

I’ve seen it happen three times now.

A team reads the Go 1.24 release notes, gets excited about weak.Pointer[T], and sprinkles it into a codebase that was doing just fine without it. Six months later, the person who introduced it has moved on, and the remaining engineers are debugging nil dereferences that only reproduce under GC pressure in production.

Go’s weak package isn't the problem. The problem is the gap between "this is technically useful" and "my team should adopt this."

Let me explain.

What Go 1.24 Actually Shipped

Go 1.24 introduced two complementary primitives in February 2025:

1. weak.Pointer[T] — A reference that does not prevent garbage collection.

2. runtime.AddCleanup — A callback triggered when an object is collected (replacing the notoriously broken runtime.SetFinalizer).

The API is minimal — deliberately so:

package weak

// Make creates a weak pointer from a strong pointer.
func Make[T any](<ptr *T>) Pointer[T]

// Value returns the original pointer if the object is still alive.
// Returns nil if the GC has already collected it.
func (p Pointer[T]) Value() *T

That’s it. Two functions. No configuration, no options, no builder pattern. This is classic Go minimalism — and it’s the first sign that the Go team intended this for a very specific audience.

The Two Legitimate Use Cases

Let’s be clear: weak pointers solve real problems. But those problems live in a narrow band of system-level programming.

1. Canonicalization Maps (Interning)

The canonical use case — pun intended — is deduplication.

Imagine a DNS resolver that processes millions of queries. Many share identical domain strings. Without interning, you’re allocating "google.com" a million times:

// ❌ Without canonicalization: millions of duplicate strings
func resolve(domain string) *Record {
    // Every call allocates a new string, even for "google.com"
    return lookup(domain)
}

With weak.Pointer, you build a canonicalization map that deduplicates without pinning objects in memory forever:

// ✅ With canonicalization: one "google.com" in memory
type Interner struct {
    mu    sync.Mutex
    cache map[string]weak.Pointer[string]
}

func (i *Interner) Intern(s string) *string {
    i.mu.Lock()
    defer i.mu.Unlock()

    if wp, ok := i.cache[s]; ok {
        if v := wp.Value(); v != nil {
            return v // Return the canonical instance
        }
        delete(i.cache, s) // Stale entry, clean it up
    }

    // Create new canonical instance
    canonical := new(string)
    *canonical = s
    i.cache[s] = weak.Make(canonical)
    return canonical
}

his is exactly the pattern that powers Go’s own unique package (introduced in Go 1.23). Under the hood, unique.Handle[T] uses weak pointers to manage its internal pool. When no Handle references a value, the GC reclaims it.

This is a legitimate, well-understood use case. If you’re building a compiler, a DNS server, or a protocol parser that processes millions of structurally identical values — this is for you.

2. GC-Aware Caches

The second valid use case is caching where you want the GC — not a timer, not an LRU — to decide when entries are evicted:

type WeakCache[K comparable, V any] struct {
    mu    sync.Mutex
    items map[K]weak.Pointer[V]
}

func (c *WeakCache[K, V]) Get(key K) (*V, bool) {
    c.mu.Lock()
    defer c.mu.Unlock()

    wp, exists := c.items[key]
    if !exists {
        return nil, false
    }

    val := wp.Value()
    if val == nil {
        delete(c.items, key) // GC already collected it
        return nil, false
    }

    return val, true
}

This pattern is appropriate when:

  • ✅ You have a large working set with unpredictable access patterns
  • ✅ Memory pressure is the right eviction signal (not time or frequency)
  • ✅ A cache miss is cheap to recover from

Notice how specific those conditions are. Most caches don’t meet all three.

Why Most Teams Should Stay Away

Here’s where I get opinionated. The use cases above are real — but they describe maybe 5% of Go codebases. The other 95% will encounter one or more of these pitfalls:

Pitfall 1: The “Nil Surprise”

Every call to Value() is a coin flip. The object might exist. It might not. This is the design — but it's also a footgun for anyone used to Go's "if it compiles, it probably works" ethos.

// This looks correct. It is not.
func process(wp weak.Pointer[Config]) {
    cfg := wp.Value()

    // 🚨 cfg might be nil here. The GC doesn't care about your deadline.
    fmt.Println(cfg.DatabaseURL)
}

Every consumer of a weak pointer must handle the nil case. Not “should handle” — must handle. And you’ll find out they didn’t in production, when GC pressure is high enough to trigger collection between the Value() call and the field access.

// ✅ The correct pattern — every single time
func process(wp weak.Pointer[Config]) {
    cfg := wp.Value()
    if cfg == nil {
        // What now? Rebuild? Return an error? Panic?
        // This is the lifecycle question you've just inherited.
        return
    }
    fmt.Println(cfg.DatabaseURL)
}

Pitfall 2: The Map That Never Shrinks

Here’s the dirty secret of weak pointer caches: the weak pointers get collected, but the map entries don’t.

cache := make(map[string]weak.Pointer[ExpensiveObject])

// After 10,000 insertions and 9,000 collections:
// - 1,000 live weak pointers ✅
// - 9,000 map entries with nil-returning weak pointers 🚨
// - The map itself has NOT shrunk

You need a cleanup mechanism. The “correct” approach uses runtime.AddCleanup:

func (c *WeakCache[K, V]) Set(key K, val *V) {
    c.mu.Lock()
    c.items[key] = weak.Make(val)
    c.mu.Unlock()

    // Register cleanup to remove the map entry when val is collected
    runtime.AddCleanup(val, func(k K) {
        c.mu.Lock()
        delete(c.items, k)
        c.mu.Unlock()
    }, key)
}

Now you’re managing the GC’s relationship to your data structures. You’re writing code that runs on a separate goroutine, at an unpredictable time, mutating shared state. This is exactly the kind of “spooky action at a distance” that Go’s design philosophy was built to avoid.

Pitfall 3: Testing Becomes Non-Deterministic

How do you test a function that depends on GC timing?

func TestWeakCache(t *testing.T) {
    cache := NewWeakCache[string, Data]()

    val := &Data{Name: "test"}
    cache.Set("key", val)

    val = nil           // Remove strong reference
    runtime.GC()        // 🤞 Hope the GC collects it

    result, ok := cache.Get("key")
    // Is ok true or false? It depends on GC behavior,
    // which varies by Go version, OS, and memory pressure.
}

You’ve just introduced non-determinism into your test suite. runtime.GC() is a suggestion, not a command. Your tests might pass locally and fail in CI, or vice versa.

Pitfall 4: “Use After Free” — Go Edition

Go doesn’t have use-after-free in the C sense. But weak pointers introduce a conceptual equivalent:

func getConfig(wp weak.Pointer[Config]) *Config {
    cfg := wp.Value()
    if cfg != nil {
        return cfg
    }
    // The config was collected. Now what?
    // Recreate it? With what parameters?
    // Fall back to defaults? Which defaults?
    // Return an error? The caller isn't expecting one.

    return nil // 🚨 Pushes the problem upstream
}

The moment you use a weak pointer, you’ve accepted that any reference to the object might become invalid at any time. You haven’t eliminated lifecycle management — you’ve delegated it to the garbage collector and scattered the fallback logic across every call site.

The Decision Matrix

Before reaching for weak.Pointer, ask yourself:

If you answered “no” to any question — and be honest — you’re better off with a standard map, a sync.Pool, or a time-based cache like groupcache or ristretto.

What To Use Instead

For the vast majority of Go applications, these alternatives are simpler, more testable, and more maintainable:

For caching → Use a time-based or size-bounded cache.

// Simple TTL cache — no GC coupling, deterministic behavior
type TTLCache[K comparable, V any] struct {
    mu    sync.RWMutex
    items map[K]cacheEntry[V]
}

type cacheEntry[V any] struct {
    value     V
    expiresAt time.Time
}

func (c *TTLCache[K, V]) Get(key K) (V, bool) {
    c.mu.RLock()
    defer c.mu.RUnlock()

    entry, ok := c.items[key]
    if !ok || time.Now().After(entry.expiresAt) {
        var zero V
        return zero, false
    }
    return entry.value, true
}

For object pooling → Use sync.Pool.

var bufPool = sync.Pool{
    New: func() any {
        return new(bytes.Buffer)
    },
}

// GC-integrated already. No weak pointers needed.
buf := bufPool.Get().(*bytes.Buffer)
defer bufPool.Put(buf)

For deduplication → Use unique.Handle[T] (Go 1.23+).

// The standard library already did the hard work for you
handle := unique.Make("repeated-value")

// Canonical comparison — fast pointer equality
if handle1 == handle2 {
    // Same underlying value, guaranteed
}

The unique package is the Go team saying: "We built weak pointers so YOU don't have to use them directly."

When It’s Actually Worth It

I don’t want to be absolutist. Here are the profiles of teams that should consider weak.Pointer:

✅ Standard library and runtime contributors. You’re building the infrastructure that everyone else uses. Weak pointers are your tool.

✅ Database driver authors. Connection metadata caching where memory pressure should drive eviction — not arbitrary timeouts.

✅ Language tooling (compilers, analyzers). Interning of AST nodes, type information, or symbol tables where millions of structurally identical objects exist.

✅ High-throughput network services. DNS resolvers, proxy servers, or protocol parsers processing millions of identical values per second.

Notice the pattern: these are all infrastructure-level concerns. If you’re building a REST API, a CLI tool, or a microservice — weak pointers will add complexity with no measurable benefit.

The Lifecycle Tax

Here’s the mental model I use:

Every weak.Pointer in your codebase is a lifecycle tax. It says: "At this point, the object might not exist, and you need a plan for that."

In languages like Rust, the ownership model makes this tax explicit and compiler-enforced. In Java, WeakReference has been around for decades, and the ecosystem has collectively learned to avoid it except in very specific patterns (like WeakHashMap for metadata).

Go chose a different path. It deferred weak pointers for 15 years — not because they’re technically hard, but because the Go team understands that most code is maintained by humans who rotate through teams, who inherit codebases they didn’t write, who need to reason about behavior without a PhD in garbage collection.

weak.Pointer exists for the cases where nothing else will do. For everything else, there's sync.Pool, unique.Handle, and a well-placed defer.

  • Go 1.24 added weak.Pointer[T] and runtime.AddCleanup — real, useful primitives.
  • Legitimate uses: canonicalization maps, GC-driven caches, infrastructure-level interning.
  • Anti-patterns: general caching, flow control, lazy lifecycle management.
  • Every Value() call is a nil check you can't skip. Every map storing weak pointers needs a cleanup strategy.
  • For 95% of Go teams: use unique.Handle[T], sync.Pool, or a time-based cache instead.
  • Weak pointers are a tool for library authors, not application developers.

메타데이터
post_id
efe9e584f313
slug
weak-pointers-are-in-go-now-most-teams-shouldnt-use-them-efe9e584f313
url
https://blog.stackademic.com/weak-pointers-are-in-go-now-most-teams-shouldnt-use-them-efe9e584f313
canonical_url
https://blog.stackademic.com/weak-pointers-are-in-go-now-most-teams-shouldnt-use-them-efe9e584f313
author_url
https://medium.com/@elsyarifx
status
ok
fetched_at
2026-07-13 08:05:13