Hone logo
Hone
Problems

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 Do method 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 f should be executed only once.
  • Ignoring Subsequent Calls: After the function f has been executed once, subsequent calls to Do with 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:

  1. The first call to Do with a function f will execute f.
  2. Subsequent calls to Do with any function will not execute the function.
  3. The SafeInit struct itself should be reusable; creating a new SafeInit instance will allow for a new function to be executed once.

Edge Cases to Consider:

  • Concurrent calls to Do from multiple goroutines.
  • Calling Do multiple times sequentially from the same goroutine.
  • The function f might 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 SafeInit struct and its Do method must be implemented in Go.
  • The Do method must be thread-safe.
  • The function f passed to Do can 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.

Loading editor...
go