Introduction
Beyond Mutex and WaitGroup, the sync package offers specialized primitives: Once for one-time initialization and Cond for condition-based signaling between goroutines.
Key Concepts
- sync.Once: Ensures a function runs exactly once, even when called from multiple goroutines concurrently. All callers block until the function completes.
- sync.Cond: A condition variable that lets goroutines wait for and signal arbitrary conditions. Built on top of a Locker (usually a Mutex).
Real World Context
sync.Once is the standard way to implement lazy singleton initialization in Go — database connection pools, configuration loading, and logger setup all use this pattern. sync.Cond is less common (channels are usually preferred) but appears in low-level libraries like database drivers that need to signal when a connection becomes available.
Deep Dive
sync.Once
Ensures a function runs exactly once, even with concurrent calls:
govar once sync.Once var instance *Singleton func GetInstance() *Singleton { once.Do(func() { instance = &Singleton{} instance.init() }) return instance }
Use cases: lazy initialization, singleton pattern, one-time setup.
sync.Cond (Condition Variable)
For signaling between goroutines. Less common than channels but useful for broadcast notifications:
govar mu sync.Mutex cond := sync.NewCond(&mu) // Waiting goroutine cond.L.Lock() for !condition { cond.Wait() // Atomically unlocks and waits } // condition is true, we have the lock cond.L.Unlock() // Signaling goroutine cond.L.Lock() condition = true cond.Signal() // Wake one waiter // or cond.Broadcast() // Wake all waiters cond.L.Unlock()
Common Pitfalls
- Checking condition without a loop — Spurious wakeups can occur. Always use
for !condition { cond.Wait() }, neverif !condition. - Using Once.Do with a function that panics — If the function passed to
Dopanics,Oncestill considers it "done" and will not retry on subsequent calls.
Best Practices
- Prefer channels over sync.Cond — Channels are simpler and compose better. Use
Condonly when you need to broadcast to multiple waiters. - Use
sync.OnceValue(Go 1.21+) — For initializing and returning a value,sync.OnceValueis cleaner thanOncewith a package-level variable.
Summary
sync.Onceguarantees exactly-one execution, ideal for lazy initialization.sync.Condprovides Wait/Signal/Broadcast for condition-based coordination.- Always use a
forloop (notif) aroundcond.Wait()to handle spurious wakeups. - Prefer channels over
sync.Condunless you need broadcast semantics.
Code Examples
go
var loadConfigOnce sync.Once
var config *Config
func getConfig() *Config {
loadConfigOnce.Do(func() {
data, _ := os.ReadFile("config.json")
config = &Config{}
json.Unmarshal(data, config)
})
return config
}