sync.Pool: Thread-Local Caches, Victim Caches, and GC Drain
- Last updated
- Prerequisites:
- Go goroutines and memory
- Understanding of object reuse and GC
- go
- sync-pool
- object-pool
- gc
- allocation
- performance
Read at your depth
The practical view
sync.Pool reuses objects to reduce allocations: var pool = sync.Pool{ New: func() any { return &Buffer{} } }; b := pool.Get().(*Buffer); ...; pool.Put(b). Use it for frequently allocated, expensive-to-create objects (buffers, temporary structs). Critical rules: Get() may return nil if the pool is empty or was drained by GC (use New or handle nil); Put() returns an object for reuse; objects in the pool can be discarded at ANY time (especially after GC) — sync.Pool is NOT a persistent cache. It's for reducing allocation churn, not for guaranteeing object retention.
The same idea in other frameworks
N/A — Go runtime/library concern.N/A — Go concern.N/A — Go concern.N/A — Go concern.Legacy vs modern
Allocating per request vs reusing from a pool
Creating a new buffer per request drives allocation and GC; pooling reuses buffers with the same zero-value semantics via Reset.
func handle(r io.Reader) error {
buf := make([]byte, 32*1024) // ❌ OLD: new allocation per call
...
}var bufPool = sync.Pool{
New: func() any { b := make([]byte, 32*1024); return &b }, // ✅ NEW
}
func handle(r io.Reader) error {
buf := bufPool.Get().(*[]byte) // ✅ NEW: reuse
defer bufPool.Put(buf) // ✅ NEW: return for reuse
...
}Interview gotchas
Context
This tests the pool lifecycle semantics — interviewers want the GC-drain behavior and why Pool is only for reducing allocation churn, not for caching.
The mechanical answer
The GC drains sync.Pool: on every GC cycle, the runtime moves the current pool objects to a victim cache and clears the live pools (the victim cache survives one more cycle). This means a pooled object can disappear at any collection — Get() may find the pool empty and return nil (or call New). Therefore sync.Pool guarantees NOTHING about retention: it is a best-effort reuse cache, designed purely to cut allocation rate for short-lived, frequently-created objects. It is the wrong tool for a persistent bounded cache (that's a purpose-built cache with eviction, or just holding references). Additionally, the pooled objects' state is whatever you left: you must Reset() them after Get if the previous user dirtied them, because Get returns the same object to different goroutines.
Trap
The naive answer is 'sync.Pool caches objects so they're always there'. GC can drain it at any moment — Get returning nil is normal. Another trap: using pooled objects without resetting — they carry the previous user's state, causing cross-request data leaks. Candidates who describe the GC drain and the reset requirement show real understanding.
Context
This probes the internal design — interviewers want the per-P local pools (contention avoidance) and the victim-cache mechanism (why recently-freed objects survive one GC).
The mechanical answer
sync.Pool avoids global contention by giving each scheduler P (processor) its own poolLocal: Get/Put first touch the P's private slot and its shared poolChain, so the overwhelmingly common case (same goroutine, same P) does no cross-P locking. When a P's pool is empty, it steals from other P's shared chains, which uses a lock-free-ish deque. The victim cache addresses the GC timing problem: when GC drains the pool, rather than discarding everything instantly, the runtime moves the current generation into a victim list; Get checks the victim cache too, so objects freed right before/at a GC can still be reused for one more cycle — smoothing the post-GC allocation spike. This design makes Pool fast in the hot path and resilient to GC.
Trap
The naive answer is 'sync.Pool is a single shared pool with a mutex'. It's sharded per-P to avoid contention. Another trap: assuming pooled objects are shared across goroutines safely — they are NOT; Get gives exclusive ownership to one caller, who must Put it back without sharing. Candidates who describe per-P sharding and the victim generation show real depth.