Avatar ·

Когда и в каком порядке выполняется функция init() в Go?

Всем привет! Я только начал изучать Go и столкнулся с непонятным моментом. В документации написано, что в пакетах можно объявлять функцию `init()`, которая выполняется автоматически. Но я никак не могу понять, когда именно она запускается и в каком порядке, если таких функций несколько. У меня есть небольшой проект из трёх файлов. В пакете `main` я определил функцию `init()`, которая печатает "main init". В другом файле того же пакета я объявил ещё одну `init()`, которая увеличивает счётчик. В импортированном пакете `config` тоже есть своя `init()`, которая читает настройки. Я ожидал, что они выполнятся в каком-то логичном порядке, но при запуске программы вывод оказался неочевидным. Вот пример кода: ```go // main.go package main import "fmt" var counter int func init() { fmt.Println("First init in main") counter = 10 } func main() { fmt.Println("Main function, counter =", counter) } ``` ```go // extra.go (тоже package main) package main import "fmt" func init() { fmt.Println("Second init in main") counter = 20 } ``` Когда я запускаю программу, я вижу: ``` First init in main Second init in main Main function, counter = 20 ``` Почему сначала выполняется первый init, а потом второй? Как Go определяет этот порядок? И что будет, если я импортирую пакет, в котором тоже есть `init()`? Когда выполнится его init — до или после моих? Можете объяснить на пальцах, как работает инициализация в Go? Заранее спасибо! <<>>

Когда и в каком порядке выполняется init() в Go?

Функция init() — это специальная функция в Go, которая выполняется автоматически до функции main(). Она предназначена для инициализации состояния пакета: установки переменных, проверки конфигураций, регистрации драйверов и т.д. Важно понимать, что init() нельзя вызывать явно — она запускается рантаймом.

Порядок выполнения init()

Правила простые, но их нужно запомнить:

  • Сначала — init() импортированных пакетов. Если ваш пакет main импортирует пакет config, то init() из config выполнится перед любыми init() в main.
  • Внутри одного пакета — в порядке появления в исходниках. Go компилирует файлы в алфавитном порядке имён файлов (это деталь реализации, но на практике так). Поэтому init() из файла, который идёт раньше по алфавиту, выполнится первым.
  • Если несколько init() в одном файле — они выполняются в порядке их объявления.

В вашем примере:

// main.go — идёт раньше по алфавиту, чем extra.go
// extra.go — идёт позже

Поэтому сначала выполняется init() из main.go, потом из extra.go. Переменная counter сначала получает значение 10, затем перезаписывается на 20. Именно поэтому в main() вы видите counter = 20.

Пример с импортированным пакетом

Допустим, у вас есть пакет config:

// config/config.go
package config

import "fmt"

var AppName string

func init() {
    fmt.Println("Config init: loading settings...")
    AppName = "MyApp"
}

И ваш main.go:

package main

import (
    "fmt"
    "yourmodule/config"
)

var counter int

func init() {
    fmt.Println("Main init")
    counter = config.AppName // используем данные из config
}

func main() {
    fmt.Println("App:", config.AppName, "Counter:", counter)
}

Вывод будет:

Config init: loading settings...
Main init
App: MyApp Counter: MyApp

Как видите, init() из config выполнился первым, потому что пакет config импортируется. Затем — init() из main.

Важные нюансы

  • init() не может принимать аргументы и возвращать значения. Это void-функция.
  • Вы не можете вызвать init() вручную — компилятор выдаст ошибку.
  • Если пакет импортируется, но не используется напрямую (только ради побочного эффекта init), используйте пустой импорт: import _ "some/package".
  • Порядок инициализации переменных пакета — сначала инициализируются переменные верхнего уровня (в порядке их объявления), потом выполняются init().

Как проверить порядок на практике

Вы можете использовать флаг -x при сборке или просто добавить логирование в каждый init. Но помните: полагаться на порядок init() в рамках одного пакета — плохая практика. Лучше выносить логику инициализации в явные функции, которые вызываются из main().

В итоге: init() выполняется один раз при запуске программы, до main(), в строго определённом порядке: сначала импортированные пакеты (рекурсивно), затем текущий пакет (в порядке файлов по алфавиту).

<<>>

Дополнение: как избежать зависимости от порядка init()

В больших проектах полагаться на порядок выполнения init() может быть опасно. Если вам нужно, чтобы один init() выполнялся строго после другого, лучше сделать это явно.

Вместо двух init() в разных файлах:

// Не делайте так:
func init() { setupA() }
func init() { setupB() } // B может выполниться до A

Используйте явную функцию инициализации:

// Инициализация по порядку:
func init() {
    setupA()
    setupB() // B гарантированно после A
}

Или, что ещё лучше, вынесите инициализацию в отдельную функцию, вызываемую из main():

func main() {
    if err := initialize(); err != nil {
        log.Fatal(err)
    }
    // ... основной код
}

Такой подход даёт полный контроль над порядком и упрощает тестирование. Используйте init() только для простых, однозначных инициализаций (например, регистрация драйвера БД), где порядок не важен.