В Go заданнная функция init()
выделяет элемент кода, который запускатся до любой другой части вашего пакета. Этот код запускается сразу же после импорта пакета, и его можно использовать при необходимости инициализации приложения в определенном состоянии, например, если для запуска приложения требуется определенная конфигурация или набор ресурсов. Также используется при импорте побочных эффектов, то есть при применении методики установки состояния программы посредством импорта определенного пакета. Часто используется для регистрации
одного пакета в другом, чтобы программа рассматривала правильный код для этой задачи.
Хотя init()
представляет собой полезный инструмент, иногда он делает код менее удобочитаемым, поскольку трудный в поиске экземпляр init()
серьезно влияет на порядок выполнения кода. В связи с этим, начинающим разработчикам Go важно понимать все аспекты этой функции, чтобы они использовали init()
в читаемой форме при написании кода.
В этом обучающем руководстве вы узнаете, как init()
используется для настройки и инициализации переменных определенных пакетов, разовых вычислений и регистрации пакетов для использования внутри других пакетов.
Для некоторых из приведенных в этой статье примеров вам потребуется следующее:
.
├── 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 run main.go
Поскольку значение weekday
пустое, при запуске программы мы увидим следующее:
OutputToday 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()
для инициализации weekday
с этим значением.
Теперь при запуске программы она выводит текущий день недели:
OutputToday is Monday
Хотя это показывает принцип работы функции init()
, гораздо чаще init()
используется при импорте пакета. Это может быть полезно, если вам требуется выполнить в пакете определенные задачи по настройке, прежде чем использовать этот пакет. Чтобы продемонстрировать это, создадим программу, которая потребует определенной инициализации для обеспечения требуемой работы пакета.
Вначале мы напишем код, который выбирает случайное существо из среза и выводит его. Однако мы не будем использовать init()
в начальной программе. Это лучше покажет стоящую перед нами проблему и возможность ее решения с помощью функции init()
.
Создайте в каталоге src/github.com/gopherguides/
папку с именем creature
с помощью следующей команды:
- mkdir creature
Создайте в папке 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
.
Сохраните и закройте этот файл.
Теперь создайте пакет cmd
, который мы используем для записи функции main()
и вызова пакета creature
.
На том же уровне файла, где мы создали папку creature
, создайте папку 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()
, чтобы получить случайное существо и вывести его четыре раза.
Сохранение и выход из main.go
.
Теперь у нас написана вся программа. Однако перед запуском этой программы нам также потребуется создать несколько файлов конфигурации, чтобы обеспечить правильную работу нашего кода. 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
Мы завершили настройку и теперь можем запустить программу main
с помощью следующей команды:
- go run cmd/main.go
Это даст нам следующее:
Outputjellyfish
squid
squid
dolphin
При запуске этой программы мы получили четыре значения и вывели их. Если мы запустим программу несколько раз, результаты всегда будут одинаковыми, хотя ожидается случайный результат. Это связано с тем, что пакет rand
создает псевдослучайные числа, постоянно дающие один и тот же результат для одного и того же начального состояния. Чтобы получить действительно случайное число, мы можем задать начальное случайное число для пакета или задать изменяющийся источник, чтобы при каждом запуске программы состояние было разным. В Go обычно используется текущее время в качестве начального случайного числа
для пакета rand.
Поскольку нам нужно, чтобы пакет creature
работал с функцией случайных чисел, откроем этот файл:
- nano creature/creature.go
Добавьте в файл 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
Добавьте следующие строчки кода:
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()
выполняются в том порядке, в каком они содержатся в программе. Рассмотрим в качестве примера следующий код:
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
добавьте следующие строчки:
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, составляющих пакет 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
со следующим фрагментом кода:
. . .
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 об этой функции.
Дополнительную информацию о функциях можно найти в статье Определение и вызов функций и в других статьях из серии статей по программированию на Go.
Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.
This textbox defaults to using Markdown to format your answer.
You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!