ПрограммированиеSenior Go разработчик

В чем особенности работы с глобальными переменными в Go, как избежать их некорректной инициализации и конкуренции во времени запуска (init order)? В чем заключаются типичные ловушки?

Проходите собеседования с ИИ помощником Hintsage

Ответ.

В Go глобальные переменные инициализируются в порядке объявления файлов и последовательности их init-функций. От этого зависит корректность обращения к глобальному состоянию: если переменная в одном пакете зависит от значения переменной другого пакета, но этот пакет еще не был инициализирован — возникнет ошибка или неочевидное поведение.

Прямого объявления "стартового порядка" между init-функциями разных пакетов нельзя задать. Компилятор определяет этот порядок исходя из графа импорта.

  • Все глобальные переменные инициализируются до вызова функции main.main().
  • Не рекомендуется делать инициализацию состояния с чрезмерными зависимостями во время package init!
  • Для потокобезопасности глобальных переменных используют sync.Once или mutexы при сложной инициализации.

Пример безопасной отложенной инициализации (lazy init):

var cfg *Config var once sync.Once func GetConfig() *Config { once.Do(func() { cfg = LoadConfigFromDisk() }) return cfg }

Вопрос с подвохом.

Вопрос: Могут ли init-функции разных пакетов выполняться одновременно (параллельно) при запуске программы на Go?

Правильный ответ: Нет, init-функции и глобальные переменные выполняются строго последовательно в порядке определённого компилятором при анализе зависимостей между пакетами. Параллельного запуска init-функций не происходит.


Примеры реальных ошибок из-за незнания тонкостей темы.


История

В крупном проекте несколько модулей грузили глобальные конфиги с диска через init(). Один модуль зависел от другого, но в силу переустройства графа go.mod порядок инициализации поменялся — и внезапно значения конфига оказались пустыми! Ошибка проявлялась случайным образом, зависящим от порядка сборки, и нашлась только после разбора зависимостей и перевода инициализации в явную функцию.


История

В REST-сервисе для кэширования справочников использовался глобальный map без mutex. Инициализация начиналась из нескольких goroutine, стартовавших в init. Как результат — data race, периодические паники, невалидные данные внутри map. После замены на sync.Once и явного вызова инициализации проблема ушла.


История

Глобальный логгер создавался в init-функции одного из вспомогательных пакетов, однако другой пакет обращался к этому логгеру тоже при старте, до его инициализации. В итоге часть логов о старте терялась, пока логгер ещё не существовал. Решение — инъекция зависимостей и явная инициализация логгера до запуска всех сервисов.