Skip to content

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.Do and the lazy-singleton pattern (plus OnceFunc/OnceValue)
  • sync.Pool Get/Put, the New factory, and GC-driven eviction
  • Once-per-key memoisation

📖 Reading / Sources

📝 Notes

  • [[once]] (sync.Once): once.Do(f) runs f exactly once, ever. The first caller runs f; all other callers block until f returns, then proceed, guaranteed to observe f's writes. The classic thread-safe lazy singleton.
  • Once.Do counts the call as done even if f panics — it will not retry. So Once is 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 bare Once + package var.
  • Once has no reset. For "once per key", combine sync.Map.LoadOrStore (one *entry per key) with a per-key sync.Once. → [[memoization]]
  • [[pool]] (sync.Pool): a free list of reusable values. Get() returns a pooled value or calls New if empty; Put(x) returns x for reuse. It is typed any, so Get needs a type assertion.
  • A sync.Pool may 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 Reset a pooled object before reuse (e.g. buf.Reset()), and copy out any data before Put — after Put, another goroutine may overwrite the underlying memory (the buf.Bytes() aliasing trap).
  • Pools shine for short-lived, frequently-allocated objects like *bytes.Buffer in 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) after pool.Put(buf) — another goroutine reused the buffer and corrupted my "result". Copied to a string before Put.
  • Expected Once to retry after the init function returned an error; it didn't (the slot was already consumed). Restructured to not use Once for fallible init.

❓ Open Questions

  • How aggressively does the runtime drain a sync.Pool across 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)

  1. Q: If f passed to once.Do(f) panics, will a later once.Do(f) re-run f?
    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 -benchmem numbers 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)