Паттерн Singleton
Часть раздела Объектно-ориентированное программирование путешествия по GO на Coddy. Урок 86 из 107.
Паттерн Singleton обеспечивает наличие только одного экземпляра типа на протяжении всей программы и предоставляет глобальную точку доступа к нему. Это полезно для общих ресурсов, таких как менеджеры конфигурации, подключения к базам данных или logger.
В Go мы реализуем Singleton с помощью переменной уровня package в сочетании с 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 умеренно. Хотя это удобно, он вводит глобальное состояние, которое может усложнить тестирование и скрыть зависимости. Подумайте, не будет ли внедрение зависимостей лучшей альтернативой для вашего случая.
Задание
ЛегкоДавайте создадим Singleton Logger, который гарантирует, что во всём вашем приложении существует only one экземпляр Logger! Это классический вариант использования паттерна Singleton. Вы хотите, чтобы все части вашей программы использовали одну и ту же конфигурацию и состояние Logger.
Вы организуете код в двух файлах:
logger.go: Implement потокобезопасный Singleton Logger.Создайте структуру
Loggerс двумя полями:Prefix(string) для префиксов сообщений журнала иMessageCount(int) для отслеживания того, сколько сообщений было logged.Используйте переменные уровня package с
sync.Once, чтобы ensure, что only one instance когда-либо создаётся. Implement функциюGetLogger(), которая инициализирует Logger с префиксом default"[LOG]"и возвращает Singleton instance.Добавьте следующие методы в ваш Logger:
SetPrefix(prefix string)— изменяет префикс LoggerLog(message string) string— increments счётчик сообщений и возвращает formatted строку:[prefix] #[count]: [message]GetCount() int— возвращает общее number сообщений, которые были logged
main.go: Продемонстрируйте, что multiple вызовыGetLogger()возвращают тот же instance.Прочитайте новое значение префикса, затем прочитайте count сообщений, за которым следует each сообщение для журналирования.
Получите instance Logger, установите custom префикс, затем logged каждое сообщение и выведите результат. После журналирования всех сообщений снова получите instance Logger (чтобы доказать, что это тот же самый instance) и выведите общее number сообщений.
Будут предоставлены следующие входные данные:
- Строка 1: Custom префикс для установки
- Строка 2: Number сообщений
- Следующие строки: Each сообщение для журналирования
Например, если даны:
[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(), вы always получаете один и тот же instance с общим состоянием. Счётчик сообщений сохраняется, потому что существует only one Logger!
Попробуйте сами
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: Получить экземпляр логгера снова (чтобы доказать, что это тот же самый)
// and print the total message count in format: "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Потренируйтесь самостоятельно: Онлайн-компилятор Go