# Memory and Sync Primitives

> Go zero-allocation needs escape analysis, sync.Pool reuse and the right primitive: atomic for one value, Mutex for a block of state, RWMutex if reads dominate.

Source: https://sheng.page/en/posts/go-memory-and-sync-primitives/ · Published: 2025-09-28 · Author: Sheng

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.

```go
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:

```go
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 (see [The Zero-Allocation Secret: Context Pool and smartParams](/en/posts/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 (see [Concurrent Counting and Resilience: Sharded Metrics and a Circuit Breaker](/en/posts/gortex-metrics-and-circuit-breaker/)).

## From Effective Go: Data

Effective Go's [Data](https://go.dev/doc/effective_go#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.


## Notes

- Escape analysis decides stack vs heap; read it with `-gcflags='-m'`. Returned pointers and interface values escape easily.
- `sync.Pool` reuses 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; the zero-allocation post's Context pool and the metrics post's sharded counters both build on them.

Further reading: the series goes on to dissect the gortex framework built on these ideas — source at [yshengliao/gortex](https://github.com/yshengliao/gortex).

---

> Ideas and technical judgement 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.