sync.Once
is a synchronization primitive provided by the sync
package in Go. It ensures that a particular piece of code is executed only once throughout the lifetime of the program, regardless of how many goroutines attempt to execute it.
sync.Once
is commonly used in scenarios like:
- Lazy initialization.
- Ensuring an action is executed only once, such as setting up configurations or opening database connections.
sync.Once
operates with a single method called Do(f func())
. This method accepts a function and guarantees that it will be executed exactly once.
var once sync.Once
once.Do(func() {
// Code to execute once
})
package main
import (
"fmt"
"sync"
)
var once sync.Once
func initialize() {
fmt.Println("Initialization executed.")
}
func main() {
for i := 0; i < 5; i++ {
go once.Do(initialize)
}
// Wait for goroutines to finish
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
// This won't execute again
once.Do(initialize)
}()
wg.Wait()
}
Output:
Initialization executed.
Explanation:
- Even though multiple goroutines are calling
once.Do(initialize)
, the functioninitialize
is executed only once.
sync.Once
is well-suited for implementing the singleton pattern in Go, ensuring that a single instance is initialized.
package main
import (
"fmt"
"sync"
)
type Singleton struct {
name string
}
var instance *Singleton
var once sync.Once
func GetInstance() *Singleton {
once.Do(func() {
instance = &Singleton{name: "MySingleton"}
fmt.Println("Singleton instance created.")
})
return instance
}
func main() {
for i := 0; i < 5; i++ {
go func(id int) {
s := GetInstance()
fmt.Printf("Goroutine %d: Got instance: %v\n", id, s)
}(i)
}
// Wait for goroutines to finish
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
GetInstance()
}()
wg.Wait()
}
Output:
Singleton instance created.
Goroutine 0: Got instance: &{MySingleton}
Goroutine 1: Got instance: &{MySingleton}
...
Explanation:
- The instance is created only once and shared across all goroutines.
package main
import (
"fmt"
"sync"
)
var (
config map[string]string
onceConfig sync.Once
)
func loadConfig() {
fmt.Println("Loading configuration...")
config = map[string]string{
"host": "localhost",
"port": "8080",
}
}
func GetConfig() map[string]string {
onceConfig.Do(loadConfig)
return config
}
func main() {
for i := 0; i < 5; i++ {
go func(id int) {
cfg := GetConfig()
fmt.Printf("Goroutine %d: Config: %v\n", id, cfg)
}(i)
}
// Wait for goroutines to finish
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
GetConfig()
}()
wg.Wait()
}
Output:
Loading configuration...
Goroutine 0: Config: map[host:localhost port:8080]
...
Explanation:
loadConfig
is called only once, even thoughGetConfig
is called multiple times from different goroutines.
-
The function passed to
Do
should not return an error:sync.Once
does not provide a mechanism to handle errors within the function that it calls.- If your function needs to return an error, you should handle it outside
sync.Once
.
-
Avoid complex functions in
Do
:- The function executed by
Do
should only perform necessary tasks to avoid deadlocks or hard-to-debug errors.
- The function executed by
-
Cannot reset
sync.Once
:- After
Do
successfully executes, it cannot be "reset" to execute again. If you need to re-run the logic, you should consider other synchronization mechanisms.
- After
sync.Once
ensures that a piece of code runs only once, helping you avoid issues related to multiple initializations or concurrent accesses.- It's especially useful for implementing patterns like singleton or lazy initialization.
- Use it when you want to execute an action safely and exactly once, especially in a multi-threaded environment.