sync.Pool and Zero-Allocation Patterns
sync.Pool and Zero-Allocation Patterns
Overview
Reducing allocations is the highest-leverage GC optimization. sync.Pool reuses temporary objects; zero-allocation patterns avoid heap traffic entirely on hot paths.
X and production war stories hammer this constantly: pprof → mallocgc → fix allocs before spinning on GOGC.
Diagram: Pool get/put
sequence (top → bottom):
actors: goroutine, sync.Pool
goroutine --> sync.Pool : Get
sync.Pool --> goroutine : buffer
goroutine --> goroutine : use reslice
goroutine --> sync.Pool : Put
note: may drop on GC
sync.Pool
var bufPool = sync.Pool{
New: func() any {
b := make([]byte, 0, 32*1024)
return &b
},
}
func handle() {
bp := bufPool.Get().(*[]byte)
buf := (*bp)[:0]
defer func() {
if cap(buf) > 64*1024 { // don't retain huge buffers
return
}
*bp = buf
bufPool.Put(bp)
}()
// append into buf...
}Rules:
- No correctness dependency — pool may drop items on GC.
- Reset objects before Put (clear sensitive data).
- Cap retention — avoid pooling multi-MB buffers forever.
- Prefer pooling
*[]byteor structs with slices, not giant value copies.
Zero-Alloc Techniques
| Technique | Idea |
|---|---|
| Stack arrays | var a [256]byte then a[:n] if n bounded |
sync.Pool |
Reuse buffers across requests |
strconv.Append* |
Append to []byte without intermediate strings |
strings.Builder |
Single alloc for multi-part strings |
Avoid fmt in hot paths |
Prefer AppendInt / specialized encoders |
| Interface-free APIs | Avoid boxing in tight loops |
b := make([]byte, 0, 64)
b = strconv.AppendInt(b, 42, 10)
b = append(b, ' ')
b = append(b, "ok"...)Measuring
go test -bench=. -benchmem
go test -bench=. -memprofile=mem.out
go tool pprof mem.outtesting.AllocsPerRun(1000, func() { /* hot path */ })Anti-Patterns
- Pooling when objects are already tiny and short-lived (overhead > gain)
- Sharing pooled buffers across goroutines without ownership discipline
- Forgetting to reslice to zero length (
buf[:0])
Experiment
go test -bench=. -benchmempackage pool_test
import (
"strconv"
"sync"
"testing"
)
var pool = sync.Pool{New: func() any { b := make([]byte, 0, 64); return &b }}
func BenchmarkAllocFmt(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = strconv.Itoa(i) + "x"
}
}
func BenchmarkAppendPool(b *testing.B) {
for i := 0; i < b.N; i++ {
bp := pool.Get().(*[]byte)
buf := (*bp)[:0]
buf = strconv.AppendInt(buf, int64(i), 10)
buf = append(buf, 'x')
*bp = buf
pool.Put(bp)
}
}What to notice: Append + pool often wins on alloc/op versus string concat.
Try next: Profile a JSON encoder hot path; see if json.Encoder reuse or code generation helps more than pooling.