Day 157 — sync.Pool & optimization¶
Month 6 · Week 3 · ⬅ Day 156 · Day 158 ➡ · Journal index
🎯 Learning Objective¶
Use sync.Pool to recycle short-lived hot-path objects, understand its caveats (not zeroed, GC may drain it), and confirm the win with -benchmem before adopting it.
📚 Topics¶
sync.PoolNew/Get/Put; reuse vs the GC- Reset-after-Get discipline; safe vs unsafe pooled returns
- Benchmark-driven optimization; the optimization checklist
📖 Reading / Sources¶
-
sync.Poolpackage docs - Go blog — A Guide to the Go Garbage Collector
-
testing— benchmarks &B.ReportAllocs - Std-lib use as a model:
encoding/jsonuses async.Poolof encoders
📝 Notes¶
sync.Poolis a free-list of temporary objects, scoped per-P (per logical processor) soGet/Putare mostly lock-free.Getreturns a pooled object or callsNewif empty;Putoffers one back for reuse → [[sync-pool]].- It is an optimization, not a cache: the GC may drain the pool. Since Go 1.13 a victim cache keeps objects across one GC, but never rely on an object surviving —
Newmust always be able to recreate one cheaply. - A pooled object is NOT zeroed.
Gethands back whatever the last user left in it. AlwaysReset()(for*bytes.Buffer) or overwrite before use, or you leak stale data into the next request → a real security/correctness bug → [[pool-reset]]. - Pool pointers, not values.
Puttakesany; putting a non-pointer boxes it and allocates, defeating the purpose. Store*bytes.Buffer,*[]byte, etc. - Don't keep a reference after
Put. Once returned, another goroutine mayGetthe same object concurrently. Putting then continuing to write is a data race. - Beware unbounded growth from outliers. If you pool sized buffers, a rare huge buffer Put back gets retained and reused. Drop oversized objects before
Put(if cap(b) > max { return }), asfmt/httpdo internally. - The right object: short-lived, frequently allocated, on a measured hot path (per-request buffers, JSON encoders, gzip writers, scratch slices). The wrong object: long-lived state, connections (use a real pool), or anything where correctness depends on it persisting.
- Optimization order: profile first ([[day-155]]) → cut allocations via escape-analysis fixes ([[day-156]]) → only then pool what you can't eliminate. Re-measure with
go test -bench=. -benchmemand compareallocs/op/B/op(usebenchstatacross runs).
💻 Code Examples¶
var bufPool = sync.Pool{New: func() any { return new(bytes.Buffer) }}
func render(name string) string {
b := bufPool.Get().(*bytes.Buffer)
b.Reset() // CRITICAL: pooled objects keep old contents
defer bufPool.Put(b)
b.WriteString("Hello, ")
b.WriteString(name)
return b.String() // String() copies, so reuse after Put is safe
}
Full code:
examples/month-06/syncpool/main.go· Run:go run ./examples/month-06/syncpool
🏋️ Exercises / Practice¶
| Exercise | Status | Link |
|---|---|---|
| Allocs: pooled vs fresh 64 KiB buffer | ✅ | examples/month-06/syncpool |
Safe sync.Pool wrapper (Reset, race-clean) |
✅ | exercises/month-06/week-3/bufpool |
🐛 Mistakes Made¶
- Forgot
b.Reset()afterGet→ the previous request's bytes leaked into the next response. Reset (or overwrite) is mandatory. Put-ed abytes.Buffer(value) instead of*bytes.Buffer→ boxing allocated on every Put, erasing the benefit.
❓ Open Questions¶
- How aggressively does the GC drain pools under memory pressure, and how does
GOGC/GOMEMLIMITinteract with pool retention?
🧠 Active Recall (answer without looking)¶
- Q: Why must you
Reset/overwrite an object returned bypool.Get()?A
sync.Pool does not zero objects; Get returns one exactly as the previous user left it. Without resetting, stale data (another request's bytes) leaks into the new use — a correctness and potential security bug.
2. Q: Why is sync.Pool described as "not a cache," and what does that imply for New? A
The garbage collector may drain the pool at any time, so you can't depend on an object still being there. Therefore New must always be able to construct a fresh object cheaply — the pool only opportunistically reduces allocations, it doesn't guarantee them.
🪶 Feynman Reflection¶
sync.Pool is a bin of clean-ish rags by the workbench: instead of buying a new rag every time (allocating) and tossing it (GC), you grab one from the bin, use it, and drop it back. But the bin isn't sterile — you must wipe a rag before trusting it (Reset), the janitor (GC) may empty the bin overnight, and you must let go of a rag the moment you return it.
🕳️ Knowledge Gaps¶
benchstatworkflow for statistically comparing before/after optimizations.- Per-P internals and how pool behavior scales with
GOMAXPROCS.
✅ Summary¶
I can recycle hot-path objects with sync.Pool safely — pointers only, Reset after Get, drop outliers, never reference after Put — and I treat it as the last optimization step after profiling and allocation-cutting, validated with -benchmem.
⏭️ Next Steps / Prep for Tomorrow¶
- Day 158: prove the optimizations under real traffic — load testing with k6 and vegeta.
| Time spent | Difficulty | Confidence |
|---|---|---|
| 90 min | 🟦🟦⬜⬜⬜ | 🟦🟦🟦⬜⬜ |
Suggested commit: docs(journal): sync.Pool & optimization (day 157)