Files
cv-site/doc/_go-learning/patterns/07-singleton-pattern.md
T
juanatsap d95c62bad4 refactor: remove outdated server design documentation
Remove 557-line server-design.md from _go-learning/architecture - content is now covered in updated architecture documentation with real implementation examples and test coverage.
2025-12-02 20:25:05 +00:00

10 KiB

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

var (
    instance *Singleton
    once     sync.Once
)

func GetInstance() *Singleton {
    once.Do(func() {
        instance = &Singleton{
            // initialization
        }
    })
    return instance
}

Real Implementation: Configuration Singleton

Configuration Loading

// 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

// 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

// 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

// ❌ 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

// 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

// 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

// 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

// 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

// 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

// ❌ 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

// ✅ 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

// 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

// 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

// 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

// Initialize at package load time
var instance = &Singleton{
    // initialization
}

func GetInstance() *Singleton {
    return instance
}

2. Lazy Initialization

// Initialize on first use
var (
    instance *Singleton
    once     sync.Once
)

func GetInstance() *Singleton {
    once.Do(func() {
        instance = &Singleton{}
    })
    return instance
}

3. With Error Handling

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

// 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

// 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

// 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

// For request-scoped "singletons"
ctx := context.WithValue(ctx, ConfigKey, config)

// Retrieve in handler
config := ctx.Value(ConfigKey).(*Config)

Package Functions

// For stateless utilities
package mathutil

func Max(a, b int) int {
    if a > b {
        return a
    }
    return b
}

// No singleton needed
  • Dependency Injection: Alternative to singleton
  • Factory Pattern: Can create singletons
  • Multiton Pattern: Multiple instances keyed by ID

Further Reading