Вступление
В Go предопределенная функцияinit()
запускает фрагмент кода перед любой другой частью вашего пакета. Этот код будет выполняться, как толькоpackage is imported, и может использоваться, когда вам нужно, чтобы ваше приложение инициализировалось в определенном состоянии, например, когда у вас есть определенная конфигурация или набор ресурсов, с которыми ваше приложение должно запускаться. Он также используется, когдаimporting a side effect, метод, используемый для установки состояния программы путем импорта определенного пакета. Это часто используется дляregister
одного пакета с другим, чтобы убедиться, что программа учитывает правильный код для задачи.
Хотяinit()
- полезный инструмент, иногда он может затруднить чтение кода, поскольку трудно найти экземплярinit()
будет сильно влиять на порядок, в котором выполняется код. Из-за этого разработчикам, которые плохо знакомы с Go, важно понимать аспекты этой функции, чтобы они могли использоватьinit()
в разборчивой форме при написании кода.
В этом руководстве вы узнаете, какinit()
используется для установки и инициализации конкретных переменных пакета, однократных вычислений и регистрации пакета для использования с другим пакетом.
Предпосылки
Для некоторых примеров из этой статьи вам понадобится:
-
Рабочее пространство Go настраивается следующим образом:How To Install Go and Set Up a Local Programming Environment. В этом руководстве будет использоваться следующая файловая структура:
.
├── 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)
}
В этой программе мы объявили глобальныйvariable с именем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()
является его использование при импорте пакета. Это может быть полезно, когда вам нужно выполнить определенные задачи установки в пакете, прежде чем вы захотите использовать пакет. Чтобы продемонстрировать это, давайте создадим программу, которая потребует специальной инициализации, чтобы пакет работал так, как задумано.
Инициализация пакетов при импорте
Сначала мы напишем код, который выбирает случайное существо изslice и распечатывает его. Однако мы не будем использовать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
, которая имеет набор морских существ, инициализированных как значения. Он также имеет функциюexportedRandom
, которая возвращает случайное значение из переменной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
Когда мы запустили эту программу, мы получили четыре значения и распечатали их. Если мы запустим программу несколько раз, мы заметим, чтоalways получим тот же результат, а не случайный результат, как ожидалось. Это связано с тем, что пакетrand
создает псевдослучайные числа, которые будут последовательно генерировать один и тот же вывод для одного начального состояния. Чтобы получить более случайное число, мы можемseed пакет или установить изменяющийся источник так, чтобы начальное состояние было другим при каждом запуске программы. В 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()+`s can make it difficult to know which one has priority over the others. In this section, we will show how to maintain control over multiple `+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
Поскольку у нас есть несколько файлов Go в папкеcmd
, составляющих пакетmain
, нам нужно сообщить компилятору, что все файлы.go
в папкеcmd
должны быть составлен. Использование*.go
указывает компилятору загрузить все файлы в папкеcmd
, заканчивающиеся на.go
. Если мы введем командуgo run main.go
, программа не сможет скомпилироваться, так как не увидит код в файлахa.go
иb.go
.
Это даст следующий вывод:
Outputa ->
b -> Hello
Согласно спецификации языка Go дляPackage Initialization, когда в пакете встречается несколько файлов, они обрабатываются в алфавитном порядке. Из-за этого, когда мы впервые распечатали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()
не изменился, вам также следует попытаться избежать управления состоянием в вашем пакете, используяglobal variables, то есть переменные, которые доступны из любого места в пакете. В предыдущей программе переменнаяmessage.Message
была доступна для всего пакета и поддерживала состояние программы. Благодаря этому доступу операторыinit()
смогли изменить переменную и нарушить предсказуемость вашей программы. Чтобы избежать этого, попробуйте работать с переменными в контролируемых пространствах, которые имеют как можно меньший доступ, но при этом позволяют программе работать.
Мы видели, что вы можете иметь несколько объявленийinit()
в одном пакете. Однако это может привести к нежелательным эффектам и затруднить чтение и прогнозирование вашей программы. Избегание нескольких операторовinit()
или сохранение их всех в одном файле гарантирует, что поведение вашей программы не изменится при перемещении файлов или изменении имен.
Далее мы рассмотрим, какinit()
используется для импорта с побочными эффектами.
Использованиеinit()
для побочных эффектов
В Go иногда желательно импортировать пакет не для его содержимого, а для побочных эффектов, возникающих при импорте пакета. Это часто означает, что в импортированном коде есть операторinit()
, который выполняется перед любым другим кодом, что позволяет разработчику управлять состоянием, в котором запускается его программа. Этот метод называетсяimporting for a side effect.
Типичный вариант использования импорта для побочных эффектов - это функциональностьregister в вашем коде, которая позволяет пакету знать, какую часть кода ваша программа должна использовать. Вimage
package, например, функция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()
:
image/png/reader.go
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
.
Возможно, вы заметилиblank identifier (_
) перед"image/png"
. Это необходимо, потому что Go не позволяет импортировать пакеты, которые не используются в программе. При включении пустого идентификатора значение самого импорта отбрасывается, так что проявляется только побочный эффект импорта. Это означает, что, хотя мы никогда не вызываем пакетimage/png
в нашем коде, мы все равно можем импортировать его для побочного эффекта.
Важно знать, когда вам нужно импортировать пакет для его побочного эффекта. Без надлежащей регистрации вполне вероятно, что ваша программа скомпилируется, но не будет работать должным образом при запуске. Пакеты в стандартной библиотеке заявят о необходимости такого типа импорта в своей документации. Если вы пишете пакет, который требует импорта для побочного эффекта, вы также должны убедиться, что используемый вами операторinit()
задокументирован, чтобы пользователи, импортирующие ваш пакет, могли его правильно использовать.
Заключение
В этом руководстве мы узнали, что функцияinit()
загружается до загрузки остальной части кода в вашем пакете, и что она может выполнять определенные задачи для пакета, такие как инициализация желаемого состояния. Мы также узнали, что порядок, в котором компилятор выполняет несколько операторовinit()
, зависит от порядка, в котором компилятор загружает исходные файлы. Если вы хотите узнать больше оinit()
, посмотрите официальныйGolang documentation или прочтитеthe discussion in the Go community about the function.
Вы можете узнать больше о функциях в нашей статьеHow To Define and Call Functions in Go или изучитьthe entire How To Code in Go series.