Menu
Coddy logo textTech

Паттерн 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) лучшей альтернативой для вашего случая.

challenge icon

Задание

Легко

Давайте создадим синглтон 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"
}
quiz iconПроверьте себя

В этом уроке есть небольшой тест. Начните урок, чтобы ответить на вопросы и сохранить прогресс.

Все уроки раздела Объектно-ориентированное программирование