Go имеет строгие правила инициализации пакетов, переменных и функций при запуске программы. Основной механизм — выполнение init-функций и инициализация глобальных переменных. Правильное понимание этих процессов важно для предотвращения ошибок и неожиданных эффектов.
История вопроса:
В Go с самого начала было введено строгое разделение фаз старта: объявление, инициализация и дальнейшее выполнение кода. В языках типа C/C++ часто используются конструкторы глобальных переменных, в Go же порядок инициализации детерминирован, но есть свои нюансы.
Проблема:
Легко попасть в ловушку, когда инициализация глобальных переменных или вызов init приводит к взаимозависимым или циклическим ситуациям между пакетами. Это сложно отследить, а программы могут вести себя не так, как ожидает разработчик, особенно при скрытых зависимостях или герметизации state на старте.
Решение:
Пакеты в Go инициализируются в порядке, определяемом их зависимостями: сначала зависимости, потом сам пакет. Сначала инициализируются package-level переменные (в порядке появления в исходном файле), затем вызывается любая функция init(), если такая есть. Можно объявлять несколько init() в одном файле. Порядок инициализации между файлами одного пакета не определён (и это может привести к ошибкам).
Пример кода:
// a.go package main import "fmt" func init() { fmt.Println("init from a.go") } // b.go package main import "fmt" func init() { fmt.Println("init from b.go") }
Результат исполнения этих init-функций не предсказуем между файлами одной директории, но всегда до функции main().
Ключевые особенности:
Можно ли положиться на порядок выполнения init-функций в разных файлах одного пакета?
Нет! Go не гарантирует порядок между init-функциями разных файлов в одном пакете. Надежды на определённый порядок могут вылиться в трудноуловимые ошибки и рассыпание бизнес-логики.
Могут ли глобальные переменные быть не инициализированы к моменту выполнения init-функции?
Нет — все global переменные пакета выполняются строго в порядке объявления до всех init-функций этого пакета. Исключения — лишь перекрёстные инициализации между пакетами (см. ниже).
Как избежать циклических зависимостей init между пакетами?
Go не допускает циклических импортов на уровне пакетов (это compile-time ошибка), но можно попасть в ловушку косвенной инициализации: A зависит от B, B — от C, а C (через глобальную переменную или init) вызывает код из A. В подобных случаях может возникать неочевидный порядок вызова init/глобальных конструкторов.
В команде логики инициализации сервисов выполнены в нескольких init-функциях разных файлов. Одна init зависит от результата другой, что влечёт случайное поведение между сборками и запуском на разных серверах.
Плюсы:
Минусы:
Всё состояние и инициализация выполнены явными вызовами в main(). init-функции используются исключительно для трейсинга запуска и небольших проверок.
Плюсы:
Минусы: