Функция init в Go

Введение

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

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

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

Предварительные требования

Для некоторых из приведенных в этой статье примеров вам потребуется следующее:

  • Рабочее пространство Go, настроенное согласно руководству по установке Go и настройке локальной среды для разработки. В настоящем обучающем модуле будет использоваться следующая структура файлов:
. ├── bin │ └── src     └── github.com         └── gopherguides 

Декларирование init()

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

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

main.go

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

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

Запустим этот код:

  • go run main.go

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

OutputToday is 

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

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() для инициализации weekday с этим значением.

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

OutputToday is Monday 

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

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

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

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

  • mkdir creature

Создайте в папке creature файл с именем creature.go:

  • nano creature/creature.go

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

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.

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

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

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

  • mkdir cmd

Создайте в папке cmd файл с именем main.go:

  • nano cmd/main.go

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

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(), чтобы получить случайное существо и вывести его четыре раза.

Сохранение и выход из main.go.

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

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

  • nano cmd/go.mod

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

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

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

creature/go.mod

 module github.com/gopherguides/creature 

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

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

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

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

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

  • go run cmd/main.go

Это даст нам следующее:

Outputjellyfish squid squid dolphin 

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

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

  • nano creature/creature.go

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

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
Outputjellyfish octopus shark jellyfish 

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

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

  • nano creature/creature.go

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

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() один раз и задать начальное случайное число для генерирования случайных чисел. Так нам не придется лишний раз выполнять код. Если мы запустим программу, мы по-прежнему будем получать случайные результаты:

  • go run cmd/main.go
Outputdolphin squid dolphin octopus 

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

Использование нескольких экземпляров init()

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

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

main.go

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

Мы получим следующий результат:

OutputFirst 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 добавьте следующие строчки:

cmd/a.go

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

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

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

cmd/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, который будет выглядеть следующим образом:

cmd/main.go

package main  func main() {} 

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

В заключение создайте файл message.go как показано здесь:

message/message.go

package message  var Message string 

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

Для запуска программы выполните следующую команду в каталоге cmd:

  • go run *.go

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

Результат будет выглядеть следующим образом:

Outputa -> b -> Hello 

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

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

Outputb -> Hello a -> Hello 

Теперь компилятор вначале получает b.go и значение message.Message уже инициализировано как Hello при появлении функции init() в c.go.

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

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

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

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

Использование init() для побочных эффектов

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

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

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

Sample Decoding Snippet

. . . 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():

image/png/reader.go

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

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

Sample Decoding Snippet

. . . 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 об этой функции.

Дополнительную информацию о функциях можно найти в статье Определение и вызов функций и в других статьях из серии статей по программированию на Go.