Por que evitar o uso da função init

Nas últimas semanas, tenho focado muito em escrever sobre o que evitar na hora de escrever programas Go.

Tenho feito isso pois, em alguns casos, o melhor que se pode saber sobre uma feature ou package é exatamente quando não utilizar.

Por isso, dando continuidade nesse tipo de post, neste, vamos explorar o que é a função init, como ela funciona, porque seu uso pode ser problemático e quando ela deve ser utilizada de forma consciente.

O que é e para que serve a função init

A função init em Go é uma função especial que é automaticamente executada pelo runtime antes da função main, sem a necessidade de ser chamada explicitamente.

O objetivo dela é executar código de inicialização, como configurações ou setups específicos que precisam ocorrer antes que a aplicação principal entre em ação.

Cada arquivo Go em um pacote pode conter uma ou mais funções init, o que pode ser útil para fazer configurações que envolvem variáveis globais, manipulações de estado inicial ou a configuração de um ambiente de testes.

Regras de execução do init:

  1. Ordem de execução no arquivo: Dentro de um único arquivo Go, as funções init são executadas na ordem em que aparecem no código.
  2. Múltiplos arquivos no mesmo pacote: Quando um pacote contém vários arquivos com funções init, a execução das funções init segue a ordem de dependência dos arquivos. Primeiro, as variáveis globais e as funções init de pacotes importados são executadas. Em seguida, as funções init dos arquivos do pacote corrente são executadas na ordem alfabética dos nomes dos arquivos.
  3. Ordem em pacotes importados: Pacotes que importam outros pacotes executam suas funções init somente após as funções init de todos os pacotes dependentes serem executadas. Isso segue a ordem de dependências entre pacotes.

Embora init tenha um papel útil na inicialização de pacotes, sua presença pode trazer diversos desafios, como veremos a seguir.

Problemas de se utilizar a função init

O uso da função init pode criar dificuldades que prejudicam a legibilidade, previsibilidade e manutenção do código. Alguns dos principais problemas incluem:

  1. Falta de clareza e visibilidade: A função init não é explicitamente chamada em lugar algum no código, tornando difícil para os desenvolvedores novos no projeto entenderem o que está sendo inicializado ou quando isso está acontecendo. Isso cria uma “mágica” que pode confundir o fluxo de execução da aplicação.
  2. Dificuldade de teste: Como a função init é chamada automaticamente pelo runtime, ela é executada antes de qualquer lógica de testes. Isso pode tornar os testes imprevisíveis ou até mesmo difíceis de escrever, uma vez que qualquer comportamento inicializado por init não pode ser controlado ou configurado diretamente nos testes unitários.
  3. Ordem de execução oculta: A ordem de execução das funções init em múltiplos arquivos ou pacotes é determinada pelo compilador, com base nas dependências, tornando o comportamento difícil de prever em projetos grandes. Isso aumenta o risco de bugs difíceis de rastrear, já que a ordem nem sempre é intuitiva.
  4. Complica a refatoração: Funções init escondem lógicas de inicialização que podem se tornar complicadas de refatorar ou remover. A falta de uma chamada explícita torna mais difícil entender dependências entre módulos, criando uma teia complexa de inicialização.

Quando devemos utilizar

Embora o uso da função init seja desaconselhado na maioria dos casos, ela ainda tem alguns cenários em que pode ser útil, desde que usada com parcimônia e de forma consciente.

Aqui estão três exemplos de quando o uso do init pode ser adequado:

  1. Registro de drivers ou plugins: Quando há necessidade de registrar um driver, como em um banco de dados, ou um plugin, a função init pode ser utilizada para garantir que esse registro aconteça logo que o pacote é importado. func init() { sql.Register("mydriver", &MyDriver{}) }
  2. Configuração de variáveis de ambiente: Em casos em que variáveis de ambiente precisam ser configuradas automaticamente antes da execução do restante do código, o init pode ser usado. func init() { os.Setenv("CONFIG_PATH", "/default/path") }
  3. Configuração de pacotes externos: Para inicializar e configurar pacotes externos, como registradores de métricas ou loggers globais, init pode ser útil. func init() { prometheus.MustRegister(myCollector) }

Conclusão

A função init em Go pode parecer uma solução prática para inicializações automáticas, mas seu uso deve ser evitado na maior parte dos casos devido aos problemas de clareza, testes e previsibilidade que ela introduz.

Ao invés de depender de init, é mais seguro optar por inicializações explícitas e bem controladas. No entanto, em cenários específicos, como a configuração de plugins ou validações iniciais, o init pode ser utilizado de forma consciente. A chave está em entender bem as consequências do uso dessa função e usá-la com moderação.

Até a próxima!


Faça parte da comunidade!

Receba os melhores conteúdos sobre Go, Kubernetes, arquitetura de software, Cloud e esteja sempre atualizado com as tendências e práticas do mercado.

* indicates required

Deixe uma resposta