При параллельном запуске нескольких горутин инициализация должна выполниться ровно один раз: какой примитив Go выбрать и почему?
Используйте sync.Once. Он гарантирует, что переданная функция будет выполнена не более одного раза даже при одновременном вызове из нескольких горутин, а остальные вызовы дождутся завершения первой инициализации.
sync.Once подходит для ленивой инициализации общего ресурса: конфигурации, клиента, таблицы или другого состояния. Это не просто защита от повторного входа: примитив также обеспечивает корректную публикацию результата инициализации другим горутинам.
В конкурентных программах часто требуется создать общий ресурс только при первом обращении. Наивная проверка логического флага недостаточна: несколько горутин могут одновременно увидеть значение «не инициализировано» и запустить инициализацию повторно.
sync.Once предоставляет стандартный механизм для этой задачи без ручного комбинирования флага, мьютекса и дополнительных проверок. Он выражает намерение программы явно: операция должна произойти один раз за время жизни конкретного объекта Once.
Если несколько горутин одновременно вызывают инициализацию без синхронизации, возможны повторное создание ресурса, гонка данных, потеря обновлений или наблюдение частично подготовленного состояния. Обычная проверка флага сама по себе не делает проверку и запись атомарными.
Даже при использовании мьютекса легко допустить ошибку: забыть блокировку на одном из путей, неправильно обработать ошибку или вернуть объект до завершения его настройки. Неверное решение может проявляться только под нагрузкой и быть трудно воспроизводимым.
sync.Once хранит внутреннее состояние выполнения и синхронизирует конкурирующие вызовы метода Do. Ровно одна горутина выполняет функцию, переданную в Do; остальные не выполняют её повторно и возвращаются только после завершения первой инициализации.
Минимальный пример:
Надпись initialized появится один раз. После возврата из Do остальные горутины могут безопасно использовать результат инициализации, поскольку завершение функции внутри Do синхронизировано с последующими вызовами Do.
Объект sync.Once нельзя копировать после начала использования. Поэтому его обычно хранят как поле структуры или package-level переменную и передают по ссылке вместе с владельцем состояния.
sync.Once не поддерживает сброс. Если ресурс нужно переинициализировать, проектируют новый экземпляр состояния с новым Once; попытка вручную обнулить использованный Once приводит к ошибкам синхронизации.
Если функция внутри Do завершилась паникой, последующие вызовы не запускают её повторно. Поэтому sync.Once не подходит для автоматических повторных попыток. Для инициализации с ошибкой обычно сохраняют ошибку внутри функции, а затем возвращают один и тот же результат всем вызывающим.
Функция инициализации не должна рекурсивно вызывать Do того же объекта: это приводит к взаимной блокировке. Если инициализация требует сложного жизненного цикла, отмены или повторных попыток, могут быть уместнее мьютекс, отдельная машина состояний или явно управляемый канал событий.
Сервис лениво создаёт клиент внешней системы при первом запросе. При всплеске нагрузки десятки горутин одновременно обращаются к клиенту.
Рассматривались следующие варианты:
sync.Mutex универсален, однако требует вручную защищать состояние, правильно выбирать область блокировки и обрабатывать повторные вызовы.sync.Once точно соответствует требованию «инициализировать один раз» и не заставляет поддерживать лишнюю инфраструктуру.Выбран sync.Once, а созданный клиент и ошибку инициализации сохраняют в общих переменных, доступ к которым получают только после вызова Do. В результате клиент создаётся один раз, конкурентные запросы не видят частично настроенный объект, а логика синхронизации остаётся локальной и проверяемой.
Do вернёт ошибку?sync.Once не анализирует возвращаемые значения, поскольку функция для Do ничего не возвращает. Если инициализация может завершиться ошибкой, её сохраняют во внешней переменной внутри функции Do, например как единственную ошибку инициализации, а вызывающий код после Do проверяет это значение. Повторной попытки автоматически не будет, поэтому для retry-механизма нужен другой протокол.
Do?Нет, сам факт наличия sync.Once рядом с переменной не синхронизирует произвольное чтение. Каждый путь, который использует результат, должен сначала вызвать Do или получить значение через функцию-обёртку, гарантирующую завершение инициализации. Иначе возможны гонка данных и наблюдение неинициализированного состояния.
Do из функции, переданной в Do?Внешний вызов ещё не завершён, пока функция инициализации не вернётся. Повторный вызов того же Do ждёт завершения текущего выполнения, но текущее выполнение ждёт возврата вложенного вызова, поэтому возникает взаимная блокировка. Рекурсивную инициализацию следует разнести по отдельным функциям или использовать отдельные объекты синхронизации, если это действительно необходимо.