Прежде чем говорить о функции 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 сделает ваш код более прозрачным и менее связанным. Все будет работать явно, затраты будут видны, а ваш код будет простым и легко читаемым.

Комментарии (4)


  1. icecube092
    04.11.2023 16:34
    +3

    Использовать init уже давно не рекомендуется самим гуглом.


  1. Tuxman
    04.11.2023 16:34
    +1

    Я был слегка удивлён, когда просто import мне добавил несколько опций в сommand-line options, и мой --help стал выбавать ещё какую-то хрень, о которой я не ожидал.

    И, главное, я просто хочу подключить внешнюю библиотеку, и не затащить ещё себе опций в комманд-лайн дополнительных.


  1. jlllk
    04.11.2023 16:34

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

    Ммм.. что? Пакет является отправной точкой для инициализации пакета?

    Пакет по-прежнему работает. Но функция init выполняется неявно, как вы и не знали. В Golang вы должны принять затраты на init, если вы являетесь пользователем пакета. Это просто, но затраты будут не только на неявно выполняемую функцию, но и на весь пакет.

    Как мы и не знали, мы должны принять затраты...

    Текст, вроде, перевели, но хотелось бы на русский язык.


  1. olivera507224
    04.11.2023 16:34

    Когда я только начинал знакомиться с Го, первое, о чемя подумал после прочтения о функции init, было "Вау! Тут же можно без каких-то костылей просто и беззастенчиво генерировать синглтоны!" Должен признаться, я с таким подходом знатно наговнокодил в своём первом коммерческом проекте на Го...