How do you implement a thread-safe singleton or lazy initialization in Go? Compare sync.Once, sync.OnceValue and package-level init.
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
- Q548Explain the functional options pattern. Compare it with a config struct and a builder. How do you validate options and return errors?
- Q550How do you model enums in Go with iota? How do you add String() via go generate stringer, validate values, and handle the zero value?
- Q551How do variadic functions work? What is the aliasing trap when passing s... to a function that modifies or appends to its argument?
- Q552Explain closures in Go: what they capture, when captured variables escape to the heap, and how to build a generator or middleware with them.
- Q553Why does Go have no inheritance, function overloading, default arguments, ternary operator or exceptions? How do idiomatic Go programs work around each?
- Q554Can you compare functions in Go? Why is a func only comparable to nil, and how does that affect using funcs as map keys?