Implementing a Safe Initialization Function with sync.Once in Go
The sync.Once type in Go provides a mechanism to ensure that a function is executed only once, even when multiple goroutines attempt to execute it concurrently. This is crucial for initializing resources that should only be created once, preventing race conditions and ensuring data integrity. Your task is to implement a simplified version of sync.Once's functionality.
Problem Description
You are to implement a SafeInit struct that mimics the core behavior of sync.Once. This struct should have a single method, Do, which accepts a function f as an argument. The Do method should ensure that the function f is executed only once, regardless of how many times Do is called concurrently from different goroutines. After the first execution, subsequent calls to Do with different functions should be ignored.
Key Requirements:
- Thread Safety: The
Domethod must be thread-safe, meaning it can be called concurrently from multiple goroutines without causing race conditions or unexpected behavior. - Single Execution: The provided function
fshould be executed only once. - Ignoring Subsequent Calls: After the function
fhas been executed once, subsequent calls toDowith any function should be ignored. The function should not panic or error if called multiple times. - No External Dependencies: Your solution should only use standard Go library packages.
Expected Behavior:
- The first call to
Dowith a functionfwill executef. - Subsequent calls to
Dowith any function will not execute the function. - The
SafeInitstruct itself should be reusable; creating a newSafeInitinstance will allow for a new function to be executed once.
Edge Cases to Consider:
- Concurrent calls to
Dofrom multiple goroutines. - Calling
Domultiple times sequentially from the same goroutine. - The function
fmight take a long time to execute. The implementation should still guarantee single execution.
Examples
Example 1:
Input:
package main
import (
"fmt"
"sync"
)
type SafeInit struct{}
func (s *SafeInit) Do(f func()) {
// Your implementation here
}
func main() {
var wg sync.WaitGroup
safeInit := &SafeInit{}
for i := 0; i < 5; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
safeInit.Do(func() {
fmt.Println("Initialization:", i)
})
}(i)
}
wg.Wait()
}
Output:
Initialization: 0
Explanation: Despite 5 goroutines calling Do, the "Initialization" message is printed only once, demonstrating single execution.
Example 2:
Input:
package main
import (
"fmt"
"sync"
)
type SafeInit struct{}
func (s *SafeInit) Do(f func()) {
// Your implementation here
}
func main() {
safeInit := &SafeInit{}
safeInit.Do(func() { fmt.Println("First call") })
safeInit.Do(func() { fmt.Println("Second call") })
safeInit.Do(func() { fmt.Println("Third call") })
}
Output:
First call
Explanation: The first call executes the function. Subsequent calls are ignored.
Example 3: (Edge Case - Long Running Function)
Input:
package main
import (
"fmt"
"sync"
"time"
)
type SafeInit struct{}
func (s *SafeInit) Do(f func()) {
// Your implementation here
}
func main() {
safeInit := &SafeInit{}
safeInit.Do(func() {
fmt.Println("Starting long initialization...")
time.Sleep(2 * time.Second) // Simulate a long initialization
fmt.Println("Long initialization complete.")
})
// Simulate other goroutines trying to initialize concurrently
var wg sync.WaitGroup
wg.Add(3)
for i := 0; i < 3; i++ {
go func() {
defer wg.Done()
safeInit.Do(func() { fmt.Println("Attempting concurrent initialization") })
}()
}
wg.Wait()
}
Output:
Starting long initialization...
Long initialization complete.
Explanation: Even though other goroutines attempt to call Do while the initialization is running, they are ignored after the first execution completes.
Constraints
- The
SafeInitstruct and itsDomethod must be implemented in Go. - The
Domethod must be thread-safe. - The function
fpassed toDocan be any function with no arguments and no return values (func()). - The solution should not use any external libraries beyond the standard Go library.
- The solution should be efficient and avoid unnecessary overhead.
Notes
Consider using a mutex to protect access to a flag that indicates whether the function has already been executed. Think about how to ensure that the function is executed atomically, even when multiple goroutines are racing to call Do. The goal is to replicate the core functionality of sync.Once without using it directly.