Golang

Artigo 46 — Injeção de dependência sem frameworks Já leu

10 min de leitura

Artigo 46 — Injeção de dependência sem frameworks
Artigo 46 — Injeção de dependência sem frameworks Dependências explícitas: a filosofia Go Em Java e C#, frameworks de injeção de dependência como Spring e .NET

Artigo 46 — Injeção de dependência sem frameworks

Curso: Dominando Go em 1 Ano Prof. Ricardo Matos Módulo 8 — Arquitetura e Padrões


Dependências explícitas: a filosofia Go

Em Java e C#, frameworks de injeção de dependência como Spring e .NET DI são onipresentes. Em Go, a comunidade converge em torno de uma abordagem diferente: injeção de dependência manual, explícita, sem mágica de reflection em tempo de execução.

Essa escolha não é limitação — é filosofia. Código Go bem estruturado tem suas dependências visíveis na assinatura dos construtores. Não há anotações que escaneiam pacotes em tempo de execução, não há contêineres que resolvem dependências de forma implícita. O que você vê no código é exatamente o que acontece.


O problema que DI resolve

Sem injeção de dependência, componentes criam suas próprias dependências:

// Acoplamento forte — difícil de testar, difícil de substituir
type ServicoEmailCria struct{}

func (s *ServicoEmailCria) CriarUsuario(nome, email string) error {
    // Cria dependências internamente — impossível substituir em testes
    db, _ := sql.Open("postgres", "postgres://localhost/app")
    smtp := novoClienteSMTP("smtp.gmail.com", 587)
    logger := slog.Default()

    usuario := &Usuario{Nome: nome, Email: email}
    db.Exec("INSERT INTO usuarios ...", usuario)
    smtp.Enviar(email, "Bem-vindo!")
    logger.Info("usuário criado", "email", email)
    return nil
}

Com injeção de dependência, as dependências são fornecidas de fora:

// Acoplamento fraco — fácil de testar, fácil de substituir
type ServicoUsuario struct {
    repo   RepositorioUsuario
    mailer Mailer
    logger *slog.Logger
}

func NovoServicoUsuario(
    repo RepositorioUsuario,
    mailer Mailer,
    logger *slog.Logger,
) *ServicoUsuario {
    return &ServicoUsuario{
        repo:   repo,
        mailer: mailer,
        logger: logger,
    }
}

O padrão construtor: a DI idiomática em Go

O padrão central é simples: cada componente recebe suas dependências pelo construtor e as armazena em campos privados.

// Interfaces — o contrato
type RepositorioUsuario interface {
    Criar(ctx context.Context, u *Usuario) error
    BuscarPorEmail(ctx context.Context, email string) (*Usuario, error)
    BuscarPorID(ctx context.Context, id int64) (*Usuario, error)
}

type Mailer interface {
    EnviarBoasVindas(ctx context.Context, email, nome string) error
    EnviarRedefinicaoSenha(ctx context.Context, email, token string) error
}

type Cache interface {
    Get(ctx context.Context, chave string) (string, bool)
    Set(ctx context.Context, chave, valor string, ttl time.Duration) error
    Delete(ctx context.Context, chave string) error
}

// Serviço com dependências injetadas
type ServicoUsuario struct {
    repo   RepositorioUsuario
    mailer Mailer
    cache  Cache
    logger *slog.Logger
}

func NovoServicoUsuario(
    repo RepositorioUsuario,
    mailer Mailer,
    cache Cache,
    logger *slog.Logger,
) *ServicoUsuario {
    if repo == nil {
        panic("repo não pode ser nil")
    }
    if logger == nil {
        logger = slog.Default()
    }
    return &ServicoUsuario{
        repo:   repo,
        mailer: mailer,
        cache:  cache,
        logger: logger,
    }
}

func (s *ServicoUsuario) Criar(ctx context.Context, input CriarUsuarioInput) (*Usuario, error) {
    // Verifica duplicata via cache antes do banco
    if _, existe := s.cache.Get(ctx, "email:"+input.Email); existe {
        return nil, ErrEmailJaUsado
    }

    usuario := &Usuario{
        Nome:  input.Nome,
        Email: input.Email,
        Senha: hashSenha(input.Senha),
    }

    if err := s.repo.Criar(ctx, usuario); err != nil {
        return nil, fmt.Errorf("persistir usuário: %w", err)
    }

    // Invalida cache e envia email — falhas não propagam
    s.cache.Set(ctx, "email:"+input.Email, "1", 24*time.Hour)

    go func() {
        if s.mailer != nil {
            if err := s.mailer.EnviarBoasVindas(ctx, input.Email, input.Nome); err != nil {
                s.logger.Warn("enviar boas-vindas falhou", "email", input.Email, "err", err)
            }
        }
    }()

    s.logger.Info("usuário criado", "id", usuario.ID, "email", usuario.Email)
    return usuario, nil
}

Opções funcionais: construtores flexíveis

Quando um construtor tem muitos parâmetros opcionais, o padrão de opções funcionais resolve o problema sem proliferar sobrecargas:

package servidor

import (
    "net/http"
    "time"
)

type Servidor struct {
    host         string
    porta        int
    readTimeout  time.Duration
    writeTimeout time.Duration
    middlewares  []func(http.Handler) http.Handler
    logger       *slog.Logger
    tls          *TLSConfig
}

// Opcao é uma função que configura um Servidor.
type Opcao func(*Servidor)

// Construtores de opções — cada um configura um aspecto

func ComHost(host string) Opcao {
    return func(s *Servidor) {
        s.host = host
    }
}

func ComPorta(porta int) Opcao {
    return func(s *Servidor) {
        s.porta = porta
    }
}

func ComReadTimeout(d time.Duration) Opcao {
    return func(s *Servidor) {
        s.readTimeout = d
    }
}

func ComWriteTimeout(d time.Duration) Opcao {
    return func(s *Servidor) {
        s.writeTimeout = d
    }
}

func ComMiddleware(m func(http.Handler) http.Handler) Opcao {
    return func(s *Servidor) {
        s.middlewares = append(s.middlewares, m)
    }
}

func ComLogger(logger *slog.Logger) Opcao {
    return func(s *Servidor) {
        s.logger = logger
    }
}

func ComTLS(certFile, keyFile string) Opcao {
    return func(s *Servidor) {
        s.tls = &TLSConfig{CertFile: certFile, KeyFile: keyFile}
    }
}

// NovoServidor cria um Servidor com valores padrão e aplica as opções.
func NovoServidor(handler http.Handler, opts ...Opcao) *Servidor {
    s := &Servidor{
        host:         "0.0.0.0",
        porta:        8080,
        readTimeout:  5 * time.Second,
        writeTimeout: 10 * time.Second,
        logger:       slog.Default(),
    }

    for _, opt := range opts {
        opt(s)
    }

    return s
}

// Uso: limpo, explícito, sem struct Config separada
srv := NovoServidor(
    meuHandler,
    ComPorta(9090),
    ComReadTimeout(10*time.Second),
    ComLogger(logger),
    ComMiddleware(middlewareAuth),
    ComMiddleware(middlewareLogging),
)

Composição no main: a raiz da árvore de dependências

O main.go é o único lugar onde todas as dependências são instanciadas e conectadas. Esse ponto de composição é chamado de composition root:

// cmd/api/main.go
package main

import (
    "context"
    "database/sql"
    "log/slog"
    "os"

    "loja/internal/config"
    "loja/internal/handler"
    "loja/internal/infra/cache/redis"
    "loja/internal/infra/email/sendgrid"
    "loja/internal/infra/postgres"
    "loja/internal/servidor"
    "loja/internal/usecase"
)

func main() {
    ctx := context.Background()

    // 1. Logger
    logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
    slog.SetDefault(logger)

    // 2. Configuração
    cfg, err := config.Carregar("")
    if err != nil {
        logger.Error("configuração inválida", "err", err)
        os.Exit(1)
    }

    // 3. Infraestrutura — implementações concretas
    db, err := abrirBancoDados(cfg.Banco)
    if err != nil {
        logger.Error("banco inacessível", "err", err)
        os.Exit(1)
    }
    defer db.Close()

    cacheCliente := redis.NovoCache(cfg.Cache.URL)
    mailerCliente := sendgrid.NovoMailer(cfg.Sendgrid.APIKey)

    // 4. Repositórios
    usuarioRepo := postgres.NovoRepositorioUsuario(db)
    produtoRepo := postgres.NovoRepositorioProduto(db)
    pedidoRepo := postgres.NovoRepositorioPedido(db)

    // 5. Serviços de domínio
    politicaDesconto := domainservico.NovaPoliticaDesconto()

    // 6. Casos de uso — injetam repositórios e serviços
    criarUsuario := usecase.NovoCriarUsuario(usuarioRepo, mailerCliente, cacheCliente, logger)
    criarPedido := usecase.NovoCriarPedido(pedidoRepo, produtoRepo, politicaDesconto, logger)
    processarPagamento := usecase.NovoProcessarPagamento(pedidoRepo, logger)

    // 7. Handlers HTTP — injetam casos de uso
    usuarioHandler := handler.NovoUsuarioHandler(criarUsuario, logger)
    pedidoHandler := handler.NovoPedidoHandler(criarPedido, processarPagamento, logger)

    // 8. Servidor
    srv := servidor.NovoServidor(
        montarRotas(usuarioHandler, pedidoHandler),
        servidor.ComPorta(cfg.Servidor.Porta),
        servidor.ComLogger(logger),
        servidor.ComMiddleware(middlewareAuth(cfg.JWT.Segredo)),
    )

    if err := srv.Iniciar(ctx); err != nil {
        logger.Error("servidor encerrado com erro", "err", err)
        os.Exit(1)
    }
}

Testando com mocks manuais

A vantagem mais concreta da DI é a facilidade de testar. Mocks manuais são simples e explícitos:

// Mocks para testes

type mockRepositorioUsuario struct {
    criarFn         func(ctx context.Context, u *Usuario) error
    buscarPorEmailFn func(ctx context.Context, email string) (*Usuario, error)
    buscarPorIDFn    func(ctx context.Context, id int64) (*Usuario, error)
}

func (m *mockRepositorioUsuario) Criar(ctx context.Context, u *Usuario) error {
    if m.criarFn != nil {
        return m.criarFn(ctx, u)
    }
    return nil
}

func (m *mockRepositorioUsuario) BuscarPorEmail(ctx context.Context, email string) (*Usuario, error) {
    if m.buscarPorEmailFn != nil {
        return m.buscarPorEmailFn(ctx, email)
    }
    return nil, nil
}

func (m *mockRepositorioUsuario) BuscarPorID(ctx context.Context, id int64) (*Usuario, error) {
    if m.buscarPorIDFn != nil {
        return m.buscarPorIDFn(ctx, id)
    }
    return nil, nil
}

type mockMailer struct {
    boasVindasEnviadas []string
    erroEnviar         error
}

func (m *mockMailer) EnviarBoasVindas(_ context.Context, email, _ string) error {
    m.boasVindasEnviadas = append(m.boasVindasEnviadas, email)
    return m.erroEnviar
}

func (m *mockMailer) EnviarRedefinicaoSenha(_ context.Context, _, _ string) error {
    return nil
}

type mockCache struct {
    dados map[string]string
}

func novoMockCache() *mockCache {
    return &mockCache{dados: make(map[string]string)}
}

func (m *mockCache) Get(_ context.Context, chave string) (string, bool) {
    v, existe := m.dados[chave]
    return v, existe
}

func (m *mockCache) Set(_ context.Context, chave, valor string, _ time.Duration) error {
    m.dados[chave] = valor
    return nil
}

func (m *mockCache) Delete(_ context.Context, chave string) error {
    delete(m.dados, chave)
    return nil
}

// Testes usando os mocks
func TestCriarUsuario_Sucesso(t *testing.T) {
    var usuarioCriado *Usuario

    repo := &mockRepositorioUsuario{
        criarFn: func(_ context.Context, u *Usuario) error {
            u.ID = 1 // simula ID gerado pelo banco
            usuarioCriado = u
            return nil
        },
    }
    mailer := &mockMailer{}
    cache := novoMockCache()
    logger := slog.New(slog.NewTextHandler(io.Discard, nil))

    servico := NovoServicoUsuario(repo, mailer, cache, logger)

    resultado, err := servico.Criar(context.Background(), CriarUsuarioInput{
        Nome:  "Ricardo",
        Email: "ricardo@exemplo.com",
        Senha: "senha-forte-123",
    })

    if err != nil {
        t.Fatalf("esperava sucesso, obteve: %v", err)
    }
    if resultado.ID != 1 {
        t.Errorf("ID esperado 1, obtido %d", resultado.ID)
    }
    if usuarioCriado.Email != "ricardo@exemplo.com" {
        t.Errorf("email incorreto: %s", usuarioCriado.Email)
    }

    // Verifica que o email foi agendado
    time.Sleep(10 * time.Millisecond) // goroutine assíncrona
    if len(mailer.boasVindasEnviadas) != 1 {
        t.Error("email de boas-vindas não foi enviado")
    }
}

func TestCriarUsuario_EmailDuplicado(t *testing.T) {
    cache := novoMockCache()
    cache.Set(context.Background(), "email:duplicado@exemplo.com", "1", time.Hour)

    servico := NovoServicoUsuario(
        &mockRepositorioUsuario{},
        &mockMailer{},
        cache,
        slog.Default(),
    )

    _, err := servico.Criar(context.Background(), CriarUsuarioInput{
        Nome:  "Outro",
        Email: "duplicado@exemplo.com",
        Senha: "senha123",
    })

    if !errors.Is(err, ErrEmailJaUsado) {
        t.Errorf("esperava ErrEmailJaUsado, obteve: %v", err)
    }
}

Wire: geração de código para DI complexa

Quando o grafo de dependências cresce — dezenas de serviços com dependências profundas — o main.go manual pode se tornar verboso. O Wire da Google gera automaticamente o código de inicialização:

go install github.com/google/wire/cmd/wire@latest
go get github.com/google/wire
// wire.go — apenas em tempo de desenvolvimento, não compila no binário final
//go:build wireinject

package main

import (
    "github.com/google/wire"
    "loja/internal/infra/postgres"
    "loja/internal/usecase"
)

func inicializarApp(cfg *config.Config) (*App, error) {
    wire.Build(
        // Infraestrutura
        abrirBancoDados,
        postgres.NovoRepositorioUsuario,
        postgres.NovoRepositorioProduto,
        redis.NovoCache,
        sendgrid.NovoMailer,

        // Casos de uso
        usecase.NovoCriarUsuario,
        usecase.NovoCriarPedido,

        // Handlers
        handler.NovoUsuarioHandler,
        handler.NovoPedidoHandler,

        // App
        novaApp,
    )
    return &App{}, nil
}
# Gera wire_gen.go com o código de inicialização
wire ./cmd/api/

O Wire analisa as assinaturas dos construtores, resolve o grafo de dependências e gera código Go limpo e legível — sem reflection em runtime.


Comparativo: manual vs Wire

Aspecto Manual Wire
Complexidade inicial Baixa Média
Legibilidade do código gerado Excelente Excelente
Manutenção com muitas deps Verboso Automático
Erros detectados Em runtime Em tempo de geração
Dependência externa Nenhuma wire (dev only)
Adequado para Projetos pequenos/médios Projetos grandes

Para projetos com até 20-30 serviços, DI manual é perfeitamente gerenciável e preferível pela clareza. Para projetos maiores, Wire elimina uma classe de erros de inicialização e reduz código boilerplate.


Resumo do que foi coberto

Este artigo apresentou injeção de dependência em Go sem frameworks: o problema do acoplamento forte, o padrão construtor para DI explícita, opções funcionais para construtores com parâmetros opcionais, a composition root no main.go como único ponto de instanciação, mocks manuais para testes unitários sem dependência de infraestrutura, e o Wire para geração automática de código em projetos com grafos de dependência complexos. O próximo artigo explora Design Patterns idiomáticos em Go.


Referências e leituras complementares

  • Wire — Google — Documentação e guia do gerador de DI. https://github.com/google/wire

  • Wire Tutorial — Tutorial oficial de Wire com exemplos progressivos. https://github.com/google/wire/blob/main/docs/guide.md

  • Dependency Injection in Go — Artigo de referência da comunidade. https://go.dev/doc/faq#methods_on_values_or_pointers

  • Functional Options Pattern — Dave Cheney — Artigo seminal sobre opções funcionais. https://dave.cheney.net/2014/10/17/functional-options-for-friendly-apis

  • Testify/mock — Alternativa à mocks manuais com geração automática. https://github.com/stretchr/testify

  • mockery — Gerador de mocks a partir de interfaces Go. https://github.com/vektra/mockery


Próximo artigo: Artigo 47 — Design Patterns em Go: Repository, Factory e Strategy**


you asked

Sim


claude response

Comentários

Mais em Golang

Composição vs herança: o jeito Go de reutilizar código
Composição vs herança: o jeito Go de reutilizar código

A herança é um dos pilares da orientação a objetos clássica. Em Java, C++ e C...

Artigo 45 — Domain-Driven Design aplicado a projetos Go
Artigo 45 — Domain-Driven Design aplicado a projetos Go

Artigo 45 — Domain-Driven Design aplicado a projetos Go DDD: o domínio como c...

Go com MongoDB: operações CRUD com o driver oficial
Go com MongoDB: operações CRUD com o driver oficial

MongoDB armazena dados como documentos BSON — uma representação binária de JS...