Explain sync.Once semantics, including panics, and the OnceFunc / OnceValue / OnceValues helpers.
Question 226MediumGo 1.22 to 1.25
once.Do(f) runs f exactly once, even with concurrent callers. All callers block until f returns, and the completion of f happens-before any Do returns, so callers see everything f wrote.
Gotchas:
- If
fpanics,Oncestill counts it as done. Later calls return immediately and the value was never initialized. - If
fcallsonce.Doon the same Once, it deadlocks. Docannot return an error, so people end up capturing variables in the closure.
Go 1.21 added generic helpers:
var loadConfig = sync.OnceValues(func() (*Config, error) {
return parseConfig("app.yaml")
})
func handler() {
cfg, err := loadConfig() // parsed once; same (cfg, err) every call
if err != nil {
log.Fatal(err)
}
_ = cfg
}
var initMetrics = sync.OnceFunc(func() { registerMetrics() })
OnceValue, OnceValues and OnceFunc differ from Once on panics: if f panics, every call re-panics with the same value, so failed initialization is never silently ignored.
Watch out: the error is cached permanently too. A transient failure can never be retried. If you need retries, write a mutex-guarded lazy initializer instead.
More on Concurrency Patterns & sync
- Q224What does this print, and why? (copying a lock)
- Q225What is the classic sync.WaitGroup bug, and what does wg.Go (Go 1.25) change?
- Q227When would you use sync.Cond, and why must Wait always be called in a loop?
- Q228How does sync.Pool work, how does it interact with the GC, and what are the common misuse pitfalls?
- Q229When should you use sync.Map instead of a map guarded by a Mutex?
- Q230What are typed atomics (atomic.Int64, atomic.Pointer[T]) and why prefer them over atomic.AddInt64 / atomic.Value?