Go Concurrency Patterns Series: ← WaitGroup Pattern | Series Overview | Context Pattern →


What is the Once Pattern?

The Once pattern uses sync.Once to ensure that a piece of code executes exactly once, regardless of how many goroutines call it. This is essential for thread-safe initialization, singleton patterns, and one-time setup operations in concurrent programs.

Key Characteristics:

  • Thread-safe: Multiple goroutines can call it safely
  • Exactly once: Code executes only on the first call
  • Blocking: Subsequent calls wait for the first execution to complete
  • No return values: The function passed to Do() cannot return values

Real-World Use Cases

  • Singleton Initialization: Create single instances of objects
  • Configuration Loading: Load config files once at startup
  • Database Connections: Initialize connection pools
  • Logger Setup: Configure logging systems
  • Resource Initialization: Set up expensive resources
  • Feature Flags: Initialize feature flag systems

Basic Once Usage

package main

import (
    "fmt"
    "sync"
    "time"
)

var (
    instance *Database
    once     sync.Once
)

// Database represents a database connection
type Database struct {
    ConnectionString string
    IsConnected      bool
}

// Connect simulates database connection
func (db *Database) Connect() {
    fmt.Println("Connecting to database...")
    time.Sleep(100 * time.Millisecond) // Simulate connection time
    db.IsConnected = true
    fmt.Println("Database connected!")
}

// GetDatabase returns the singleton database instance
func GetDatabase() *Database {
    once.Do(func() {
        fmt.Println("Initializing database instance...")
        instance = &Database{
            ConnectionString: "localhost:5432",
        }
        instance.Connect()
    })
    return instance
}

func main() {
    var wg sync.WaitGroup
    
    // Multiple goroutines trying to get database instance
    for i := 0; i < 5; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            
            fmt.Printf("Goroutine %d requesting database\n", id)
            db := GetDatabase()
            fmt.Printf("Goroutine %d got database: %+v\n", id, db)
        }(i)
    }
    
    wg.Wait()
    
    // Verify all goroutines got the same instance
    fmt.Printf("Final instance: %p\n", GetDatabase())
}

Configuration Manager with Once

package main

import (
    "encoding/json"
    "fmt"
    "os"
    "sync"
)

// Config represents application configuration
type Config struct {
    DatabaseURL string `json:"database_url"`
    APIKey      string `json:"api_key"`
    Debug       bool   `json:"debug"`
    Port        int    `json:"port"`
}

// ConfigManager manages application configuration
type ConfigManager struct {
    config *Config
    once   sync.Once
    err    error
}

// NewConfigManager creates a new config manager
func NewConfigManager() *ConfigManager {
    return &ConfigManager{}
}

// loadConfig loads configuration from file
func (cm *ConfigManager) loadConfig() {
    fmt.Println("Loading configuration...")
    
    // Simulate config file reading
    configData := `{
        "database_url": "postgres://localhost:5432/myapp",
        "api_key": "secret-api-key-123",
        "debug": true,
        "port": 8080
    }`
    
    var config Config
    if err := json.Unmarshal([]byte(configData), &config); err != nil {
        cm.err = fmt.Errorf("failed to parse config: %w", err)
        return
    }
    
    cm.config = &config
    fmt.Println("Configuration loaded successfully!")
}

// GetConfig returns the configuration, loading it once if needed
func (cm *ConfigManager) GetConfig() (*Config, error) {
    cm.once.Do(cm.loadConfig)
    return cm.config, cm.err
}

func main() {
    configManager := NewConfigManager()
    var wg sync.WaitGroup
    
    // Multiple goroutines accessing configuration
    for i := 0; i < 3; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            
            config, err := configManager.GetConfig()
            if err != nil {
                fmt.Printf("Goroutine %d: Error loading config: %v\n", id, err)
                return
            }
            
            fmt.Printf("Goroutine %d: Port=%d, Debug=%v\n", 
                id, config.Port, config.Debug)
        }(i)
    }
    
    wg.Wait()
}

Logger Initialization with Once

package main

import (
    "fmt"
    "log"
    "os"
    "sync"
)

// Logger wraps the standard logger with additional functionality
type Logger struct {
    *log.Logger
    level string
}

var (
    logger     *Logger
    loggerOnce sync.Once
)

// initLogger initializes the global logger
func initLogger() {
    fmt.Println("Initializing logger...")
    
    // Create log file
    file, err := os.OpenFile("app.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
    if err != nil {
        log.Fatalln("Failed to open log file:", err)
    }
    
    logger = &Logger{
        Logger: log.New(file, "APP: ", log.Ldate|log.Ltime|log.Lshortfile),
        level:  "INFO",
    }
    
    logger.Println("Logger initialized")
    fmt.Println("Logger setup complete!")
}

// GetLogger returns the singleton logger instance
func GetLogger() *Logger {
    loggerOnce.Do(initLogger)
    return logger
}

// Info logs an info message
func (l *Logger) Info(msg string) {
    l.Printf("[INFO] %s", msg)
}

// Error logs an error message
func (l *Logger) Error(msg string) {
    l.Printf("[ERROR] %s", msg)
}

func main() {
    var wg sync.WaitGroup
    
    // Multiple goroutines using the logger
    for i := 0; i < 5; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            
            logger := GetLogger()
            logger.Info(fmt.Sprintf("Message from goroutine %d", id))
            
            if id%2 == 0 {
                logger.Error(fmt.Sprintf("Error from goroutine %d", id))
            }
        }(i)
    }
    
    wg.Wait()
    
    // Clean up
    if logger != nil {
        logger.Info("Application shutting down")
    }
}

Resource Pool Initialization

package main

import (
    "fmt"
    "sync"
    "time"
)

// Connection represents a database connection
type Connection struct {
    ID        int
    Connected bool
}

// Connect simulates connecting to database
func (c *Connection) Connect() error {
    time.Sleep(50 * time.Millisecond) // Simulate connection time
    c.Connected = true
    return nil
}

// Close simulates closing the connection
func (c *Connection) Close() error {
    c.Connected = false
    return nil
}

// ConnectionPool manages a pool of database connections
type ConnectionPool struct {
    connections []*Connection
    available   chan *Connection
    once        sync.Once
    initErr     error
}

// NewConnectionPool creates a new connection pool
func NewConnectionPool(size int) *ConnectionPool {
    return &ConnectionPool{
        available: make(chan *Connection, size),
    }
}

// initialize sets up the connection pool
func (cp *ConnectionPool) initialize() {
    fmt.Println("Initializing connection pool...")
    
    poolSize := cap(cp.available)
    cp.connections = make([]*Connection, poolSize)
    
    // Create and connect all connections
    for i := 0; i < poolSize; i++ {
        conn := &Connection{ID: i + 1}
        if err := conn.Connect(); err != nil {
            cp.initErr = fmt.Errorf("failed to connect connection %d: %w", i+1, err)
            return
        }
        
        cp.connections[i] = conn
        cp.available <- conn
    }
    
    fmt.Printf("Connection pool initialized with %d connections\n", poolSize)
}

// GetConnection gets a connection from the pool
func (cp *ConnectionPool) GetConnection() (*Connection, error) {
    cp.once.Do(cp.initialize)
    
    if cp.initErr != nil {
        return nil, cp.initErr
    }
    
    select {
    case conn := <-cp.available:
        return conn, nil
    case <-time.After(5 * time.Second):
        return nil, fmt.Errorf("timeout waiting for connection")
    }
}

// ReturnConnection returns a connection to the pool
func (cp *ConnectionPool) ReturnConnection(conn *Connection) {
    select {
    case cp.available <- conn:
    default:
        // Pool is full, close the connection
        conn.Close()
    }
}

// Close closes all connections in the pool
func (cp *ConnectionPool) Close() error {
    close(cp.available)
    
    for _, conn := range cp.connections {
        if conn != nil {
            conn.Close()
        }
    }
    
    return nil
}

func main() {
    pool := NewConnectionPool(3)
    defer pool.Close()
    
    var wg sync.WaitGroup
    
    // Multiple goroutines using the connection pool
    for i := 0; i < 5; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            
            conn, err := pool.GetConnection()
            if err != nil {
                fmt.Printf("Worker %d: Failed to get connection: %v\n", id, err)
                return
            }
            
            fmt.Printf("Worker %d: Got connection %d\n", id, conn.ID)
            
            // Simulate work
            time.Sleep(200 * time.Millisecond)
            
            pool.ReturnConnection(conn)
            fmt.Printf("Worker %d: Returned connection %d\n", id, conn.ID)
        }(i)
    }
    
    wg.Wait()
}

Advanced Once Patterns

1. Once with Error Handling

package main

import (
    "fmt"
    "sync"
)

// OnceWithError provides Once functionality with error handling
type OnceWithError struct {
    once sync.Once
    err  error
}

// Do executes the function once and stores any error
func (o *OnceWithError) Do(f func() error) error {
    o.once.Do(func() {
        o.err = f()
    })
    return o.err
}

// ExpensiveResource represents a resource that's expensive to initialize
type ExpensiveResource struct {
    Data string
}

var (
    resource     *ExpensiveResource
    resourceOnce OnceWithError
)

// initResource initializes the expensive resource
func initResource() error {
    fmt.Println("Initializing expensive resource...")
    
    // Simulate potential failure
    if false { // Change to true to simulate error
        return fmt.Errorf("failed to initialize resource")
    }
    
    resource = &ExpensiveResource{
        Data: "Important data",
    }
    
    fmt.Println("Resource initialized successfully!")
    return nil
}

// GetResource returns the resource, initializing it once if needed
func GetResource() (*ExpensiveResource, error) {
    err := resourceOnce.Do(initResource)
    if err != nil {
        return nil, err
    }
    return resource, nil
}

func main() {
    var wg sync.WaitGroup
    
    for i := 0; i < 3; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            
            resource, err := GetResource()
            if err != nil {
                fmt.Printf("Goroutine %d: Error: %v\n", id, err)
                return
            }
            
            fmt.Printf("Goroutine %d: Got resource: %s\n", id, resource.Data)
        }(i)
    }
    
    wg.Wait()
}

2. Resettable Once

package main

import (
    "fmt"
    "sync"
    "sync/atomic"
)

// ResettableOnce allows resetting the once behavior
type ResettableOnce struct {
    mu   sync.Mutex
    done uint32
}

// Do executes the function once
func (ro *ResettableOnce) Do(f func()) {
    if atomic.LoadUint32(&ro.done) == 0 {
        ro.doSlow(f)
    }
}

func (ro *ResettableOnce) doSlow(f func()) {
    ro.mu.Lock()
    defer ro.mu.Unlock()
    
    if ro.done == 0 {
        defer atomic.StoreUint32(&ro.done, 1)
        f()
    }
}

// Reset allows the once to be used again
func (ro *ResettableOnce) Reset() {
    ro.mu.Lock()
    defer ro.mu.Unlock()
    atomic.StoreUint32(&ro.done, 0)
}

// IsDone returns true if the function has been executed
func (ro *ResettableOnce) IsDone() bool {
    return atomic.LoadUint32(&ro.done) == 1
}

func main() {
    var once ResettableOnce
    counter := 0
    
    task := func() {
        counter++
        fmt.Printf("Task executed, counter: %d\n", counter)
    }
    
    // First round
    fmt.Println("First round:")
    for i := 0; i < 3; i++ {
        once.Do(task)
    }
    fmt.Printf("Done: %v\n", once.IsDone())
    
    // Reset and second round
    fmt.Println("\nAfter reset:")
    once.Reset()
    fmt.Printf("Done: %v\n", once.IsDone())
    
    for i := 0; i < 3; i++ {
        once.Do(task)
    }
}

Best Practices

  1. Use for Initialization: Perfect for one-time setup operations
  2. Keep Functions Simple: The function passed to Do() should be straightforward
  3. Handle Errors Separately: Use wrapper types for error handling
  4. Avoid Side Effects: Be careful with functions that have external side effects
  5. Don’t Nest Once Calls: Avoid calling Do() from within another Do()
  6. Consider Alternatives: Use init() for package-level initialization when appropriate

Common Pitfalls

1. Expecting Return Values

// ❌ Bad: Once.Do doesn't support return values
var once sync.Once
var result string

func badExample() string {
    once.Do(func() {
        // Can't return from here
        result = "computed value"
    })
    return result // This works but is not ideal
}

// ✅ Good: Use a wrapper or store results in accessible variables
type OnceResult struct {
    once   sync.Once
    result string
    err    error
}

func (or *OnceResult) Get() (string, error) {
    or.once.Do(func() {
        or.result, or.err = computeValue()
    })
    return or.result, or.err
}

2. Panic in Once Function

// ❌ Bad: Panic prevents future calls
var once sync.Once

func badOnceFunc() {
    once.Do(func() {
        panic("something went wrong") // Once will never execute again
    })
}

// ✅ Good: Handle panics appropriately
func goodOnceFunc() {
    once.Do(func() {
        defer func() {
            if r := recover(); r != nil {
                // Handle panic appropriately
                fmt.Printf("Recovered from panic: %v\n", r)
            }
        }()
        // risky operation
    })
}

Testing Once Patterns

package main

import (
    "sync"
    "testing"
)

func TestOnceExecution(t *testing.T) {
    var once sync.Once
    counter := 0
    
    var wg sync.WaitGroup
    
    // Start multiple goroutines
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            once.Do(func() {
                counter++
            })
        }()
    }
    
    wg.Wait()
    
    if counter != 1 {
        t.Errorf("Expected counter to be 1, got %d", counter)
    }
}

func TestOnceWithError(t *testing.T) {
    var onceErr OnceWithError
    callCount := 0
    
    // First call with error
    err1 := onceErr.Do(func() error {
        callCount++
        return fmt.Errorf("test error")
    })
    
    // Second call should return same error without executing function
    err2 := onceErr.Do(func() error {
        callCount++
        return nil
    })
    
    if callCount != 1 {
        t.Errorf("Expected function to be called once, got %d", callCount)
    }
    
    if err1 == nil || err2 == nil {
        t.Error("Expected both calls to return error")
    }
    
    if err1.Error() != err2.Error() {
        t.Error("Expected same error from both calls")
    }
}

The Once pattern is essential for thread-safe initialization in Go. It ensures that expensive or critical setup operations happen exactly once, making it perfect for singletons, configuration loading, and resource initialization in concurrent applications.


Next: Learn about Context Pattern for cancellation, timeouts, and request-scoped values.