Go does not have a traditional "thread-safe singleton" pattern like Java or C++ because the language's concurrency model and memory guarantees make explicit locking unnecessary for initialization. Instead, you rely on the sync.Once type or the language's package-level variable initialization, which is guaranteed to be thread-safe by the Go specification.
The most robust approach for a singleton that requires complex initialization logic is using sync.Once. This ensures the initialization function runs exactly once, even if multiple goroutines call it simultaneously. For simpler cases, a package-level variable initialized at load time is sufficient and inherently thread-safe.
Here is the standard implementation using sync.Once:
package main
import (
"fmt"
"sync"
)
type Singleton struct {
// Add your fields here
value string
}
var (
instance *Singleton
once sync.Once
)
// GetInstance returns the singleton instance, initializing it if necessary.
func GetInstance() *Singleton {
once.Do(func() {
instance = &Singleton{
value: "initialized once",
}
// Perform expensive initialization logic here
})
return instance
}
func main() {
// Simulate concurrent access
for i := 0; i < 10; i++ {
go func(id int) {
s := GetInstance()
fmt.Printf("Goroutine %d: %v\n", id, s.value)
}(i)
}
}
If your singleton only requires simple data structures without complex setup, you can skip sync.Once entirely. Go guarantees that package-level variables are initialized in a thread-safe manner before main starts executing.
package main
import "fmt"
type SimpleSingleton struct {
data string
}
// This variable is initialized once at package load time, thread-safely.
var simpleInstance = &SimpleSingleton{data: "static init"}
func GetSimpleInstance() *SimpleSingleton {
return simpleInstance
}
func main() {
fmt.Println(GetSimpleInstance().data)
}
Avoid using sync.Mutex manually to guard a nil check (the "double-checked locking" pattern common in other languages). In Go, this is error-prone due to memory reordering unless you use sync/atomic correctly, which adds unnecessary complexity. sync.Once handles the memory barriers and locking logic for you, ensuring correctness with minimal code.
Use the package-level variable approach when possible for its simplicity. Switch to sync.Once only when you need to defer initialization until the instance is first requested or when the initialization logic is expensive and should not run at startup.