Day 066 — sync.Once & sync.Pool¶
Month 3 · Week 2 · ⬅ Day 065 · Day 067 ➡ · Journal index
🎯 Learning Objective¶
Run initialisation exactly once across goroutines with sync.Once, and cut allocations by recycling transient objects with sync.Pool — knowing the sharp edges of each.
📚 Topics¶
sync.Once.Doand the lazy-singleton pattern (plusOnceFunc/OnceValue)sync.PoolGet/Put, theNewfactory, and GC-driven eviction- Once-per-key memoisation
📖 Reading / Sources¶
-
sync.Oncedocs -
sync.Pooldocs -
sync.OnceValuedocs (Go 1.21+)
📝 Notes¶
- [[once]] (
sync.Once):once.Do(f)runsfexactly once, ever. The first caller runsf; all other callers block untilfreturns, then proceed, guaranteed to observef's writes. The classic thread-safe lazy singleton. Once.Docounts the call as done even iffpanics — it will not retry. SoOnceis wrong when init can fail and you want a retry; for that, handle the error and reset state yourself.- Go 1.21 added
sync.OnceFunc/OnceValue/OnceValues: helpers that wrap a function so it runs once and caches its result — less boilerplate than a bareOnce+ package var. Oncehas no reset. For "once per key", combinesync.Map.LoadOrStore(one*entryper key) with a per-keysync.Once. → [[memoization]]- [[pool]] (
sync.Pool): a free list of reusable values.Get()returns a pooled value or callsNewif empty;Put(x)returnsxfor reuse. It is typedany, soGetneeds a type assertion. - A
sync.Poolmay be emptied at any GC — it is an allocation optimisation, not a cache for things you must keep. Never store state you can't reconstruct. - Always
Reseta pooled object before reuse (e.g.buf.Reset()), and copy out any data beforePut— afterPut, another goroutine may overwrite the underlying memory (thebuf.Bytes()aliasing trap). - Pools shine for short-lived, frequently-allocated objects like
*bytes.Bufferin hot encode/decode paths; benchmark before adding one — it is not free.
💻 Code Examples¶
var once sync.Once
var cfg *config
func loadConfig() *config {
once.Do(func() { cfg = &config{env: "production"} }) // runs once, ever
return cfg
}
// Go 1.21+ equivalent, no package-level var needed:
var loadConfig2 = sync.OnceValue(func() *config { return &config{env: "production"} })
Full code:
examples/month-03/once-pool/main.go· Run:go run ./examples/month-03/once-pool
🏋️ Exercises / Practice¶
| Exercise | Status | Link |
|---|---|---|
| Once-per-key memoising cache | ✅ | exercises/month-03/week-2/oncecache |
🐛 Mistakes Made¶
- Returned
buf.Bytes()(a view into the buffer) afterpool.Put(buf)— another goroutine reused the buffer and corrupted my "result". Copied to a string beforePut. - Expected
Onceto retry after the init function returned an error; it didn't (the slot was already consumed). Restructured to not useOncefor fallible init.
❓ Open Questions¶
- How aggressively does the runtime drain a
sync.Poolacross GC cycles? (Per the docs it may drop all items; victim cache softens but doesn't guarantee survival across two GCs.)
🧠 Active Recall (answer without looking)¶
- Q: If
fpassed toonce.Do(f)panics, will a lateronce.Do(f)re-runf?A
No. Do records the call as completed before/around running f; a panic does not reset it, so f never runs again. Once is unsuitable for retry-on-failure init.
2. Q: Why must you copy data out of a pooled *bytes.Buffer before calling Put? A
After Put, the buffer is available for reuse; another goroutine's Get can Reset and overwrite its backing array, corrupting any slice/Bytes() view you still hold. Copy (e.g. buf.String()) first.
🪶 Feynman Reflection¶
sync.Once is a turnstile that lets the first person through to flip a switch while everyone else waits, then opens for all — the switch is flipped exactly once. sync.Pool is a tray of scratch paper: grab a sheet, use it, wipe it, return it — but the janitor (GC) may clear the tray anytime, so never write anything you need to keep on it.
🕳️ Knowledge Gaps¶
- Real allocation impact of
sync.Pool— needs-benchmemnumbers before I trust the intuition.
✅ Summary¶
I can do thread-safe lazy init with sync.Once/OnceValue, memoise once-per-key with sync.Map + Once, and recycle transient objects with sync.Pool while avoiding its aliasing and eviction traps.
⏭️ Next Steps / Prep for Tomorrow¶
- Day 067:
sync/atomic— lock-free counters, flags, and the compare-and-swap loop.
| Time spent | Difficulty | Confidence |
|---|---|---|
| 90 min | 🟦🟦⬜⬜⬜ | 🟦🟦🟦⬜⬜ |
Suggested commit: feat(examples): sync.Once lazy init and sync.Pool reuse (day 066)