Init functions e ordem de inicialização

1. O que são Init Functions?

Em Go, a função init() é uma função especial que executa automaticamente antes da função main(). Ela não aceita parâmetros, não retorna valores e não pode ser chamada explicitamente pelo programador. O compilador Go identifica e executa todas as funções init() definidas em um programa.

package main

import "fmt"

func init() {
    fmt.Println("Executando init()")
}

func main() {
    fmt.Println("Executando main()")
}

Diferenças entre init() e main():
- init() pode existir em qualquer pacote; main() apenas no pacote main
- Podem existir múltiplos init() em um mesmo pacote; apenas um main() por programa
- init() não pode ser chamado diretamente; main() é chamado pelo runtime

2. Ordem de Inicialização em um Único Pacote

Dentro de um único pacote, a ordem segue regras claras:

  1. Primeiro, variáveis no nível do pacote são inicializadas
  2. Depois, as funções init() são executadas

Exemplo com variáveis e init:

package main

import "fmt"

var x = func() int {
    fmt.Println("Inicializando x")
    return 10
}()

func init() {
    fmt.Println("Primeiro init")
}

func init() {
    fmt.Println("Segundo init")
}

func main() {
    fmt.Println("Valor de x:", x)
}

Saída:

Inicializando x
Primeiro init
Segundo init
Valor de x: 10

Ordem entre múltiplos arquivos: Quando um pacote possui múltiplos arquivos, as variáveis e init() são processados em ordem alfabética dos nomes dos arquivos.

// arquivo: a_init.go
package main

func init() {
    fmt.Println("init de a_init.go")
}

// arquivo: b_init.go
package main

func init() {
    fmt.Println("init de b_init.go")
}

Saída:

init de a_init.go
init de b_init.go

3. Ordem de Inicialização entre Pacotes Dependentes

O Go adota uma abordagem bottom-up: pacotes importados são inicializados antes dos pacotes que os importam.

// pacote a/a.go
package a

import "fmt"

func init() {
    fmt.Println("Pacote A: init")
}

// pacote b/b.go
package b

import "fmt"

func init() {
    fmt.Println("Pacote B: init")
}

// pacote main
package main

import (
    "fmt"
    "meuprojeto/a"
    "meuprojeto/b"
)

func init() {
    fmt.Println("Pacote main: init")
}

func main() {
    fmt.Println("Executando main")
}

Saída:

Pacote A: init
Pacote B: init
Pacote main: init
Executando main

Regras adicionais:
- Se pacotes no mesmo nível de dependência, a ordem é alfabética
- Se A importa B, B é inicializado antes de A
- Dependências transitivas seguem a mesma regra recursivamente

4. Múltiplas Init Functions no Mesmo Pacote

É possível ter múltiplos init() no mesmo arquivo ou em arquivos diferentes do mesmo pacote.

package config

import "fmt"

var settings map[string]string

func init() {
    fmt.Println("Config init 1")
    settings = make(map[string]string)
}

func init() {
    fmt.Println("Config init 2")
    settings["timeout"] = "30s"
    settings["retries"] = "3"
}

Boas práticas: Evite criar dependências entre múltiplos init() do mesmo pacote. A ordem dentro do mesmo arquivo segue a declaração, mas entre arquivos depende da ordem alfabética, o que pode ser frágil.

5. Init Functions e Ciclos de Importação

O compilador Go não permite ciclos de importação. Se o pacote A importa B e B importa A, o compilador gera erro.

// pacote a/a.go
package a

import "meuprojeto/b"

func init() {
    b.Do()
}

// pacote b/b.go
package b

import "meuprojeto/a"

func init() {
    a.Do()
}

Erro: import cycle not allowed

Solução: Extrair a funcionalidade comum para um terceiro pacote ou usar interfaces.

// pacote common/common.go
package common

type Service interface {
    Do()
}

// pacote a/a.go
package a

import "meuprojeto/common"

func UseService(s common.Service) {
    s.Do()
}

// pacote b/b.go
package b

import "meuprojeto/common"

type BService struct{}

func (b BService) Do() {
    // implementação
}

6. Casos de Uso Comuns

Registro de drivers de banco de dados:

package main

import (
    "database/sql"
    _ "github.com/lib/pq" // init() registra o driver PostgreSQL
)

func main() {
    db, err := sql.Open("postgres", "dsn")
    // ...
}

Inicialização de configurações:

package config

var AppConfig struct {
    Port int
    Debug bool
}

func init() {
    AppConfig.Port = 8080
    AppConfig.Debug = true
}

População de mapas globais:

package handlers

var routeMap = map[string]func(){}

func init() {
    routeMap["/api/users"] = handleUsers
    routeMap["/api/products"] = handleProducts
}

7. Boas Práticas e Armadilhas

Evite lógica complexa em init():

// Ruim: lógica complexa em init
func init() {
    resp, err := http.Get("https://api.exemplo.com/config")
    if err != nil {
        panic(err)
    }
    // processamento demorado
}

// Bom: função explícita de inicialização
func InitConfig() error {
    resp, err := http.Get("https://api.exemplo.com/config")
    if err != nil {
        return err
    }
    return processConfig(resp)
}

Cuidado com concorrência: Não use goroutines dentro de init().

Testabilidade: Código em init() dificulta testes unitários. Prefira funções explícitas:

// Em vez de init(), use:
func Initialize() error {
    // lógica de inicialização
    return nil
}

func TestSomething(t *testing.T) {
    if err := Initialize(); err != nil {
        t.Fatal(err)
    }
    // teste
}

8. Comparação com Table-Driven Tests

init() pode preparar dados para testes, mas tem limitações:

var testCases []struct {
    input    int
    expected int
}

func init() {
    testCases = []struct {
        input    int
        expected int
    }{
        {1, 2},
        {3, 4},
    }
}

func TestWithInit(t *testing.T) {
    for _, tc := range testCases {
        result := process(tc.input)
        if result != tc.expected {
            t.Errorf("esperado %d, obtido %d", tc.expected, result)
        }
    }
}

Alternativa com inicialização explícita:

func getTestCases() []struct {
    input    int
    expected int
} {
    return []struct {
        input    int
        expected int
    }{
        {1, 2},
        {3, 4},
    }
}

func TestWithExplicit(t *testing.T) {
    for _, tc := range getTestCases() {
        result := process(tc.input)
        if result != tc.expected {
            t.Errorf("esperado %d, obtido %d", tc.expected, result)
        }
    }
}

A inicialização explícita é preferível por ser mais testável e previsível.

Referências