Как реализовать паттерн singleton в Go?
Что проверяют
Знаете ли вы идиоматичный для Go способ ленивой инициализации и понимаете ли, почему сам паттерн в Go используют осторожно. Следом обычно спрашивают, что будет, если инициализация вернула ошибку, и как такой код тестировать.
Как отвечать
Сначала простейший вариант: переменная уровня пакета, инициализированная при объявлении или в init(). Go гарантирует, что инициализация пакета выполняется один раз и до того, как им начнут пользоваться, поэтому гонки здесь нет. Минус: объект создаётся при старте, даже если не понадобится.
Для ленивого создания стандартный инструмент sync.Once:
once.Do(f):fвыполнится ровно один раз, остальные вызовы из любых горутин дождутся её завершения и увидят готовый объект;sync.OnceValueиsync.OnceValues(с Go 1.21): оборачивают функцию-конструктор и возвращают функцию-геттер.OnceValuesудобен, когда конструктор возвращает(T, error);- мьютекс на каждый вызов: работает, но каждый доступ платит за блокировку. Для singleton это лишнее.
Дальше скажите о цене паттерна. Глобальный экземпляр это скрытая зависимость: функция, которая внутри зовёт GetInstance(), не показывает в сигнатуре, что ей нужна база или конфиг, и в тестах такую зависимость не подменить. Поэтому в Go чаще создают объект один раз в main и передают через конструкторы (NewService(db *sql.DB)), а интерфейс принимают там, где нужна подмена. Singleton оставляют для действительно глобального: логгер, метрики, пул соединений в маленькой утилите.
Где спотыкаются
Пишут double-checked locking: проверка instance == nil без блокировки, потом мьютекс. Без атомиков это гонка, и go test -race её поймает. Именно эту задачу и решает sync.Once.
Не учитывают поведение при ошибке. sync.Once считает функцию выполненной, даже если она запаниковала или вернула ошибку, повторной попытки не будет. OnceValues тоже закэширует ошибку навсегда, поэтому для подключений с ретраями нужна своя логика.
Возвращают неэкспортируемый тип из экспортируемой функции, как func GetInstance() *singleton. Пользоваться таким API неудобно, линтеры на это ругаются, лучше экспортируемый тип или интерфейс.