Go

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 f panics, Once still counts it as done. Later calls return immediately and the value was never initialized.
  • If f calls once.Do on the same Once, it deadlocks.
  • Do cannot 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

All 38 Concurrency Patterns & sync questions