# Singleton Pattern in Go ## Pattern Overview The Singleton Pattern ensures that a class has only one instance and provides a global point of access to it. In Go, this is typically achieved through package-level variables and `sync.Once` for thread-safe initialization. ## Pattern Structure ```go var ( instance *Singleton once sync.Once ) func GetInstance() *Singleton { once.Do(func() { instance = &Singleton{ // initialization } }) return instance } ``` ## Real Implementation: Configuration Singleton ### Configuration Loading ```go // internal/config/config.go var ( instance *Config once sync.Once ) // Config holds application configuration type Config struct { Server ServerConfig Templates TemplateConfig } // Load returns singleton configuration instance func Load() *Config { once.Do(func() { instance = &Config{ Server: ServerConfig{ Host: getEnvOrDefault("HOST", "localhost"), Port: getEnvOrDefault("PORT", ":8080"), }, Templates: TemplateConfig{ Dir: getEnvOrDefault("TEMPLATE_DIR", "templates"), PartialsDir: getEnvOrDefault("PARTIALS_DIR", "templates/partials"), HotReload: getBoolEnv("HOT_RELOAD", true), }, } }) return instance } func getEnvOrDefault(key, defaultValue string) string { if value := os.Getenv(key); value != "" { return value } return defaultValue } ``` ### Template Manager Singleton ```go // In a larger application, template manager might be singleton var ( templateManager *templates.Manager tmplOnce sync.Once ) func GetTemplateManager() (*templates.Manager, error) { var err error tmplOnce.Do(func() { cfg := Load() // Get config singleton templateManager, err = templates.NewManager(cfg.Templates) }) return templateManager, err } ``` ## Thread-Safe Singleton ### Using sync.Once ```go // sync.Once guarantees initialization happens exactly once type Database struct { conn *sql.DB } var ( db *Database once sync.Once ) func GetDatabase() (*Database, error) { var err error once.Do(func() { db = &Database{} db.conn, err = sql.Open("postgres", "connection-string") if err != nil { db = nil // Reset on error } }) if db == nil { return nil, err } return db, nil } ``` ### Thread-Safety Comparison ```go // ❌ NOT thread-safe var instance *Singleton func GetInstance() *Singleton { if instance == nil { // Race condition! instance = &Singleton{} } return instance } // ✅ Thread-safe with mutex (but slower) var ( instance *Singleton mu sync.Mutex ) func GetInstance() *Singleton { mu.Lock() defer mu.Unlock() if instance == nil { instance = &Singleton{} } return instance } // ✅ Thread-safe with sync.Once (best) var ( instance *Singleton once sync.Once ) func GetInstance() *Singleton { once.Do(func() { instance = &Singleton{} }) return instance } ``` ## Singleton vs Package-Level Variables ### Simple Package-Level Variable ```go // For simple, non-lazy initialization package logger var std = New(os.Stdout, InfoLevel) func Info(msg string) { std.Log(InfoLevel, msg) } func Debug(msg string) { std.Log(DebugLevel, msg) } ``` ### When to Use Singleton vs Package Variable **Use Singleton (sync.Once) when:** - Initialization is expensive - Initialization might fail - Need lazy initialization - Need thread-safe initialization **Use Package Variable when:** - Initialization is cheap - Initialization always succeeds - Want immediate initialization - Simple, stateless utility ## Singleton Use Cases ### 1. Configuration ```go // config/config.go var ( cfg *Config once sync.Once ) func Load() *Config { once.Do(func() { cfg = &Config{} // Load from file, env, etc. cfg.loadFromEnv() cfg.loadFromFile() }) return cfg } ``` ### 2. Database Connection Pool ```go // database/db.go var ( pool *sql.DB once sync.Once ) func GetPool() (*sql.DB, error) { var err error once.Do(func() { pool, err = sql.Open("postgres", getConnectionString()) if err != nil { return } pool.SetMaxOpenConns(25) pool.SetMaxIdleConns(5) err = pool.Ping() if err != nil { pool.Close() pool = nil } }) if pool == nil { return nil, err } return pool, nil } ``` ### 3. Logger ```go // logger/logger.go var ( logger *Logger once sync.Once ) type Logger struct { writer io.Writer level Level } func Get() *Logger { once.Do(func() { logger = &Logger{ writer: os.Stdout, level: InfoLevel, } }) return logger } // Convenience functions func Info(msg string) { Get().Log(InfoLevel, msg) } func Error(msg string) { Get().Log(ErrorLevel, msg) } ``` ### 4. Cache ```go // cache/cache.go var ( cache *Cache once sync.Once ) type Cache struct { data map[string]interface{} mu sync.RWMutex } func Get() *Cache { once.Do(func() { cache = &Cache{ data: make(map[string]interface{}), } }) return cache } func Set(key string, value interface{}) { c := Get() c.mu.Lock() defer c.mu.Unlock() c.data[key] = value } func Retrieve(key string) (interface{}, bool) { c := Get() c.mu.RLock() defer c.mu.RUnlock() val, ok := c.data[key] return val, ok } ``` ## Anti-Pattern: Global State ### Problem ```go // ❌ BAD: Mutable global state var Config = &AppConfig{ Timeout: 30, } func main() { Config.Timeout = 60 // Mutating global state // Hard to test, unpredictable behavior } ``` ### Solution: Immutable Singleton ```go // ✅ GOOD: Immutable singleton var ( config *Config once sync.Once ) func GetConfig() *Config { once.Do(func() { config = &Config{ Timeout: 30, } }) return config // Read-only access } // To change config, create new instance func WithTimeout(timeout int) *Config { old := GetConfig() return &Config{ Timeout: timeout, // Copy other fields from old } } ``` ## Testing Singletons ### Problem with Testing ```go // Singleton makes testing difficult func TestFeature(t *testing.T) { instance := GetInstance() instance.value = "test1" // Test 1 passes // But now instance.value is "test1" for next test! } ``` ### Solution: Reset for Tests ```go // Add reset function for tests func ResetForTest() { once = sync.Once{} instance = nil } func TestFeature(t *testing.T) { defer ResetForTest() instance := GetInstance() instance.value = "test1" // Test with clean state } ``` ### Alternative: Dependency Injection ```go // Instead of singleton, use DI for testability type Handler struct { config *Config // Injected, not singleton } func NewHandler(config *Config) *Handler { return &Handler{config: config} } // Easy to test with different configs func TestHandler(t *testing.T) { testConfig := &Config{Timeout: 10} handler := NewHandler(testConfig) // Test with test config } ``` ## Singleton Variations ### 1. Eager Initialization ```go // Initialize at package load time var instance = &Singleton{ // initialization } func GetInstance() *Singleton { return instance } ``` ### 2. Lazy Initialization ```go // Initialize on first use var ( instance *Singleton once sync.Once ) func GetInstance() *Singleton { once.Do(func() { instance = &Singleton{} }) return instance } ``` ### 3. With Error Handling ```go var ( instance *Singleton once sync.Once err error ) func GetInstance() (*Singleton, error) { once.Do(func() { instance, err = initialize() }) return instance, err } func initialize() (*Singleton, error) { s := &Singleton{} if err := s.connect(); err != nil { return nil, err } return s, nil } ``` ## Best Practices ### ✅ DO ```go // Use sync.Once for thread-safety var once sync.Once // Make fields private type Singleton struct { privateField string } // Provide accessor methods func (s *Singleton) GetValue() string { return s.privateField } // Handle initialization errors func GetInstance() (*Singleton, error) { var err error once.Do(func() { instance, err = newSingleton() }) return instance, err } // Document singleton nature // GetDatabase returns the singleton database connection pool. // Thread-safe and initialized lazily on first call. func GetDatabase() *Database { // ... } ``` ### ❌ DON'T ```go // DON'T use mutable global state var GlobalConfig Config // Mutable! // DON'T forget thread-safety if instance == nil { // Race condition! instance = &Singleton{} } // DON'T make everything a singleton // Only use for truly global, single-instance resources // DON'T ignore errors in initialization once.Do(func() { instance, _ = newSingleton() // Ignoring error! }) ``` ## When NOT to Use Singleton 1. **Testing is Important**: Dependency injection is better 2. **Multiple Instances Needed**: Use factory pattern 3. **State Changes**: Avoid mutable singletons 4. **Simple Utilities**: Use package functions 5. **Request-Scoped**: Use context pattern ## Alternatives to Singleton ### Dependency Injection ```go // Better for testability type Handler struct { config *Config // Injected db *DB // Injected } func NewHandler(config *Config, db *DB) *Handler { return &Handler{config: config, db: db} } ``` ### Context Values ```go // For request-scoped "singletons" ctx := context.WithValue(ctx, ConfigKey, config) // Retrieve in handler config := ctx.Value(ConfigKey).(*Config) ``` ### Package Functions ```go // For stateless utilities package mathutil func Max(a, b int) int { if a > b { return a } return b } // No singleton needed ``` ## Related Patterns - **Dependency Injection**: Alternative to singleton - **Factory Pattern**: Can create singletons - **Multiton Pattern**: Multiple instances keyed by ID ## Further Reading - [Singleton Pattern](https://refactoring.guru/design-patterns/singleton) - [sync.Once Documentation](https://pkg.go.dev/sync#Once) - [Go Singleton Best Practices](https://www.sohamkamani.com/golang/singleton-pattern/)