Memory and Sync Primitives
Memory and Sync Primitives
Zero-allocation isn’t magic, it’s escape analysis plus object reuse plus the right sync primitive. This post puts sync.Pool, atomic, and Mutex/RWMutex side by side so gortex’s zero-alloc hot path lands later.
Escape analysis: stack or heap
Whether a variable lives on the stack or the heap is decided by the compiler, not by whether you wrote new or &. If it can prove the variable doesn’t outlive the function, it stays on the stack (no GC, reclaimed on return); the moment it’s referenced from outside — returned as a pointer, stored in an interface, captured by a closure, placed in something that itself escapes — it escapes to the heap.
func New() *T { return &T{} } // &T escapes to the heap: the pointer leaves the function
go build -gcflags='-m' prints the escape decision for every variable. The first step toward zero-allocation is cutting escapes you don’t need.
sync.Pool: reuse, ease GC
sync.Pool reuses short-lived objects, amortising allocation cost and GC pressure. Get takes one (calling New if the pool is empty); Put returns it:
var bufPool = sync.Pool{New: func() any { return new(bytes.Buffer) }}
b := bufPool.Get().(*bytes.Buffer)
b.Reset() // clean it on the way out/in
defer bufPool.Put(b)
Two things: reset before returning, so stale data doesn’t leak across uses; and never assume the pool keeps your object — GC clears a Pool. Gortex takes each request’s Context from a pool like this and returns it when done (B4, “The Zero-Allocation Secret”, gortex-zero-allocation-context).
atomic vs Mutex vs RWMutex
atomic: lock-free operations on a single integer or pointer (AddInt64,CompareAndSwap) — fastest, but it only guards that one value.sync.Mutex: guards a critical section, right when you mutate several fields together.sync.RWMutex: with many readers and few writers, readers run concurrently and writers take exclusive access; but it’s heavier than Mutex and is slower unless the read/write ratio is genuinely lopsided.
The rule in one line: a single counter → atomic, a block of state → Mutex, reads vastly outnumbering writes → RWMutex. Gortex’s sharded metrics use both atomic counters and per-shard locks (B5, “Sharded Metrics and a Circuit Breaker”).
From Effective Go: Data
Effective Go’s Data covers the allocation basics: new(T) returns a *T to zeroed memory; make is reserved for slices / maps / channels and initialises them; a composite literal &T{...} gives a value directly. Knowing these allocation paths is how you tell which values land on the heap and which can stay on the stack.
Takeaways
- Escape analysis decides stack vs heap; read it with
-gcflags='-m'. Returned pointers and interface values escape easily. sync.Poolreuses objects to flatten GC; reset before returning, and don’t assume an object survives.- Choosing a primitive:
atomic(single value) →Mutex(a block of state) →RWMutex(worth it only when reads dominate). - These three are the prerequisites for gortex’s zero-alloc hot path — B4’s Context pool and B5’s sharded metrics both build on them.
Further reading: the series goes on to dissect the gortex framework built on these ideas — source at yshengliao/gortex.
Outline by Sheng, drafted with Claude · Go 1.25 (latest release at the time) · compiled retroactively · part of the 2026-06-13 blog renovation, paint still drying.