sync.Once Overview
sync.Once is a Go standard library mechanism designed to ensure a function executes exactly once, regardless of concurrent access patterns. This pattern is particularly useful for one-time initialization tasks, such as setting up singleton clients or loading configuration data.
The Fast-Path and Slow-Path Pattern
Slow Path
The slow path represents a conservative, thread-safe approach that prioritizes correctness over performance. It typically involves synchronization primitives like mutexes to guarantee data consistency. While this approach ensures correctness in concurrent scenarios, it introduces performance overhead due to explicit locking operations.
Fast Path
The fast path implements an optimistic, high-performance strategy using non-blocking synchronization mechanisms like atomic operations. By avoiding mutex contention, this approach minimizes synchronization overhead. However, it carries the risk of race conditions in certain edge cases.
Implementation Analysis
type Once struct {
done uint32
mu Mutex
}
func (o *Once) Do(fn func()) {
if atomic.LoadUint32(&o.done) == 0 {
o.doSlow(fn)
}
}
func (o *Once) doSlow(fn func()) {
o.mu.Lock()
defer o.mu.Unlock()
if o.done == 0 {
defer atomic.StoreUint32(&o.done, 1)
fn()
}
}
The struct contains two fields: a Mutex for synchronization and a uint32 flag tracking initialization state.
The Do method first attempts the fast path by atomically checking the done flag. If initialization is already complete, it returns immediately without any locking overhead.
When the fast path fails (meaning initialization hasn't occurred), execution falls through to doSlow, which acquires a mutex before proceeding with double-checked locking. The second check inside the locked section prevents race conditions when multiple goroutines reach the slow path simultaneously.
After fn() completes successfully, done is atomically set to 1 using a deferred call.
Practical Applications
Singleton Client Initialization
var (
initOnce sync.Once
rpcClient *RPCClient
)
type RPCClient struct {
connection string
}
func GetRPCClient() *RPCClient {
initOnce.Do(func() {
rpcClient = &RPCClient{
connection: "tcp://localhost:8080",
}
})
return rpcClient
}
This pattern ensures that expensive resources like RPC clients or Kafka consumers are iintialized exactly once, preventing redundant connection attempts.
Configuration Loading
var configOnce sync.Once
var globalConfig *Config
type Config struct {
Host string
Port int
}
func LoadConfig(path string) (*Config, error) {
var loadErr error
configOnce.Do(func() {
data, err := os.ReadFile(path)
if err != nil {
loadErr = err
return
}
globalConfig = &Config{}
json.Unmarshal(data, globalConfig)
})
return globalConfig, loadErr
}
Unlike init() functions which run before main() and have strict ordering constraints, sync.Once provides explicit control over initialization timing, which is critical when subsequent operations depend on loaded configuration.
Key Design Considerations
The function argument fn executes before the atomic store operation sets done to 1. This ordering is essential: any goroutine that sees done == 1 must be able to safely use the result of fn() without additional synchronization.
A subtle but important question arises: why not replace the slow path with atomic.CompareAndSwapUint32?
// This approach is incorrect
if atomic.CompareAndSwapUint32(&o.done, 0, 1) {
fn()
}
This implementation fails because CompareAndSwapUint32 cannot guarantee that fn() has completed when Do returns. Consider two goroutines calling Do concurrently on a fresh Once. The first goroutine might set done to 1 via CAS and begin executing fn(), while a second goroutine also successfully performs CAS and returns immediately. If the second goroutine attempts to use the result of fn() before the first completes, the program enters an undefined state.
The slow path with mutex ensures proper happens-before guarantees: the mutex acquisition in the slow path synchronizes with subsequent lock acquisitions, ensuring that any goroutine observing done == 1 has visibility to the fully initialized state.