Mastering sync.Once in Go: Thread‑Safe Singleton Patterns Explained
This article explains Go's sync.Once primitive, compares it with init, details various singleton implementations—including lazy, eager, and double‑checked locking—and provides practical code examples and a deep dive into its source implementation for thread‑safe one‑time initialization.
sync.Once Introduction
sync.Once is a Go standard‑library primitive that guarantees a function runs only once, making it ideal for singleton patterns such as initializing configuration or maintaining a database connection. Unlike the init function, sync.Once can be invoked lazily at any point in the code and is safe for concurrent use.
Usage Scenarios
Understanding common singleton implementations helps illustrate when sync.Once is appropriate. The classic lazy ("懒汉") pattern creates the instance on first use, but it is not thread‑safe:
<code>type DemoModel struct{}
var ins *DemoModel
func InsDemoModel() *DemoModel {
if ins == nil {
ins = new(DemoModel)
}
return ins
}</code>The eager ("饿汉") pattern instantiates the object at program start, which can increase startup time and waste memory if the instance is never used:
<code>type DemoModel struct{}
var ins *DemoModel = new(DemoModel)
func InsDemoModel() *DemoModel { return ins }</code>Adding a mutex makes the lazy version thread‑safe but incurs locking overhead on every call:
<code>type DemoModel struct{}
var ins *DemoModel
var mu sync.Mutex
func InsDemoModel() *DemoModel {
mu.Lock()
defer mu.Unlock()
if ins == nil {
ins = new(DemoModel)
}
return ins
}</code>Double‑checked locking reduces the locking cost by first checking the instance without a lock, then acquiring the lock only when necessary:
<code>type DemoModel struct{}
var ins *DemoModel
var mu sync.Mutex
func InsDemoModel() *DemoModel {
if ins == nil {
mu.Lock()
defer mu.Unlock()
if ins == nil {
ins = new(DemoModel)
}
}
return ins
}</code>sync.Once Usage Example
Combining sync.Once with a singleton eliminates both race conditions and unnecessary locking:
<code>type DemoModel struct{}
var (
once sync.Once
ins *DemoModel
)
func InsDemoModel() *DemoModel {
once.Do(func() {
ins = new(DemoModel)
log.Println("Get DemoModel instance")
})
return ins
}
func main() {
for i := 0; i < 10; i++ {
go func() { _ = InsDemoModel() }()
}
time.Sleep(time.Second)
}</code>Running the program prints the log line only once, confirming that the initialization function executed a single time:
<code>$ go run main.go
2021/06/23 20:27:06 Get DemoModel instance</code>sync.Once Implementation Details
The core of sync.Once is a struct with a done flag and a mutex. Placing done as the first field reduces instruction count on the hot path, improving performance on many architectures.
<code>package sync
import "sync/atomic"
type Once struct {
done uint32
m Mutex
}
func (o *Once) Do(f func()) {
if atomic.LoadUint32(&o.done) == 0 {
o.doSlow(f)
}
}
func (o *Once) doSlow(f func()) {
o.m.Lock()
defer o.m.Unlock()
if o.done == 0 {
defer atomic.StoreUint32(&o.done, 1)
f()
}
}
</code>Comments in the source explain that the done field is placed first to keep the hot‑path code compact, allowing the compiler to generate fewer instructions when checking whether the action has already been performed.
360 Zhihui Cloud Developer
360 Zhihui Cloud is an enterprise open service platform that aims to "aggregate data value and empower an intelligent future," leveraging 360's extensive product and technology resources to deliver platform services to customers.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.