Entendendo o init em Go

Introdução

Em Go, a função pré-definida init() faz com que uma parte do código execute antes de qualquer outra parte do seu pacote. Esse código vai executar assim que o pacote for importado e poderá ser usado quando você precisar inicializar seu aplicativo em um estado específico, como quando você tem uma configuração específica ou um conjunto de recursos com os quais seu aplicativo precisa iniciar. Ele também é usado na importação de um efeito colateral – técnica usada para definir o estado de um programa por meio da importação de um pacote específico. Isso é frequentemente usado para register [registrar] um pacote junto ao outro, para garantir que o programa está considerando o código correto para a tarefa.

Embora o init() seja uma ferramenta útil, ela pode, por vezes, deixar o código difícil de ler, uma vez que uma instância difícil de encontrar de init() irá afetar imensamente a ordem na qual o código é executado. Por isso, é importante para os desenvolvedores que são novos em Go entender as nuances dessa função, para que eles possam garantir que usem o init() de uma maneira legível ao escrever seus códigos.

Neste tutorial, você aprenderá como o init() é usado para a configuração e inicialização de variáveis de pacotes específicos, processos computacionais únicos e o registro de um pacote para uso com outro pacote.

Pré-requisitos

Para alguns dos exemplos neste artigo, você vai precisar do seguinte:

  • Um espaço de trabalho em Go, configurado de acordo com o artigo Como instalar o Go e configurar um ambiente de programação local. Este tutorial usará a seguinte estrutura de arquivo:
. ├── bin │ └── src     └── github.com         └── gopherguides 

Declarando o init()

Sempre que você declarar uma função init(), o Go irá carregá-la e executá-la antes de qualquer outra coisa naquele pacote. Para demonstrar isso, esta seção traz um passo a passo de como definir uma função init() e mostrar os efeitos na maneira como o pacote executa.

Vamos primeiro usar seguinte exemplo de código sem a função init():

main.go

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

Neste programa, declaramos uma variável global chamada weekday. Por padrão, o valor de weekday é uma string vazia.

Vamos executar este código:

  • go run main.go

Como o valor de weekday está em branco, quando executarmos o programa, vamos obter o seguinte resultado:

OutputToday is 

Podemos preencher a variável em branco, introduzindo uma função init() que inicializa o valor de weekday no dia atual. Adicione as linhas destacadas a seguir ao 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) } 

Neste código, importamos e usamos o pacote time para obter o dia atual da semana (Now(). Weekday(). String() ) e, em seguida, usamos o init() para inicializar weekday com aquele valor.

Agora, quando executarmos o programa, ele imprimirá o valor do dia da semana atual:

OutputToday is Monday 

Embora isso mostre como o init() funciona, um caso de uso muito mais típico para o init() é usá-lo na importação de um pacote. Isso pode ser útil quando você precisar fazer tarefas específicas de configuração em um pacote, antes do momento em que vai querer que o pacote seja usado. Para demonstrar isso, vamos criar um programa que precisará de uma inicialização específica para que o pacote funcione como pretendido.

Inicializando pacotes na importação

Primeiro, vamos escrever um código que seleciona uma criatura aleatória de uma fatia e o imprime. No entanto, não vamos usar o init() em nosso programa inicial. Isso mostrará melhor o problema que temos e como o init() resolverá nosso problema.

Em seu diretório src/github.com/gopherguides/, crie uma pasta chamada creature com o seguinte comando:

  • mkdir creature

Dentro da pasta creature, crie um arquivo chamado creature.go:

  • nano creature/creature.go

Neste arquivo, adicione o seguinte conteúdo:

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] } 

Esse arquivo define uma variável chamada creatures que tem um conjunto de criaturas marinhas inicializadas como valores. Ele também tem uma função exportada Random que retornará um valor aleatório da variável creatures.

Salve e saia desse arquivo.

Em seguida, vamos criar um pacote cmd que usaremos para escrever nossa função main() e chamar o pacote creature.

No mesmo nível de arquivo no qual criamos a pasta creature, crie uma pasta cmd com o seguinte comando:

  • mkdir cmd

Na pasta cmd, crie um arquivo chamado main.go:

  • nano cmd/main.go

Adicione o conteúdo a seguir ao arquivo:

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()) } 

Aqui, importamos o pacote creature e, em seguida, na função main() usamos a função creature.Random() para recuperar uma criatura aleatória e imprimi-la quatro vezes.

Salve e saia do main.go.

Agora, temos nosso programa escrito por completo. No entanto, antes de executarmos esse programa, também precisaremos criar alguns dos arquivos de configuração para que o nosso código funcione corretamente. A linguagem Go usa Módulos Go para configurar as dependências de pacotes para a importação de recursos. Esses módulos são arquivos de configuração colocados no seu diretório de pacotes. Eles dizem ao compilador de onde importar os pacotes. Embora o aprendizado sobre os módulos esteja fora do escopo deste artigo, podemos escrever apenas algumas linhas de configuração para fazer com que este exemplo funcione localmente.

No diretório cmd, crie um arquivo chamado go.mod:

  • nano cmd/go.mod

Assim que o arquivo estiver aberto, coloque o seguinte conteúdo:

cmd/go.mod

module github.com/gopherguides/cmd  replace github.com/gopherguides/creature => ../creature 

A primeira linha desse arquivo diz ao compilador que o pacote cmd que criamos é, na verdade, o github.com/gopherguides/cmd. A segunda linha diz ao compilador que o github.com/gopherguides/creature pode ser encontrado localmente em disco no diretório ../creature.

Salve e feche o arquivo. Em seguida, crie um arquivo go.mod no diretório creature:

  • nano creature/go.mod

Adicione a linha de código a seguir ao arquivo:

creature/go.mod

 module github.com/gopherguides/creature 

Isso diz ao compilador que o pacote creature que criamos é, na verdade, o pacote github.com/gopherguides/creature. Sem isso, o pacote cmd não saberia de onde importar esse pacote.

Salve e saia do arquivo.

Agora, você deve ter a seguinte estrutura de diretório e layout de arquivo:

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

Agora que temos toda a configuração completa, podemos executar o programa main com o seguinte comando:

  • go run cmd/main.go

Isso dará:

Outputjellyfish squid squid dolphin 

Quando executamos o programa, recebemos quatro valores e os imprimimos. Se executarmos o programa várias vezes, notaremo*s que *o resultado obtido será sempre o mesmo e não um resultado aleatório – como o esperado. Isso acontece porque o pacote rand cria números pseudoaleatórios que gerarão consistentemente o mesmo resultado para um único estado inicial. Para alcançar um número mais aleatório, podemos propagar o pacote, ou definir uma fonte de mudança para que o estado inicial seja diferente sempre que executarmos o programa. Na linguagem Go, é comum usar o tempo atual para propagar o pacote rand.

Como queremos que o pacote creature lide com a funcionalidade aleatória, abra este arquivo:

  • nano creature/creature.go

Adicione as seguintes linhas destacadas ao arquivo 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] } 

Nesse código, importamos o pacote time e usamos Seed() para propagar o tempo atual. Salve e saia do arquivo.

Agora, quando executarmos o programa, vamos obter um resultado aleatório:

  • go run cmd/main.go
Outputjellyfish octopus shark jellyfish 

Se você continuar executando o programa repetidas vezes, você continuará a obter resultados aleatórios. No entanto, essa ainda não é uma implementação ideal do nosso código, pois toda vez que creature.Random() é chamada, ela também propaga novamente o pacote rand, chamando rand.Seed(time.Now(). UnixNano()) novamente. Executar novamente a propagação aumentará as chances de se propagar o mesmo valor inicial, caso o relógio interno não tiver sido alterado. Isso resultará em possíveis repetições do padrão aleatório, ou aumentará o tempo de processamento da CPU, fazendo com que o seu programa aguarde pela alteração no relógio.

Para corrigir isso, podemos usar uma função init(). Vamos atualizar o arquivo creature.go:

  • nano creature/creature.go

Adicione as seguintes linhas de código:

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] } 

A adição da função init() diz ao compilador que ao importar o pacote creature, ele deve executar a função init() uma vez, fornecendo uma única propagação para a geração aleatória de números. Isso garante que não executemos o código mais vezes do que o necessário. Agora, caso executarmos o programa, continuaremos a obter resultados aleatórios:

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

Nesta seção, vimos de que modo o uso do init() pode assegurar que os cálculos ou inicializações apropriados sejam realizados antes do pacote ser usado. Em seguida, vamos ver como usar várias instruções init() em um pacote.

Múltiplas instâncias de init()

Ao contrário do que ocorre com a função main() – que pode ser declarada apenas uma vez, a função init() pode ser declarada várias vezes ao longo de um pacote. No entanto, o uso de várias init()s pode dificultar saber qual delas tem prioridade sobre as outras. Nesta seção, mostraremos como manter o controle sobre várias instruções init().

Na maioria dos casos, as funções init() serão executadas na ordem em que você as encontrar. Vamos usar o código a seguir como exemplo:

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() {} 

Se executarmos o programa com o seguinte comando:

  • go run main.go

Receberemos o seguinte resultado:

OutputFirst init Second init Third init Fourth init 

Note que cada init() é executado na ordem em que o compilador o encontra. No entanto, nem sempre pode ser fácil determinar a ordem na qual a função init() será chamada.

Vamos ver uma estrutura de pacotes mais complicada, na qual temos vários arquivos, cada qual com sua própria função init() declarada em si. Para ilustrar isso, criaremos um programa que compartilha uma variável chamada message e a imprime.

Exclua os diretórios creature e cmd e seu conteúdo da seção anterior e os substitua pela seguinte estrutura de diretórios e arquivos:

├── cmd │   ├── a.go │   ├── b.go │   └── main.go └── message     └── message.go 

Agora, vamos adicionar o conteúdo de cada arquivo. Em a.go, adicione as seguintes linhas:

cmd/a.go

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

Esse arquivo contém uma única função init() que imprime o valor de message.Message do pacote message.

Em seguida, adicione o seguinte conteúdo ao b.go:

cmd/b.go

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

Em b.go, temos uma única função init() que define o valor de message.Message para Hello e o imprime.

Em seguida, crie um arquivo main.go, de maneira a se parecer com o seguinte:

cmd/main.go

package main  func main() {} 

Esse arquivo não faz nada, mas proporciona um ponto de entrada para o programa executar.

Por fim, crie seu arquivo message.go como o seguinte:

message/message.go

package message  var Message string 

Nosso pacote message declara a variável exportada Message.

Para executar o programa, execute o seguinte comando a partir do diretório cmd:

  • go run *.go

Como temos vários arquivos Go na pasta cmd que compõem o pacote main, precisamos dizer ao compilador que todos os arquivos .go na pasta cmd devem ser compilados. Ao usar *.go, dizemos ao compilador para que carregue todos os arquivos na pasta cmd que terminem em .go. Se emitíssemos o comando go run main.go, o programa apresentaria falha ao compilar, uma vez que não veria o código nos arquivos a.go e b.go.

Isso dará o seguinte resultado:

Outputa -> b -> Hello 

De acordo com as especificações da linguagem Go para a Inicialização de pacotes, quando vários arquivos são encontrados em um pacote, eles são processados em ordem alfabética. Por esse motivo, a primeira vez que imprimimos message.Message a partir do a.go, o valor estava em branco. O valor não foi inicializado até que a função init() do b.go tivesse sido executada.

Se alterássemos o nome do arquivo a.go para c.go, teríamos um resultado diferente:

Outputb -> Hello a -> Hello 

Agora, o compilador encontra primeiro o b.go. Dessa forma, o valor de message.Message já estará inicializado com Hello quando a função init() em c.go for encontrada.

Esse comportamento poderia criar um possível problema em seu código. É comum no desenvolvimento de software alterar os nomes dos arquivos e, pela forma como o init() é processado, alterar nomes de arquivos pode alterar a ordem em que o init() é processado. Isso pode ter o efeito indesejável de alterar o resultado do seu programa. Para garantir que o comportamento de inicialização possa ser reproduzido, os sistemas de compilação são encorajados a apresentar para o compilador os vários arquivos que pertençam ao mesmo pacote, pela ordem alfabética dos nomes dos arquivos. Uma maneira de garantir que todas as funções init() sejam carregadas na ordem é declará-las todas em um único arquivo. Isso impedirá que a ordem seja alterada, mesmo se os arquivos forem alterados.

Além de garantir que a ordem das suas funções init() não seja alterada, você também deve tentar evitar gerenciar o estado em seu pacote usando variáveis globais, ou seja, variáveis que podem ser acessadas de qualquer lugar dentro do pacote. No programa anterior, a variável message.Message estava disponível a todo o pacote e mantinha o estado do programa. Por conta disso, as instruções init() conseguiram alterar a variável e desestabilizar a previsibilidade do seu programa. Para evitar isso, tente trabalhar com variáveis em espaços controlados que tenham o mínimo de acesso possível, ao mesmo tempo em que ainda permitam que seu programa funcione.

Vimos que é possível ter várias declarações init() em um único pacote. No entanto, fazer isso pode criar efeitos indesejáveis e tornar seu programa mais difícil de ler ou prever. Evitar várias instruções init() ou mantê-las todas em um único arquivo irá garantir que o comportamento do seu programa não seja alterado quando os arquivos forem movidos ou seus nomes forem alterados.

Na sequência, vamos examinar como o init() é usado para importar efeitos colaterais.

Usando o init() para obter Efeitos colaterais

Na linguagem Go, às vezes é desejável importar um pacote não pelo seu conteúdo, mas pelos efeitos colaterais que ocorrem após a importação do pacote. Isso significa, frequentemente, que há uma instrução init() no código importado que executa antes de qualquer outro código, permitindo que o desenvolvedor manipule o estado no qual seu programa está iniciando. Essa técnica é chamada de importação para obter um efeito colateral.

Um caso de uso comum da importação para obtenção de efeitos colaterais é registrar a funcionalidade em seu código, o que permite que um pacote saiba qual parte do código o seu programa precisa usar. No pacote image, por exemplo, a função image.Decode precisa saber qual formato de imagem ela está tentando decodificar (jpg, png, gif etc) antes de poder ser executada. Você pode alcançar isso primeiro importando um programa específico que tenha um efeito colateral da instrução init().

Vamos supor que esteja tentando usar o image.Decode em um arquivo .png com o seguinte trecho de código:

Sample Decoding Snippet

. . . func decode(reader io.Reader) image.Rectangle {     m, _, err := image.Decode(reader)     if err != nil {         log.Fatal(err)     }     return m.Bounds() } . . . 

Um programa com esse código ainda será compilado, mas sempre que tentarmos decodificar uma imagem png, vamos obter um erro.

Para corrigir isso, precisamos primeiro registrar um formato de imagem para o image.Decode. Felizmente, o pacote image/png contém a seguinte instrução init():

image/png/reader.go

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

Portanto, caso importemos image/png para o nosso trecho de decodificação, então a função image.RegisterFormat() em image/png será executada antes de qualquer parte do nosso código:

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() } 

Isso definirá o estado e registrará que precisamos da versão png do image.Decode(). Esse registro acontecerá como um efeito colateral da importação de image/png.

Você pode ter notado o identificador em branco (_) antes de "image/png". Isso é necessário porque a linguagem Go não permite que você importe pacotes que não sejam usados ao longo do programa. Ao incluir o identificador em branco, o valor da importação em si é descartado, para que apenas o efeito colateral da importação sobreviva. Isso significa que, embora nunca chamemos o pacote image/png em nosso código, ainda poderemos importá-lo para obter o seu respectivo efeito colateral.

É importante saber quando você precisa importar um pacote para obter seu efeito colateral. Sem o registro adequado, é provável que seu programa seja compilado, mas que não funcione corretamente quando for executado. Os pacotes na biblioteca padrão declararão a necessidade desse tipo de importação em sua documentação. Se escrever um pacote que exija a importação para obtenção de efeito colateral, você também deve garantir que a instrução init() que está usando seja documentada, para que os usuários que importem seu pacote possam usá-lo corretamente.

Conclusão

Neste tutorial, aprendemos que a função init() é carregada antes que o resto do código em seu pacote seja carregado e que ela pode realizar tarefas específicas em um pacote, como inicializar um estado desejado. Também aprendemos que a ordem na qual o compilador executa várias instruções init() depende da ordem na qual o compilador carregue os arquivos fonte. Se quiser aprender mais sobre o init(), verifique a documentação oficial da Golang ou leia a discussão na comunidade Go sobre a função.

Você pode ler mais sobre funções em nosso artigo Como definir e chamar funções em Go, ou explorar toda a série de artigos de Como programar em Go.