What happens if the function passed to sync.Once.Do panics? How do sync.OnceFunc and sync.OnceValue differ?
Question 329HardGo 1.22 to 1.25
With sync.Once, a panic in f still counts as "done". Do marks the Once as complete, the panic propagates to the first caller, and every later call returns immediately without running f. The protected state stays half-initialized for good, which is a quiet bug if someone recovers from the first panic.
var once sync.Once
var cfg *Config
func load() {
defer func() { recover() }() // swallow, for the demo
once.Do(func() { panic("bad config") })
}
func main() {
load()
once.Do(func() { cfg = &Config{} }) // NOT run: Once is already done
fmt.Println(cfg == nil) // true
get := sync.OnceValue(func() int { panic("boom") }) // Go 1.21
for range 2 {
func() {
defer func() { fmt.Println("recovered:", recover()) }()
get()
}()
}
// recovered: boom
// recovered: boom (same panic value re-raised on every call)
}
sync.OnceFunc, OnceValue and OnceValues (Go 1.21) also run f only once, but if f panics, the returned function panics with the same value on every call, so the failure can't be forgotten. For fallible initialization, prefer sync.OnceValues(func() (T, error)). It caches the error and returns it to every caller, which is usually better than panicking at all.
More on Error Handling & panics
- Q327What does this print? (Which side's
Ismethod gets called) - Q328What happens when a deferred function panics while the goroutine is already panicking?
- Q330What are the performance costs of errors,
deferandpanic/recoverin hot paths?