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
- Use for Initialization: Perfect for one-time setup operations
- Keep Functions Simple: The function passed to
Do()
should be straightforward - Handle Errors Separately: Use wrapper types for error handling
- Avoid Side Effects: Be careful with functions that have external side effects
- Don’t Nest Once Calls: Avoid calling
Do()
from within anotherDo()
- 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.