Files
cv-site/doc/_go-learning/patterns/07-singleton-pattern.md
T

602 lines
10 KiB
Markdown
Raw Normal View History

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