Когда в Go срабатывает init()? Разбор на примерах

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Когда в 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                

Блоги

Рекомендаций по книгам