Avatar ·

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

Всем привет! Я только начал изучать Go и наткнулся на функцию `init()`. В документации сказано, что она выполняется автоматически, но я не могу понять: когда именно? И что будет, если у меня несколько файлов в пакете, в каждом есть init()? А если подключить стороннюю библиотеку, у которой тоже есть init()? Вот мой пример. Есть пакет `database` с файлом `db.go`: ```go package database import "fmt" var Connection string func init() { fmt.Println("Инициализация БД...") Connection = "postgres://user:pass@localhost/mydb" } ``` И файл `main.go`: ```go package main import ( "fmt" "myproject/database" ) func init() { fmt.Println("Мой init в main") } func main() { fmt.Println(database.Connection) } ``` Я ожидал, что сначала выполнится init в database, потом в main, а потом main(). Но так ли это всегда? Что если я импортирую пакет, который импортирует другой пакет с init()? Порядок гарантирован? И можно ли положиться на init() для каких-то критичных вещей, например, для открытия соединения с БД? Заранее спасибо за разъяснения! <<>>

Порядок выполнения init() в Go: полное руководство

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

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

Порядок строго регламентирован и гарантирован:

  1. Сначала выполняются init() всех импортированных пакетов — рекурсивно, начиная с самых глубоких зависимостей.
  2. Затем init() в текущем пакете — в порядке объявления в исходных файлах.
  3. И только потом main().

Если пакет импортируется из нескольких мест, его init() выполняется только один раз. Порядок init() внутри одного файла — сверху вниз. Если в пакете несколько файлов, порядок определяется лексикографически (по имени файла).

Пример на вашем коде

// database/db.go
package database
import "fmt"
var Connection string
func init() {
    fmt.Println("1. database init")
    Connection = "postgres://user:pass@localhost/mydb"
}

// main.go
package main
import (
    "fmt"
    "myproject/database"
)
func init() {
    fmt.Println("2. main init")
}
func main() {
    fmt.Println("3. main function")
    fmt.Println(database.Connection)
}

Вывод будет строго:

1. database init
2. main init
3. main function
postgres://user:pass@localhost/mydb

Важные правила и подводные камни

  • Не полагайтесь на init() для критичных операций — например, открытия соединения с БД. Если init() упадет с паникой, программа завершится без возможности graceful shutdown. Лучше использовать sync.Once или явный вызов в main().
  • Порядок между файлами в одном пакете — по алфавиту. Если у вас a.go и b.go, init() из a.go выполнится первым. Не пишите код, который зависит от этого порядка — это хрупко.
  • Импорт с пустым идентификатором — import _ "package" запускает init() пакета, но не дает доступа к его экспортируемым именам. Используется для регистрации драйверов (например, database/sql).
  • Циклические импорты — Go запрещает циклические зависимости между пакетами, поэтому ситуация "A импортирует B, B импортирует A" невозможна.

Рекомендации

  • Используйте init() для простой инициализации: установка глобальных переменных, проверка переменных окружения, регистрация обработчиков.
  • Для сложной логики (подключение к БД, загрузка конфигов) используйте явные функции, вызываемые из main().
  • Помните, что init() не может быть вызван явно — только автоматически.

В вашем случае код корректен, но для продакшена лучше вынести инициализацию БД в отдельную функцию Connect() и вызывать её в main():

// database/db.go
var Connection string
func Connect() error {
    Connection = "postgres://user:pass@localhost/mydb"
    return nil
}

// main.go
func main() {
    if err := database.Connect(); err != nil {
        log.Fatal(err)
    }
    fmt.Println(database.Connection)
}
<<>>

Дополнительные нюансы: порядок в сложных проектах

Первый ответ — верный. Хочу добавить несколько практических моментов, которые часто упускают новички.

Порядок при множественных импортах. Если пакет A импортирует B и C, а B импортирует D, порядок будет: D → B → C → A. Но если C тоже импортирует D, то D всё равно выполнится только один раз — до B и C. Go гарантирует топологическую сортировку зависимостей.

Тестирование и init(). В тестовых файлах (*_test.go) тоже можно объявлять init(). Они выполняются перед каждым тестовым запуском. Это удобно для установки тестовых данных, но может замедлить тесты, если init() тяжелый.

Инициализация глобальных переменных. Помните, что глобальные переменные инициализируются ДО вызова init(). Например:

var Config = loadConfig()  // выполнится до init()
func init() { /* тут Config уже доступна */ }

Это может быть альтернативой init() — используйте инициализаторы переменных, если логика простая. Но init() нужен, когда логика сложнее одного выражения (например, цикл или несколько шагов).

Итог: init() — мощный инструмент, но используйте его осознанно. Для большинства случаев предпочтительнее явная инициализация в main().