← Назад к вопросам

Что такое паттерн Singleton?

2.0 Middle🔥 101 комментариев
#Микросервисы и архитектура

Комментарии (1)

🐱
deepseek-v3.2PrepBro AI7 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Что такое паттерн Singleton?

Singleton (Одиночка) — это порождающий паттерн проектирования, который гарантирует, что у класса есть только один экземпляр, и предоставляет глобальную точку доступа к этому экземпляру. Он решает две ключевые задачи:

  1. Контроль создания: предотвращает создание более одного объекта класса.
  2. Глобальный доступ: предоставляет известный способ получения этого единственного экземпляра.

Зачем он нужен? Основные цели и применение

Паттерн используется, когда в системе должен существовать ровно один экземпляр некоторого класса, доступный из разных частей программы. Типичные сценарии:

  • Управление общими ресурсами: подключение к базе данных, логгер, файловый менеджер, кэш.
  • Конфигурация приложения: объект, хранящий глобальные настройки.
  • Менеджеры и контроллеры: менеджер пула потоков (thread pool), диспетчер задач.

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

Классическая реализация Singleton в Go

В отличие от языков с классическим ООП (Java, C++), в Go нет классов в традиционном понимании, но паттерн легко реализуется с использованием пакетов (package), приватности и sync.Once.

Базовая (небезопасная) реализация

package singleton

type singleton struct {
    // Поля вашей структуры
    data string
}

var instance *singleton

func GetInstance() *singleton {
    if instance == nil {
        instance = &singleton{data: "initial data"}
    }
    return instance
}

Проблема: Эта реализация небезопасна для конкурентного доступа (not thread-safe). В многопоточной среде несколько горутин могут одновременно проверить instance == nil и создать несколько объектов.

Потокобезопасная реализация с sync.Mutex

package singleton

import "sync"

type singleton struct {
    data string
}

var (
    instance *singleton
    mu       sync.Mutex
)

func GetInstance() *singleton {
    mu.Lock()
    defer mu.Unlock()

    if instance == nil {
        instance = &singleton{data: "safe data"}
    }
    return instance
}

Плюс: Решение теперь безопасно. Минус: Дорогая операция блокировки при каждом вызове, даже после создания объекта.

Оптимальная реализация с sync.Once (идиоматичный способ для Go)

Пакет sync предоставляет идеальный инструмент для этой задачи — sync.Once. Он гарантирует, что функция будет выполнена ровно один раз, независимо от количества вызовов.

package singleton

import "sync"

type singleton struct {
    data string
}

var (
    instance *singleton
    once     sync.Once
)

func GetInstance() *singleton {
    once.Do(func() {
        instance = &singleton{data: "data initialized once"}
    })
    return instance
}

Преимущества этого подхода:

  • Абсолютная потокобезопасность: sync.Once гарантирует это внутренними механизмами.
  • Высокая производительность: после инициализации вызовы GetInstance() практически бесплатны, так как избегают блокировок.
  • Идиоматичность: это общепринятый и рекомендуемый способ в Go-сообществе.

Важные аспекты и альтернативы в Go

1. Инициализация в init()

Для простых случаев, когда объект не требует сложной логики создания, можно использовать функцию init() пакета.

package config

var AppConfig *Config

type Config struct {
    Port int
    Env  string
}

func init() {
    AppConfig = &Config{Port: 8080, Env: "development"}
}
// Теперь AppConfig доступен глобально в рамках пакета

Недостаток: отсутствие ленивой инициализации (lazy initialization) — объект создается при инициализации пакета, даже если он не используется.

2. Singleton как зависимость (Dependency Injection)

Современный подход — избегать "настоящего" глобального синглтона, а передавать единственный экземпляр как зависимость через параметры функций или структуры. Это делает код более тестируемым и явным.

type Logger struct {
    // ...
}

func NewLogger() *Logger {
    return &Logger{}
}

// Вместо глобального вызова singleton.GetLogger() мы передаем его
func ProcessOrder(logger *Logger, orderID string) {
    logger.Info("Processing order", orderID)
}

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

  • Когда объект действительно должен иметь состояние, уникальное для каждого контекста.
  • Когда требуется легко тестировать код (глобальное состояние усложняет изоляцию тестов).
  • Когда можно обойтись простой передачей зависимости.

Вывод

В Go паттерн Singleton чаще всего реализуется через комбинацию приватного типа, глобальной переменной (в рамках пакета) и sync.Once для обеспечения потокобезопасной ленивой инициализации. Однако, due to идиомам языка и акценту на простоту и тестируемость, часто предпочтительнее рассматривать альтернативы: инициализацию в пакете для простых случаев или явную передачу зависимости для более сложных и гибких архитектур. Ключ — понимать, что паттерн решает проблему контроля над созданием экземпляра, а не просто является способом создать глобальную переменную.