Когда в Go запускается init()? Разбираем на реальном примере
Представьте: вы пишете микросервис на Go. В одном из пакетов вы объявили init(), чтобы прогрузить конфиг. Запускаете приложение — и вдруг оно падает с nil pointer dereference. Вы в панике: "Почему? Конфиг же должен был загрузиться первым!"
Знакомо? Если да, то вы не одиноки. Вопрос "When does the init() function run?" — один из самых популярных на Stack Overflow (тысячи просмотров). Давайте раз и навсегда разберёмся, когда и в каком порядке срабатывает init() в Go, чтобы вы больше не попадали в эту ловушку.
Что такое init() и зачем он нужен?
В Go init() — это специальная функция без параметров и возвращаемого значения. Она не вызывается явно. Её запускает рантайм Go автоматически перед выполнением функции main().
Зачем это нужно?
- Инициализация глобальных переменных, которые требуют сложной логики (например, чтение из файла).
- Регистрация драйверов баз данных или HTTP-обработчиков.
- Проверка окружения (переменные среды, доступность ресурсов).
Но главное правило: вы никогда не должны полагаться на порядок выполнения init() между разными файлами одного пакета. И вот почему.
Когда именно срабатывает init()? Точный порядок
Go выполняет init() по строгим правилам. Давайте разложим их по полочкам.
Правило №1: Один пакет — один порядок
Внутри одного пакета init() выполняется в порядке объявления файлов (по алфавиту имён файлов). Но это деталь реализации — на неё лучше не закладываться.
Правило №2: Сначала инициализация переменных, потом init()
Перед вызовом init() Go инициализирует все глобальные переменные в пакете. Если переменная зависит от другой переменной из того же пакета — порядок их объявления имеет значение.
package config
var AppName string = "MyApp"
var Version string = getVersion() // вызовется до init()
func init() {
// Здесь уже доступны AppName и Version
println("Init started for", AppName)
}
func getVersion() string {
return "1.0.0"
}
Правило №3: Импортированные пакеты инициализируются первыми
Это самое важное правило. Если пакет A импортирует пакет B, то сначала выполнятся все init() в пакете B, и только потом — в A. Go строит граф зависимостей и выполняет инициализацию "снизу вверх".
// Пакет database
package database
func init() {
println("1. database init")
}
// Пакет main (импортирует database)
package main
import "myapp/database"
func init() {
println("2. main init")
}
func main() {
println("3. main function")
}
// Вывод:
// 1. database init
// 2. main init
// 3. main function
Правило №4: Несколько init() в одном файле — можно, но не нужно
В одном файле может быть несколько init(). Они выполнятся в порядке объявления. Но это усложняет чтение кода. Лучше объединить логику в одну функцию.
Живой пример: что будет, если ошибиться с порядком?
Давайте смоделируем типичную ошибку новичка. Предположим, у нас есть пакет config, который читает настройки из файла, и пакет app, который эти настройки использует.
// config.go
package config
var Settings map[string]string
func init() {
// Имитируем чтение файла
Settings = map[string]string{
"port": "8080",
"host": "localhost",
}
println("Config loaded")
}
// app.go
package app
import "myapp/config"
var ServerPort string
func init() {
// ОШИБКА: если config.Settings ещё не инициализирован?
ServerPort = config.Settings["port"]
println("App init, port:", ServerPort)
}
// main.go
package main
import "myapp/app"
func main() {
println("Server will run on port:", app.ServerPort)
}
Сработает ли этот код? Да, сработает. Потому что config инициализируется раньше app (благодаря правилу №3). Но если бы в app был импорт с циклической зависимостью — Go бы просто не скомпилировал программу.
А вот что будет, если вы забудете импортировать config в app:
// app.go (неправильная версия)
package app
// Нет импорта config!
var ServerPort string = config.Settings["port"] // Ошибка компиляции
Go выдаст ошибку: undefined: config. Мораль: явно импортируйте пакеты, от которых зависит инициализация.
Топ-3 частых ошибок с init()
- Попытка вызвать
init()вручную. Не делайте этого. Компилятор разрешит, но это не имеет смысла — функция выполнится только один раз при старте. - Зависимость от порядка
init()в разных файлах одного пакета. Если вам нужно управлять порядком — выносите логику в отдельные функции и вызывайте их явно. - Использование
init()для долгих операций.init()блокирует запускmain(). Если внутри init() есть HTTP-запрос или чтение большого файла — приложение будет стартовать медленно.
Когда init() — это плохая практика?
Опытные Go-разработчики стараются избегать init() в production-коде. Почему?
- Тестирование. Переменные, инициализированные в
init(), сложно подменить в тестах. - Прозрачность. Код, который "сам себя запускает", сложнее отлаживать.
- Порядок выполнения. Чем больше
init()в проекте, тем выше риск неожиданного поведения.
Альтернатива: использовать фабричные функции или явную инициализацию в main().
// Вместо init()
func LoadConfig() (*Config, error) {
// явная инициализация
return &Config{Port: "8080"}, nil
}
func main() {
cfg, err := LoadConfig()
if err