Что такое паттерн Singleton (Одиночка)?

Ответ

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

В Go идиоматичная реализация Singleton достигается с помощью пакета sync и его типа Once, который обеспечивает потокобезопасную инициализацию ровно один раз.

Пример на Go (потокобезопасный):

package main

import (
    "fmt"
    "sync"
)

// Структура, которую мы хотим сделать синглтоном
// (например, подключение к БД или конфигурация)
type DatabaseConnection struct {
    connectionString string
}

func (db *DatabaseConnection) Query(q string) {
    fmt.Printf("Executing query '%s' using connection '%s'n", q, db.connectionString)
}

var (
    instance *DatabaseConnection
    once     sync.Once
)

// GetInstance предоставляет глобальную точку доступа
func GetInstance() *DatabaseConnection {
    // once.Do выполнит переданную функцию только один раз
    // при первом вызове GetInstance.
    once.Do(func() {
        fmt.Println("Creating database connection...")
        instance = &DatabaseConnection{connectionString: "user:pass@tcp(127.0.0.1:3306)/db"}
    })
    return instance
}

func main() {
    // Все вызовы будут возвращать один и тот же экземпляр
    conn1 := GetInstance()
    conn1.Query("SELECT * FROM users")

    conn2 := GetInstance()
    conn2.Query("SELECT * FROM products")
}
// Вывод:
// Creating database connection...
// Executing query 'SELECT * FROM users' using connection 'user:pass@tcp(127.0.0.1:3306)/db'
// Executing query 'SELECT * FROM products' using connection 'user:pass@tcp(127.0.0.1:3306)/db'

Преимущества:

  • Гарантия единственного экземпляра: Полезно для ресурсов, которые должны быть уникальными (пул соединений, логгер, конфигурация).
  • Ленивая инициализация (Lazy Initialization): Ресурс создается только тогда, когда он действительно нужен в первый раз.
  • Глобальный доступ: Легко получить доступ к экземпляру из любой части программы.

Недостатки и критика:

Singleton часто считают анти-паттерном в современной разработке:

  • Глобальное состояние: Вносит в программу глобальное состояние, что затрудняет отслеживание зависимостей и отладку.
  • Сложность тестирования: Модули, использующие Singleton, становятся тесно связанными с ним. Сложно подменить синглтон на mock-объект в unit-тестах.
  • Нарушение принципа единственной ответственности (SRP): Класс-синглтон отвечает не только за свою бизнес-логику, но и за контроль количества своих экземпляров.
  • Скрытые зависимости: Зависимость от синглтона не видна в сигнатуре функции или конструкторе, что делает код менее прозрачным.

Альтернатива: В большинстве случаев предпочтительнее использовать Внедрение зависимостей (Dependency Injection), так как это делает код более модульным, тестируемым и гибким.

Ответ 18+ 🔞

Вот, представляешь, есть такая штука — Singleton, или по-нашему Одиночка. Это такой паттерн, который смотрит на тебя и говорит: «Слышь, дружок, экземпляр этого класса будет один, и только один, на весь проект, и точка». А потом ещё любезно предлагает глобальную точку доступа к этому самому экземпляру, чтобы ты не бегал по коду как угорелый.

В Go, если делать по-человечески, это реализуется через пакет sync и его штуку Once, которая гарантирует, что инициализация произойдёт один раз и без гонок, даже если двадцать потоков одновременно налетят.

Пример на Go (чтобы не словить race condition):

package main

import (
    "fmt"
    "sync"
)

// Допустим, у нас есть какая-то важная хрень, которая должна быть в единственном экземпляре.
// Ну, типа подключение к базе данных или конфиг.
type DatabaseConnection struct {
    connectionString string
}

func (db *DatabaseConnection) Query(q string) {
    fmt.Printf("Executing query '%s' using connection '%s'n", q, db.connectionString)
}

var (
    instance *DatabaseConnection
    once     sync.Once
)

// GetInstance — это та самая дверь, в которую все стучатся за экземпляром.
func GetInstance() *DatabaseConnection {
    // once.Do — это магия. Функция внутри выполнится ровно один раз, даже если вызовов будет овердохуища.
    once.Do(func() {
        fmt.Println("Creating database connection...")
        instance = &DatabaseConnection{connectionString: "user:pass@tcp(127.0.0.1:3306)/db"}
    })
    return instance
}

func main() {
    // И неважно, сколько раз ты его вызываешь — вернётся один и тот же объект.
    conn1 := GetInstance()
    conn1.Query("SELECT * FROM users")

    conn2 := GetInstance()
    conn2.Query("SELECT * FROM products")
}
// Вывод:
// Creating database connection...
// Executing query 'SELECT * FROM users' using connection 'user:pass@tcp(127.0.0.1:3306)/db'
// Executing query 'SELECT * FROM products' using connection 'user:pass@tcp(127.0.0.1:3306)/db'

Что тут хорошего, спросишь?

  • Один экземпляр и ни хуя больше: Идеально для вещей, которые должны быть уникальными — типа того же пула соединений, логгера или конфигурации. Не будет ситуации, что у тебя два логгера пишут в разные файлы и потом разбирайся, чёрт ногу сломит.
  • Ленивая инициализация: Ресурс создастся только тогда, когда он впервые понадобится. Не будет грузить систему на старте просто так.
  • Глобальный доступ: Достал экземпляр из любой дыры в коде — и порядок.

А теперь про недостатки, потому что без них никуда:

Singleton частенько называют анти-паттерном, и не просто так, ёпта.

  • Глобальное состояние: Он вносит в программу глобальное состояние, а это, блядь, как мина замедленного действия. Отслеживать зависимости и отлаживать такой код — просто пиздец.
  • Тестирование превращается в ад: Модули, которые юзают синглтон, становятся с ним неразлучными. Подменить его на mock в unit-тестах — задача не для слабонервных.
  • Нарушает принцип единственной ответственности: Класс теперь отвечает не только за свою прямую работу, но и за то, чтобы экземпляр был один. Это уже перебор.
  • Скрытые зависимости: Зависимость от синглтона не видна в параметрах функции или конструкторе. Смотришь на сигнатуру — вроде чисто, а внутри него сидит и тихо управляет твоим кодом. Подло, да?

Что делать вместо этого? В большинстве случаев, честно, лучше использовать Внедрение зависимостей (Dependency Injection). Да, придётся немного больше писать, зато код станет модульным, тестируемым и гибким. А синглтон... ну, иногда его можно применить, но с оглядкой, как спичку в пороховом погребе.