keeps the object alive ◦ As long as there's a strong reference, GC won't collect the object ◦ Regular Go pointers and variable references fall into this category Weak Reference (weak pointer) ◦ When an object is only referenced by weak references, GC considers it eligible for collection ◦ Objects accessible only through weak references can be freed by GC at any time ◦ When an object is collected, the corresponding weak pointer automatically becomes nil
references: Ignored in reachability analysis (objects don't survive with only weak references) 2. Value() invalidation: When the target becomes eligible for collection, Value() method may return nil 3. Non-deterministic timing: When it starts returning nil depends on GC
automatically collected by GC By using weak pointers, data is automatically GC'd when no longer needed cache.Store(key, weak.Make(data)) if wp := cache.Load(key); wp.Value() != nil { return wp.Value() // Still alive }
underlying the unique package ip.z = unique.Make(zoneDetail{zone: "eth0"}) // Same value → same Handle, fast comparison • The purpose of canonicalization (interning) is to consolidate multiple identical values into a single canonical copy • This leads to memory savings
between multiple objects • When a listener becomes unnecessary elsewhere (no strong references remain), it's automatically removed from the bus • Efficiently manages memory whose lifetime is tied to another object's lifetime type EventBus struct { listeners []weak.Pointer[Listener] }
implement canonicalization in the unique package ◦ Identical values share the same memory ◦ Mechanism that doesn't prevent garbage collection of unused entries • Existing workarounds (borrowed from go4.org/intern) had problems
• Depends on unsafe operations • Relies on unpublished collector behavior // go4.org/intern implementation (using unsafe) type Value struct { cmpVal interface{} resurrected bool // Resurrection flag } var ( mu sync.Mutex valMap = map[key]uintptr{} // Hide *Value as uintptr ) func get(k key) *Value { if addr, ok := valMap[k]; ok { v = (*Value)((unsafe.Pointer)(addr)) // unsafe conversion v.resurrected = true // Mark as resurrected } // ... }
The unsafe techniques used in go4.org/intern are incredibly subtle and have some seriously sharp edges. There have been several different iterations of this package written by some of the most experienced Go programmers on the planet that still managed to have subtle defects and data races ref: https://mdlayher.com/blog/unsafe-string-interning-in-go/
implementation of unique package and it was compelling enough that it led to this proposal." — issue #67552 • The need for weak references became clear during unique package implementation • For efficient management of canonicalization maps • The weak reference implementation was simple and compelling, leading to this proposal
◦ 2 GC cycles required: First for resurrection check, second for actual collection ◦ Permanent leak with circular references: Resurrected objects in cycles never get collected ◦ Race conditions: Objects retrieved from weak references may resurrect mid-operation, causing inconsistent internal state
races: Safe even if resurrection occurs since weak ref is already nil • Implementation simplicity: No complex resurrection tracking logic needed • Proven in other languages: C# default, Java standard - proven sufficient in both • Performance: Completes in 1 GC cycle, no additional resurrection checks needed
Reference? ◦ A pointer pointing to already freed memory ◦ Accessing it can cause crashes or unpredictable behavior ◦ With weak references, the referenced memory can be collected at any time, creating danger of dangling references with direct access like regular pointers
Reference? ◦ A pointer pointing to already freed memory ◦ Accessing it can cause crashes or unpredictable behavior ◦ With weak references, the referenced memory can be collected at any time, creating danger of dangling references with direct access like regular pointers
freed memory. They first read a runtime-managed handle; once that handle is zero, `Value()` simply returns `nil`. • Indirection via word-sized handle: Weak pointers share a word-sized (8-byte) handle instead of pointing to the object itself. • Atomic, batched invalidation: All weak pointers to the same `(object, offset)` share the handle, so a single atomic zero invalidates them simultaneously
Weak references don't keep objects alive 2. Mark Termination └─ Value() blocks conversions until the relevant span is swept. 3. Sweep └─The runtime zeroes the shared handle for unreachable objects (this invalidates all weak pointers to those objects). 4. Observation └─ After sweep completes, Value() returns nil. If Value() is called right after mark termination, it will first force sweep of the relevant span, then read the handle (→ nil).
false) { // If GC is running, mark the handle itself to keep it alive if gcphase != _GCoff { scanblock(uintptr(unsafe.Pointer(&s.handle)), goarch.PtrSize, &oneptrmask[0], gcw, nil) } return handle } // ... handle race condition ... } Result: One `atomic.Uintptr` per target, shared by all weak pointers
package) // From src/weak/pointer.go type Pointer[T any] struct { _ [0]*T u unsafe.Pointer // ← Points to the indirection handle } func (p Pointer[T]) Value() *T { if p.u == nil { return nil // ← Zero value or nil pointer case } // Delegate to runtime for safe weak→strong conversion return (*T)(runtime_makeStrongFromWeak(p.u)) }
package) // From runtime/mheap.go func internal_weak_runtime_makeStrongFromWeak(u unsafe.Pointer) unsafe.Pointer { handle := (*atomic.Uintptr)(u) // ← Cast to handle type mp := acquirem() if work.strongFromWeak.block { releasem(mp) mp = gcParkStrongFromWeak() // ← Wait for mark to complete } p := handle.Load() // ← Atomic read from shared indirection if p == 0 { releasem(mp) return nil // ← Object was collected }
Handle immortal objects... return unsafe.Pointer(p) } // CRITICAL: Ensure span is swept before trusting the pointer span.ensureSwept() // ← Synchronize with GC ptr := unsafe.Pointer(handle.Load()) // Re-read after sweep // If GC is running, mark the target if gcphase != _GCoff { shade(uintptr(ptr)) // ← Keep target alive this cycle } releasem(mp) return ptr } Three safety mechanisms: Block during mark → Ensure sweep → Mark if necessary
only `Make` and `Value` • Clear guidance: Document when to use and when to avoid (see [GC Guide](https://golang.org/doc/gc-guide#Finalizers _cleanups_and_weak_pointers)) • Learn from other languages: Reference patterns from Java, C#, Python, etc.
Type Tracing Generational tracing (common, varies) Generational tracing GC Weak ref representation weak.Pointer[T].Va lue() WeakReference<T>.g et() + Queue WeakReference.Targ et Invalidation timing Unreachable → may become nil at finalizer queue Unreachable → nil before finalizer queue Unreachable → null when finalizable Short: null when finalizable Long: null after finalizer Resurrection possibility Prevented with "short" weak refs Prevented with weak refs Possible in finalizers
:= weak.Make(&obj) // GC determines obj unreachable → wp.Value() may return nil // C#: Choice of short or long weak references WeakReference shortWr = new WeakReference(obj); // null when finalizable WeakReference longWr = new WeakReference(obj, true); // tracks resurrection // Finalizer can resurrect; long ref still valid after resurrection Go's design choice: Weak references that truly prevent resurrection
Registers a lightweight, GC-coupled cleanup that runs at most once for a target’s lifetime • Does not receive the target object (you pass only minimal context like keys/IDs) • Does not keep the target alive and cannot resurrect it How it differs from finalizers (design intent) • Focused on cleaning up external/auxiliary structures (e.g., removing from a map) • Cleanup does not receive the target object, preventing resurrection • Intended to be short and non-blocking (avoid heavy I/O or long-held locks)
target in closures (pass only necessary info like a key) 2. Keep cleanup short and non-blocking 3. Leverage that weak pointers are comparable; confirm identity before deletion // Safely delete only the entry associated with a value that became unreachable runtime.AddCleanup(value, func(k K) { if v, ok := c.m.Load(k); ok { if v.(weak.Pointer[V]) == wp { // Weak pointers remain comparable c.m.Delete(k) } } }, key) Common use cases • Auto-deletion of cache/side-map entries • Removing external handles or registered listeners, coupled to the target's lifetime
no longer needed are automatically deleted • Lifetime coupling: Properly manages lifetimes of related objects • GC hints: Notifies GC that "it's safe to delete this resource"
small sizes that don't contain pointers type Tiny struct { a, b int32 // Very small & doesn't contain pointers } // Example workaround: Increase size type NotTiny struct { a, b int32 _ [9]byte // Padding to 17+ bytes }
data naturally freed • No need for unsafe workarounds from go4.org/intern 2. Simple and Safe API • Create with Make(), retrieve with Value() (nil check required) • Reliable cleanup with runtime.AddCleanup 3. Practical Applications • Cache, string interning (unique), event listener management