Go

How do you implement a thread-safe singleton or lazy initialization in Go? Compare sync.Once, sync.OnceValue and package-level init.

Question 549MediumGo 1.22 to 1.25

Package-level var / init() runs eagerly and in a single thread before main, so it is thread-safe with no extra work. The cost is startup time and side effects (network, env reads) at import time, which also hurts tests. sync.Once runs a function exactly once. Every caller blocks until the first call finishes, and it is race-free under the memory model. sync.OnceValue / OnceValues (Go 1.21) wrap that pattern and return the cached result, including an error.

var getDB = sync.OnceValues(func() (*sql.DB, error) {
	return sql.Open("postgres", os.Getenv("DSN"))
})

func handler(w http.ResponseWriter, r *http.Request) {
	db, err := getDB() // first call initializes; later calls return the cached pair
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	_ = db
}

Gotchas: (1) Once caches failures as well. A transient error is stuck forever, so if you need retries, use a mutex plus a nil check. (2) If f panics, sync.Once counts as done and later calls do nothing. OnceValue instead panics again with the same value on every call. (3) Calling the same Once from inside f deadlocks. (4) Hand-written "double-checked locking" with a plain bool is a data race; use atomic.Bool or just Once. Interviewer angle: singletons are global state. Prefer creating the value in main and injecting it, and keep lazy globals for expensive, process-wide resources such as compiled regexps or templates.

More on Go Idioms, Design Patterns & Language Design

All 16 Go Idioms, Design Patterns & Language Design questions