[Перевод] Проблемы функции Golang init
Прежде чем говорить о функции init
в Golang, необходимо понять, что такое пакет в Golang. Программа go организована в пакеты. Пакет собирает несколько исходных файлов в одном каталоге. Он похож на ящик, в котором находятся некоторые инструменты или небольшая машина. Он является отправной точкой для инициализации всего пакета. По-видимому, это соответствует назначению функции init
.
Предположим, что у вас есть код без функции init
, например, такой:
// foo.go
package foo
var A int
func bar(){}
// main.go
import "foo"
func main(){
fmt.Println(foo.A)
}
В данном случае импортируется пакет foo
, используется переменная A
без других частей. Все в явном виде. У вас может возникнуть вопрос, если я использую только переменную A
, могу ли я просто импортировать A
без других переменных и функций в этом пакете? Ответ в Golang таков: Нет. Так делать нельзя, необходимо импортировать весь пакет, поскольку он является единицей программирования, которую нельзя разделить. Этот код работал эффективно, пока в игру не вступила функция init
.
Пакет, имеющий несколько функций init
, может выглядеть следующим образом:
// foo.go
package foo
var A int
func init() { A = 1 }
func bar(){}
// bar.go
package foo
var B int
func init() { B = 2 }
func bar() {}
Как пользователь пакета, ваш код не меняется, в нем по-прежнему используется только переменная A
:
// main.go
import "foo"
func main(){
fmt.Println(foo.A)
}
Пакет по-прежнему работает. Но функция init
выполняется неявно, как вы и не знали. В Golang вы должны принять затраты на init
, если вы являетесь пользователем пакета. Это просто, но затраты будут не только на неявно выполняемую функцию, но и на весь пакет.
Когда вы пытаетесь написать несколько модульных тестов, вы не можете запретить функцию init
. Особенно если вы инициализируете какие-то внешние ресурсы (например, базы данных, файлы, журналы или другие), ваши юнит-тесты сломаются, они должны загрузить ресурсы, даже если вы хотите написать всего лишь маленький юнит-тест.
Если вы хотите, чтобы ваш код работал эффективно, вам следует отказаться от использования функции init
. Поскольку функция init
является глобальной и вы не можете контролировать время ее выполнения. Худшим недостатком функции init
является то, что она скрывает обработку пакета и трудно определить порядок ее выполнения, даже если вы можете написать тестовый код, чтобы узнать порядок.
Функция init
не была вызвана пользователем пакета, она была вызвана до main
. Если в функции init
произошла ошибка, что можно сделать? Как использовать единственный механизм обработки ошибок (if err != nil
)? Может быть, в ней можно использовать panic
, но как использовать recover
для обработки этой паники? Как объяснить пользователям пакета, что они должны следить за тем, чтобы пакет не паниковал? Как объяснить, что пакет может запаниковать при запуске, даже если пользователь пакета просто вставил в свой код строчку import
?
func init(){
f, err := file.Open(path) // how to handle the err?
}
Приведенный выше код откроет путь к файлу для записи или чтения. Когда вы выполняете свой код по правильному пути, все в порядке. Но если рабочий каталог изменился или вы хотите использовать относительные пути, как быть с ошибками? Поэтому не следует помещать в функцию init
код, который может содержать ошибки, и не инициализировать в ней ресурсы другого пакета.
package foo
import "bar"
func init(){
bar.Initlization()
}
Если вы сделаете это, то ваш пакет не будет работать независимо. Для очистки кода не следует помещать в функцию init
код других пакетов. Если другие пакеты нуждаются в инициализации, то они должны дать запись инициализации, либо инициализироваться самостоятельно.
Поразмыслив над проблемами, с которыми я столкнулся в функции init
, и прочитав несколько дискуссий об удалении функции init
в Go, я пришел к выводу, что лучшей практикой использования функции init
является: Не использовать.
Существует несколько способов избежать использования функции init
.
Если у вас есть глобальная переменная на уровне пакета, инициализируйте ее при объявлении.
var (
a = 0
p *foo = nil
)
Если требуется инициализация ресурсов другого пакета или инициализация каких-то дополнительных ресурсов, используйте экспортируемую функцию Init*
.
package foo
var (
f *os.File
)
func InitFoo(path string) error {
f, err := file.Open(path)
_ = f
return err
}
Если вы хотите, чтобы функция Init*
выполнялась только один раз, используйте функцию sync.Once.Do
:
package foo
var (
once sync.Once
f *os.File
)
func InitFoo(path string) error {
var err error
once.Do(func(){
f, err = os.Open(path)
})
return err
}
Если в вашем пакете есть несколько частей ресурса, и вы хотите, чтобы они инициализировались по отдельности, используйте старое и скучное объектно-ориентированное программирование (функции-конструкторы).
// foo.go
package foo
type Foo struct {}
func NewFoo() (*Foo, error) {
return &Foo{}, nil
}
// bar.go
package foo
type Bar struct {}
func NewBar() (*Bar, error) {
return &Bar{}, nil
}
Если вы все же хотите использовать функцию init
в своем коде, то единственный совет — не размещайте в функции init
вызовы других пакетов, даже просто переменных.
Удаление функции init
сделает ваш код более прозрачным и менее связанным. Все будет работать явно, затраты будут видны, а ваш код будет простым и легко читаемым.