602 lines
10 KiB
Markdown
602 lines
10 KiB
Markdown
|
|
# 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/)
|