Как работает функция init в Go

В Go есть предопределенная функция init(). Она выделяет фрагмент кода, который должен выполняться перед всеми другими частями пакета. Этот код будет выполняться сразу после импорта пакета. Его можно использовать по необходимости, чтобы приложение инициализировалось в определенном состоянии (например, когда у вас есть конкретная конфигурация или набор ресурсов, с которыми должно запускаться ваше приложение). Также функция init() используется для импорта побочного эффекта – это техника, которая используется для установки состояния программы путем импорта определенного пакета. С ее помощью часто один пакет регистрируют в другом, чтобы программа рассматривала правильный код для задачи.

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

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

Требования

Чтобы выполнить отдельные примеры из этого мануала, вам понадобится среда разработки Go. Инструкции по настройке вы найдете в этих мануалах:

Здесь мы используем следующую структуру каталогов:

.
├── bin

└── src
.   └── github.com
     └── gopherguides

Объявление функции init()

Каждый раз, когда вы объявляете функцию init(), Go загружает и запускает ее раньше, чем весь остальной код в данном пакете. В этом разделе мы поговорим о том, как определить функцию init(), и на примерах покажем ее влияние на работу пакета.

Для примера давайте возьмем следующий код – сначала без функции init():

package main
import "fmt"
var weekday string
func main() {
fmt.Printf("Today is %s", weekday)
}

В этой программе мы объявили глобальную переменную weekday. По умолчанию значение weekday является пустой строкой.

Читайте также: Переменные и константы в Go

Давайте запустим этот код:

go run main.go

Поскольку значение weekday пустое, при запуске программы мы получим следующий вывод:

Today is

Мы можем заполнить пустую переменную, введя функцию init(), которая присвоит переменной weekday значение текущего дня. Добавьте в main.go следующие выделенные строки:

package main
import (
"fmt"
"time"

)

var weekday string
func init() {

weekday = time.Now().Weekday().String()


}

func main() {
fmt.Printf("Today is %s", weekday)
}

В этом коде мы импортировали и использовали пакет time, чтобы получить текущий день недели (Now().Weekday().String()), а затем использовали init() для инициализации дня недели с этим значением.

Если теперь мы запустим программу, она выведет текущий день недели:

Today is Monday

Этот простой пример иллюстрирует, как работает init(), но гораздо чаще функция init() используется при импорте пакета. Это полезно, если вам нужно выполнить в пакете определенные задачи установки, прежде чем использовать этот пакет. Чтобы продемонстрировать этот функционал, давайте создадим программу, которая потребует специальной инициализации – иначе пакет не сможет работать так, как задумано.

Инициализация пакетов при импорте

Сначала мы напишем код, который выбирает из среза случайное существо и выводит его на экран. Однако мы не будем использовать init() в исходной программе – так вы сможете попробовать два варианта кода и разница между ними будет для вас очевидной.

Читайте также: Работа с массивами в Go

Внутри каталога src/github.com/gopherguides/ создайте папку creature с помощью следующей команды:

mkdir creature

В ней создайте файл creature.go:

nano creature/creature.go

В этот файл добавьте следующее содержимое:

package creature
import (
"math/rand"
)
var creatures = []string{"shark", "jellyfish", "squid", "octopus", "dolphin"}
func Random() string {
i := rand.Intn(len(creatures))
return creatures[i]
}

Этот файл определяет переменную creatures, которая содержит набор морских существ, инициализированных как значения. Он также содержит экспортированную функцию Random, которая будет возвращать случайное значение из переменной creatures.

Читайте также: Видимость пакетов в Go

Сохраните и закройте файл.

Затем мы создадим пакет cmd, который мы будем использовать для написания функции main() и вызова пакета creatures.

На том же уровне файловой системы, на котором мы создали папку creatures, создайте папку cmd с помощью следующей команды:

mkdir cmd

Внутри папки cmd создайте файл main.go:

nano cmd/main.go

Добавьте следующее содержимое в файл:

package main
import (
"fmt"
"github.com/gopherguides/creature"
)
func main() {
fmt.Println(creature.Random())
fmt.Println(creature.Random())
fmt.Println(creature.Random())
fmt.Println(creature.Random())
}

В этом коде мы импортировали пакет creature, а затем использовали в функции main() функцию creature.Random(), чтобы извлечь случайное существо и отобразить его на экране четыре раза. Сохраните и закройте файл.

Теперь у нас есть готовая программа. Но прежде чем мы сможем запустить ее, нам нужно будет создать пару конфигурационных файлов, чтобы код работал правильно. Go использует модули Go Modules для настройки зависимостей пакетов и импорта ресурсов. Эти модули размещаются в каталоге пакета и представляют собой конфигурационные файлы, которые сообщают компилятору, откуда импортировать пакеты. Подробное изучение модулей выходит за рамки данного мануала, но мы напишем пару строк конфигурации, чтобы этот пример работал локально.

В каталоге cmd создайте файл go.mod:

nano cmd/go.mod

Поместите в него следующее содержимое:

module github.com/gopherguides/cmd
replace github.com/gopherguides/creature => ../creature

Первая строка этого файла сообщает компилятору, что созданный нами пакет cmd на самом деле является github.com/gopherguides/cmd. Вторая строка сообщает, что github.com/gopherguides/creature можно найти локально на диске в каталоге ../creature.

Сохраните и закройте файл. Затем создайте файл go.mod в каталоге creature:

nano creature/go.mod

Добавьте следующую строку кода в файл:

module github.com/gopherguides/creature

Эта строка говорит компилятору, что созданный нами пакет creature на самом деле является пакетом github.com/gopherguides/creature. Без этого пакет cmd не знал бы, откуда импортировать его.

Сохраните и выйдите из файла.

Теперь у вас должна быть следующая структура каталогов и файлов:

├── cmd
│   ├── go.mod
│   └── main.go
└── creature
.   ├── go.mod
.   └── creature.go

Теперь мы можем запустить основную программу с помощью следующей команды:

go run cmd/main.go

На экране появится:

jellyfish
squid
squid
dolphin

Запустив программу, мы получили четыре значения. Если запустить программу несколько раз, вы увидите, что вывод всегда получается один и тот же – это совсем не случайный результат, как ожидалось. Это поведение связано с тем, что пакет rand создает псевдослучайные числа, которые будут последовательно генерировать одинаковые выходные данные для одного исходного состояния. Чтобы получить действительно случайное число, мы можем использовать Seed – установить изменяющийся исходник так, чтобы начальное состояние при каждом запуске программы отличалось. Обычно в Go с пакетом rand используется текущее время.

Так как мы хотим, чтобы пакет creature обрабатывал случайные функции, откройте этот файл:

nano creature/creature.go

и добавьте следующие выделенные строки:

package creature
import (
"math/rand"
"time"
)
var creatures = []string{"shark", "jellyfish", "squid", "octopus", "dolphin"}
func Random() string {
rand.Seed(time.Now().UnixNano())
i := rand.Intn(len(creatures))
return creatures[i]
}

В этом коде мы импортировали пакет time и использовали Seed() для заполнения текущего времени. Сохраните и закройте файл.

Теперь после запуска программы мы получим случайный результат:

go run cmd/main.go
jellyfish
octopus
shark
jellyfish

Если вы запустите программу еще несколько раз, вы будете всегда получать случайные результаты. Однако это еще не идеальная реализация нашего кода. Дело в том, что при каждом вызове creature.Random()пакет rand повторно запускается, а это снова вызывает функцию rand.Seed(time.Now().UnixNano()). Повторное выполнение этой функции повышает вероятность заполнения тем же исходным значением, если внутренние часы не изменились. Это приведет к возможным повторениям случайного шаблона или увеличит время обработки CPU, заставив программу ждать изменения времени.

Чтобы это исправить, мы можем использовать функцию init(). Давайте обновим файл creature.go:

nano creature/creature.go

Добавьте в файл следующие строки кода:

package creature
import (
"math/rand"
"time"
)
var creatures = []string{"shark", "jellyfish", "squid", "octopus", "dolphin"}
func init() {

rand.Seed(time.Now().UnixNano())


}

func Random() string {
i := rand.Intn(len(creatures))
return creatures[i]
}

Функция init() сообщает компилятору, что при импорте пакета creature он должен запускать функцию init() один раз, предоставляя единственное исходное число (seed) для генерации случайного числа. Благодаря этому программа не будет выполнять код больше, чем нужно. Если вы запустите программу сейчас, она продолжит выдавать случайные результаты:

go run cmd/main.go
dolphin
squid
dolphin
octopus

В этом разделе вы узнали, как функция init() может обеспечить выполнение соответствующих вычислений или инициализаций перед использованием пакета. Далее мы посмотрим, как использовать в пакете несколько операторов init().

Несколько экземпляров init()

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

В большинстве случаев функции init() будут выполняться последовательно – в том порядке, в котором они идут в файле. Давайте в качестве примера возьмем следующий код:

package main
import "fmt"
func init() {
fmt.Println("First init")
}
func init() {
fmt.Println("Second init")
}
func init() {
fmt.Println("Third init")
}
func init() {
fmt.Println("Fourth init")
}
func main() {}

Если мы запустим программу с помощью следующей команды:

go run main.go

Мы получим следующий вывод:

First init
Second init
Third init
Fourth init

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

Давайте посмотрим на пакет с более сложной структурой, в которой есть несколько файлов, каждый с собственной объявленной функцией init(). Сейчас мы создадим программу, которая разделяет переменную message и распечатывает ее.

Удалите каталоги creature и cmd и их содержимое из предыдущего раздела и замените их следующей файловой структурой:

├── cmd
│   ├── a.go
│   ├── b.go
│   └── main.go
└── message
.   └── message.go

Теперь давайте добавим содержимое в каждый файл. В a.go добавьте следующие строки:

package main
import (
"fmt"
"github.com/gopherguides/message"
)
func init() {
fmt.Println("a ->", message.Message)
}

Этот файл содержит одну функцию init(), которая выводит значение message.Message из пакета message.

Затем добавьте следующее содержимое в b.go:

package main
import (
"fmt"
"github.com/gopherguides/message"
)
func init() {
message.Message = "Hello"
fmt.Println("b ->", message.Message)
}

В файле b.go есть одна функция init(), которая устанавливает значение message.Message в Hello и распечатывает его.

Затем создайте файл main.go, который должен выглядеть следующим образом:

package main
func main() {}

Этот файл ничего не делает, но предоставляет точку входа для запуска программы.

Наконец, создайте файл message.go:

package message
var Message string

Наш пакет message объявляет экспортированную переменную Message.

Чтобы запустить программу, выполните следующую команду из каталога cmd:

go run *.go

Поскольку в папке cmd есть несколько файлов Go, которые составляют основной пакет, мы должны сообщить компилятору, что все файлы .go в папке cmd нужно скомпилировать. Команда *.go говорит компилятору, что он должен загружать все файлы в папке cmd, заканчивающиеся на .go. Если мы введем команду go run main.go, программа не сможет скомпилироваться, так как не увидит код в файлах a.go и b.go.

Это выдаст следующий вывод:

a ->
b -> Hello

Согласно спецификации языка Go по инициализации пакета, если в пакете встречается несколько файлов, они обрабатываются в алфавитном порядке. Потому при первом выводе сообщения message.Message из файла a.go значение было пустым. Значение не было инициализировано до тех пор, пока не была запущена функция init() из файла b.go.

Если бы мы переименовали файл a.go в c.go, мы получили бы другой результат:

b -> Hello
a -> Hello

Теперь компилятор сначала обрабатывает b.go, и поэтому значение message.Message уже инициализировано с Hello, когда встречается функция init() из файла c.go.

Такое поведение может создать проблему в вашем коде. При разработке программного обеспечения имена файлов часто меняются, а изменение имен файлов может изменить порядок обработки функций init(). Это может привести к нежелательным переменам в выводе вашей программы. Чтобы обеспечить стабильное поведение при инициализации, системам сборки рекомендуется представлять компилятору несколько файлов, принадлежащих одному и тому же пакету, в лексическом порядке имен файлов. Один из способов точно загрузить все функции init()по порядку, — объявить все эти функции в одном файле. Так вы можете предотвратить изменения порядка, даже если файлы будут переименованы.

Кроме того, что порядок функций init() не должен меняться, вам также следует избегать управления состоянием пакета с помощью глобальных переменных (то есть переменных, доступных из любой точки в пакете). В предыдущей программе переменная message.Message была доступна для всего пакета и поддерживала состояние программы. Благодаря этому доступу операторы init() смогли изменить переменную и дестабилизировали предсказуемость программы. Чтобы избежать этого, попробуйте работать с переменными в контролируемых пространствах, которые имеют как можно меньший доступ, но при этом позволяют программе работать.

Теперь вы знаете, что в одном пакете может быть несколько объявлений init(). Однако это может привести к нежелательным эффектам и затруднить чтение и прогнозирование вашей программы. Если вы будете избегать использования нескольких операторов init() или хранить их в одном файле, вы можете быть уверены, что поведение вашей программы не изменится при перемещении файлов или изменении имен.

Далее мы рассмотрим, как init() используется в импорте с побочными эффектами.

Функция init() и побочные эффекты

В Go иногда пакет нужно импортировать не ради его содержимого, а ради побочных эффектов, возникающих при импорте пакета. Это, как правило, означает, что в импортированном коде есть оператор init(), который выполняется перед любым другим кодом, что позволяет разработчику манипулировать состоянием, в котором запускается программа. Эта техника называется импортом побочного эффекта.

Распространенным вариантом использования импорта побочных эффектов является регистрация функциональности в вашем коде, что позволяет пакету знать, какую часть кода должна использовать программа. Например, в пакете image функция image.Decode должна знать, какой формат изображения она пытается декодировать (jpg, png, gif и т. д.), прежде чем она сможет работать. Для этого вы можете сначала импортировать конкретную программу, которая имеет побочный эффект оператора init().

Допустим, вы пытаетесь использовать image.Decode для обработки файла .png со следующим фрагментом кода:

. . .
func decode(reader io.Reader) image.Rectangle {
m, _, err := image.Decode(reader)
if err != nil {
log.Fatal(err)
}
return m.Bounds()
}
. . .

Программа с этим кодом все еще будет компилироваться, но каждый раз, когда мы попытаемся декодировать изображение в формате png, мы будем получать ошибку.

Чтобы это исправить, нужно сначала зарегистрировать формат изображения для image.Decode. К счастью, пакет image/png содержит следующий оператор init():

func init() {
image.RegisterFormat("png", pngHeader, Decode, DecodeConfig)
}

И если мы импортируем image/png в фрагмент кода для декодирования, то функция image.RegisterFormat() в image/png будет запускаться перед всем кодом:

. . .
import _ "image/png"
. . .
func decode(reader io.Reader) image.Rectangle {
m, _, err := image.Decode(reader)
if err != nil {
log.Fatal(err)
}
return m.Bounds()
}

Это установит состояние и зарегистрирует png-версию image.Decode(). Эта регистрация произойдет как побочный эффект импорта image/png.

Возможно, вы заметили пустой идентификатор (_) перед «image/png». Он нужен потому, что Go не позволяет импортировать пакеты, которые не используются в программе. При включении пустого идентификатора значение самого импорта отбрасывается, так что проявляется только побочный эффект. Это означает, что мы можем импортировать пакет image/png для побочного эффекта, при этом никогда не вызывая пакет в этом коде.

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

Заключение

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

Tags: ,