Паттерн Singleton
Часть раздела Объектно-ориентированное программирование путешествия по GO на Coddy — урок 86 из 107.
Паттерн Одиночка (Singleton) гарантирует, что у типа есть только один экземпляр во всей программе, и предоставляет глобальную точку доступа к нему. Это полезно для общих ресурсов, таких как менеджеры конфигурации, подключения к базе данных или логгеры.
В Go мы реализуем Singleton, используя переменную на уровне пакета в сочетании с sync.Once для обеспечения потокобезопасной инициализации:
package config
import "sync"
type Config struct {
DatabaseURL string
MaxRetries int
}
var (
instance *Config
once sync.Once
)
func GetInstance() *Config {
once.Do(func() {
instance = &Config{
DatabaseURL: "localhost:5432",
MaxRetries: 3,
}
})
return instance
}sync.Once гарантирует, что функция инициализации выполнится ровно один раз, даже если несколько горутин вызывают GetInstance() одновременно. Каждый последующий вызов возвращает тот же экземпляр без повторного выполнения кода инициализации.
cfg1 := config.GetInstance()
cfg2 := config.GetInstance()
// cfg1 и cfg2 указывают на один и тот же экземплярИспользуйте Singleton с осторожностью. Несмотря на удобство, он вводит глобальное состояние, которое может усложнить тестирование и скрыть зависимости. Рассмотрите, не будет ли внедрение зависимостей (dependency injection) лучшей альтернативой для вашего случая.
Задание
ЛегкоДавайте создадим синглтон Logger, который гарантирует существование только одного экземпляра логгера во всем вашем приложении! Это классический вариант использования паттерна Одиночка (Singleton) — вы хотите, чтобы все части вашей программы использовали одну и ту же конфигурацию и состояние логгера.
Вы организуете свой код в двух файлах:
logger.go: Реализуйте свой потокобезопасный логгер-синглтон.Создайте структуру
Loggerс двумя полями:Prefix(string) для префиксов лог-сообщений иMessageCount(int) для отслеживания количества залогированных сообщений.Используйте переменные уровня пакета вместе с
sync.Once, чтобы гарантировать создание только одного экземпляра. Реализуйте функциюGetLogger(), которая инициализирует логгер с префиксом по умолчанию"[LOG]"и возвращает экземпляр синглтона.Добавьте следующие методы в ваш Logger:
SetPrefix(prefix string)— изменяет префикс логгераLog(message string) string— увеличивает счетчик сообщений и возвращает отформатированную строку:[prefix] #[count]: [message]GetCount() int— возвращает общее количество залогированных сообщений
main.go: Продемонстрируйте, что несколько вызововGetLogger()возвращают один и тот же экземпляр.Считайте новое значение префикса, затем считайте количество сообщений, а после — каждое сообщение для логирования.
Получите экземпляр логгера, установите пользовательский префикс, затем залогируйте каждое сообщение и выведите результат. После логирования всех сообщений снова получите экземпляр логгера (чтобы доказать, что это тот же самый экземпляр) и выведите общее количество сообщений.
Будут предоставлены следующие входные данные:
- Строка 1: Пользовательский префикс для установки
- Строка 2: Количество сообщений
- Следующие строки: Каждое сообщение для логирования
Например, при вводе:
[APP]
3
Server started
User connected
Request processedВаш вывод должен быть:
[APP] #1: Server started
[APP] #2: User connected
[APP] #3: Request processed
Total messages logged: 3И при вводе:
[DEBUG]
2
Initializing cache
Cache readyВаш вывод должен быть:
[DEBUG] #1: Initializing cache
[DEBUG] #2: Cache ready
Total messages logged: 2И при вводе:
[ERROR]
1
Connection failedВаш вывод должен быть:
[ERROR] #1: Connection failed
Total messages logged: 1Ключевая идея заключается в том, что сколько бы раз вы ни вызывали GetLogger(), вы всегда получаете один и тот же экземпляр с его общим состоянием. Счетчик сообщений сохраняется, потому что существует только один Logger!
Шпаргалка
Паттерн Singleton (Одиночка) гарантирует, что у типа будет только один экземпляр на протяжении всей программы, и предоставляет глобальную точку доступа к нему. Это полезно для общих ресурсов, таких как менеджеры конфигурации, подключения к базе данных или логгеры.
Реализуйте Singleton, используя переменную на уровне пакета в сочетании с sync.Once для потокобезопасной инициализации:
package config
import "sync"
type Config struct {
DatabaseURL string
MaxRetries int
}
var (
instance *Config
once sync.Once
)
func GetInstance() *Config {
once.Do(func() {
instance = &Config{
DatabaseURL: "localhost:5432",
MaxRetries: 3,
}
})
return instance
}sync.Once гарантирует, что функция инициализации выполнится ровно один раз, даже если несколько горутин вызывают GetInstance() одновременно. Каждый последующий вызов возвращает тот же самый экземпляр:
cfg1 := config.GetInstance()
cfg2 := config.GetInstance()
// cfg1 и cfg2 указывают на один и тот же экземплярИспользуйте Singleton с осторожностью, так как он вводит глобальное состояние, которое может усложнить тестирование и скрыть зависимости. Рассмотрите внедрение зависимостей (dependency injection) в качестве альтернативы.
Попробуйте сами
package main
import (
"bufio"
"fmt"
"os"
"strconv"
"strings"
)
func main() {
reader := bufio.NewReader(os.Stdin)
// Прочитать пользовательский префикс
prefix, _ := reader.ReadString('\n')
prefix = strings.TrimSpace(prefix)
// Прочитать количество сообщений
countStr, _ := reader.ReadString('\n')
countStr = strings.TrimSpace(countStr)
count, _ := strconv.Atoi(countStr)
// Прочитать каждое сообщение
messages := make([]string, count)
for i := 0; i < count; i++ {
msg, _ := reader.ReadString('\n')
messages[i] = strings.TrimSpace(msg)
}
// TODO: Получить экземпляр логгера с помощью GetLogger()
// TODO: Установить пользовательский префикс с помощью SetPrefix()
// TODO: Залогировать каждое сообщение и вывести результат
// TODO: Снова получить экземпляр логгера (чтобы доказать, что это тот же самый экземпляр)
// и вывести общее количество сообщений в формате: "Total messages logged: X"
}
В этом уроке есть небольшой тест. Начните урок, чтобы ответить на вопросы и сохранить прогресс.
Все уроки раздела Объектно-ориентированное программирование
1Основы ООП в Go
Внешние файлыРабочее пространство и модули GoПакеты и импортыЭкспортируемые и неэкспортируемые именаВведение в ООП в GoСтруктуры как классыОпределение методов структурПолучатели-указатели и получатели-значенияИнициализация структурФункции-конструкторыИтоги — Простой калькулятор4Интерфейсы
Введение в интерфейсыНеявная реализацияИнтерфейс как контрактПустой интерфейс (any)Утверждение типаПереключатель типовКомпозиция интерфейсовИнтерфейсы Stringer и ErrorПовторение: Калькулятор фигур7Инкапсуляция
Экспортируемые и неэкспортируемые поляИнкапсуляция на уровне пакетовГеттеры и сеттерыСокрытие информации в GoИтоги — Записи о студентах10Обобщения (Generics) (Go 1.18+)
Введение в GenericsПараметры типовОграничения типовОбобщенные структурыОбходной путь для обобщенных методовИтоги — Обобщенная коллекция13Паттерны проектирования. Часть 1
Введение в паттерны проектированияПаттерн SingletonПаттерн FactoryПаттерн Abstract FactoryПаттерн ObserverПаттерн Strategy2Глубокое погружение в типы и структуры
Базовые и составные типыОпределение пользовательских типовТеги структурАнонимные структурыВложенные структурыНулевые значения и значения по умолчаниюПовторение — Контактная книга5Композиция вместо наследования
Почему в Go нет наследованияОсновы встраивания структурПродвижение методовВстраивание нескольких структурВстраивание против агрегацииЗатенение встроенных методовИтоги — Иерархия сотрудников8Обработка ошибок и ООП
Интерфейс errorПользовательские типы ошибокОбертывание ошибок (fmt.Errorf)Sentinel-ошибкиerrors.Is() и errors.As()Panic, Defer и RecoverИтоги — Парсер файлов11Стандартная библиотека и ООП
io.Reader и io.Writersort.InterfaceИнтерфейс fmt.Stringerencoding/json со структурамиИнтерфейс http.HandlerПовторение: модели REST API14Паттерны проектирования. Часть 2
Паттерн КомандаПаттерн АдаптерПаттерн ДекораторПаттерн Шаблонный методПаттерн СостояниеПаттерн КомпоновщикMiddleware как Декоратор3Указатели и память
Основы указателей в GoУказатели на структурыПередача по значению и по ссылкеФункция new()Сборка мусора в GoПовторение: Конструктор связного списка6Полиморфизм в Go
Полиморфизм через интерфейсыУтиная типизация в GoПравила реализации интерфейсовПолиморфные коллекцииВнедрение зависимостейИтоги — Обработчик платежей9Конкурентность и ООП
Основы горутинКаналы и взаимодействиеБуферизованные и небуферизованные каналыОператор selectsync.Mutex и sync.RWMutexsync.WaitGroupПроектирование потокобезопасных структурПовторение — Worker Pool