Блог инженера

History is written by its contributors

Singleton Pattern в Go: реализация и случаи использования

2025-06-16 время чтения 4 мин Patterns Ilya Brin

Singleton — один из самых спорных паттернов. Одни называют его антипаттерном, другие используют повсеместно. Разберём, как правильно реализовать Singleton в Go и когда он действительно нужен.

Что такое Singleton

Singleton гарантирует, что у класса есть только один экземпляр, и предоставляет глобальную точку доступа к нему.

В Go это означает: один экземпляр структуры на всё приложение.

Наивная реализация

type Database struct {
    connection string
}

var instance *Database

func GetInstance() *Database {
    if instance == nil {
        instance = &Database{
            connection: "postgres://...",
        }
    }
    return instance
}

Проблема: не потокобезопасно. Две горутины могут создать два экземпляра.

Правильная реализация: sync.Once

type Database struct {
    connection string
}

var (
    instance *Database
    once     sync.Once
)

func GetInstance() *Database {
    once.Do(func() {
        instance = &Database{
            connection: "postgres://...",
        }
    })
    return instance
}

sync.Once гарантирует, что функция выполнится ровно один раз, даже при конкурентном доступе.

Реализация с инициализацией

type Config struct {
    DBHost string
    DBPort int
}

var (
    config *Config
    once   sync.Once
)

func GetConfig() *Config {
    once.Do(func() {
        config = &Config{
            DBHost: os.Getenv("DB_HOST"),
            DBPort: 5432,
        }
    })
    return config
}

Ленивая инициализация с ошибкой

type Database struct {
    conn *sql.DB
}

var (
    instance *Database
    once     sync.Once
    initErr  error
)

func GetInstance() (*Database, error) {
    once.Do(func() {
        conn, err := sql.Open("postgres", "...")
        if err != nil {
            initErr = err
            return
        }
        instance = &Database{conn: conn}
    })
    
    if initErr != nil {
        return nil, initErr
    }
    return instance, nil
}

Eager инициализация

type Logger struct {
    level string
}

var instance = &Logger{
    level: "info",
}

func GetLogger() *Logger {
    return instance
}

Инициализируется при старте программы. Проще, но нет контроля над моментом создания.

Singleton с параметрами

type Cache struct {
    data map[string]interface{}
    mu   sync.RWMutex
}

var (
    cache *Cache
    once  sync.Once
)

func InitCache(size int) {
    once.Do(func() {
        cache = &Cache{
            data: make(map[string]interface{}, size),
        }
    })
}

func GetCache() *Cache {
    if cache == nil {
        panic("cache not initialized")
    }
    return cache
}

Когда использовать Singleton

1. Конфигурация приложения

type AppConfig struct {
    Port     int
    LogLevel string
    DBUrl    string
}

var (
    config *AppConfig
    once   sync.Once
)

func GetConfig() *AppConfig {
    once.Do(func() {
        config = &AppConfig{
            Port:     getEnvInt("PORT", 8080),
            LogLevel: getEnv("LOG_LEVEL", "info"),
            DBUrl:    getEnv("DB_URL", ""),
        }
    })
    return config
}

2. Пул соединений

type ConnectionPool struct {
    db *sql.DB
}

var (
    pool *ConnectionPool
    once sync.Once
)

func GetPool() *ConnectionPool {
    once.Do(func() {
        db, err := sql.Open("postgres", GetConfig().DBUrl)
        if err != nil {
            panic(err)
        }
        db.SetMaxOpenConns(25)
        pool = &ConnectionPool{db: db}
    })
    return pool
}

3. Логгер

type Logger struct {
    *log.Logger
}

var (
    logger *Logger
    once   sync.Once
)

func GetLogger() *Logger {
    once.Do(func() {
        logger = &Logger{
            Logger: log.New(os.Stdout, "", log.LstdFlags),
        }
    })
    return logger
}

Когда НЕ использовать Singleton

1. Для бизнес-логики

// Плохо
type UserService struct {
    repo UserRepository
}

var userService *UserService

// Хорошо - используйте DI
func NewUserService(repo UserRepository) *UserService {
    return &UserService{repo: repo}
}

2. Для тестируемого кода

// Плохо - невозможно подменить в тестах
func ProcessUser(id string) {
    db := GetDatabase()
    // ...
}

// Хорошо - явная зависимость
func ProcessUser(db *Database, id string) {
    // ...
}

3. Для состояния

// Плохо - глобальное состояние
type Counter struct {
    value int
    mu    sync.Mutex
}

var counter *Counter

// Хорошо - локальное состояние
type Handler struct {
    counter *Counter
}

Альтернативы Singleton

1. Dependency Injection

type App struct {
    config *Config
    db     *Database
    logger *Logger
}

func NewApp() *App {
    config := LoadConfig()
    db := ConnectDB(config)
    logger := NewLogger(config)
    
    return &App{
        config: config,
        db:     db,
        logger: logger,
    }
}

2. Context

type contextKey string

const configKey contextKey = "config"

func WithConfig(ctx context.Context, cfg *Config) context.Context {
    return context.WithValue(ctx, configKey, cfg)
}

func GetConfig(ctx context.Context) *Config {
    return ctx.Value(configKey).(*Config)
}

3. Функциональные опции

type Server struct {
    config *Config
    logger *Logger
}

type Option func(*Server)

func WithConfig(cfg *Config) Option {
    return func(s *Server) {
        s.config = cfg
    }
}

func NewServer(opts ...Option) *Server {
    s := &Server{}
    for _, opt := range opts {
        opt(s)
    }
    return s
}

Тестирование Singleton

// Проблема: нельзя сбросить состояние между тестами
func TestSingleton(t *testing.T) {
    instance := GetInstance()
    // Тест изменяет состояние
}

// Решение: добавить метод для тестов
var (
    instance *Database
    once     sync.Once
)

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

func ResetForTest() {
    instance = nil
    once = sync.Once{}
}

func TestWithReset(t *testing.T) {
    defer ResetForTest()
    instance := GetInstance()
    // Тест
}

Производительность

func BenchmarkSingleton(b *testing.B) {
    b.RunParallel(func(pb *testing.PB) {
        for pb.Next() {
            _ = GetInstance()
        }
    })
}

func BenchmarkNew(b *testing.B) {
    b.RunParallel(func(pb *testing.PB) {
        for pb.Next() {
            _ = &Database{}
        }
    })
}

Результаты:

BenchmarkSingleton-8    1000000000    0.5 ns/op
BenchmarkNew-8          50000000      30 ns/op

Singleton быстрее, но разница незначительна для большинства случаев.

Заключение

Singleton в Go:

  • Используйте sync.Once для потокобезопасности
  • Применяйте для конфигурации, логгеров, пулов соединений
  • Избегайте для бизнес-логики и тестируемого кода
  • Рассмотрите альтернативы: DI, context, функциональные опции

Singleton — это инструмент. Используйте его осознанно, а не по умолчанию.

Главное правило: если сомневаетесь — не используйте Singleton.

comments powered by Disqus