Injeção de Dependência em Go: Padrões e Melhores Práticas

Domine padrões de DI para código Go testável

Conteúdo da página

A injeção de dependência (DI) é um padrão de projeto fundamental que promove código limpo, testável e mantível em aplicações Go.

Seja você construindo APIs REST, implementando padrões de banco de dados multi-tenant ou trabalhando com bibliotecas ORM, entender a injeção de dependência melhorará significativamente a qualidade do seu código.

go-dependency-injection

O que é Injeção de Dependência?

A injeção de dependência é um padrão de projeto onde os componentes recebem suas dependências de fontes externas, em vez de criá-las internamente. Essa abordagem desacopla os componentes, tornando seu código mais modular, testável e fácil de manter.

Em Go, a injeção de dependência é particularmente poderosa devido à filosofia de design baseada em interfaces da linguagem. A satisfação implícita de interfaces em Go significa que você pode trocar implementações facilmente sem modificar o código existente.

Por que Usar Injeção de Dependência em Go?

Melhor Testabilidade: Ao injetar dependências, você pode facilmente substituir implementações reais por mocks ou duplicatas de teste. Isso permite que você escreva testes unitários que são rápidos, isolados e não exigem serviços externos como bancos de dados ou APIs.

Melhor Manutenibilidade: As dependências tornam-se explícitas no seu código. Ao olhar para uma função construtora, você vê imediatamente o que um componente requer. Isso torna a base de código mais fácil de entender e modificar.

Acoplamento Fraco: Os componentes dependem de abstrações (interfaces) em vez de implementações concretas. Isso significa que você pode alterar implementações sem afetar o código dependente.

Flexibilidade: Você pode configurar diferentes implementações para diferentes ambientes (desenvolvimento, teste, produção) sem alterar sua lógica de negócios.

Injeção via Construtor: A Maneira Go

A maneira mais comum e idiomática de implementar injeção de dependência em Go é através de funções construtoras. Elas são tipicamente nomeadas NewXxx e aceitam dependências como parâmetros.

Exemplo Básico

Aqui está um exemplo simples demonstrando a injeção via construtor:

// Define uma interface para o repositório
type UserRepository interface {
    FindByID(id int) (*User, error)
    Save(user *User) error
}

// O serviço depende da interface do repositório
type UserService struct {
    repo UserRepository
}

// O construtor injeta a dependência
func NewUserService(repo UserRepository) *UserService {
    return &UserService{repo: repo}
}

// Os métodos usam a dependência injetada
func (s *UserService) GetUser(id int) (*User, error) {
    return s.repo.FindByID(id)
}

Este padrão deixa claro que UserService requer um UserRepository. Você não pode criar um UserService sem fornecer um repositório, o que evita erros de tempo de execução devidos a dependências faltantes.

Múltiplas Dependências

Quando um componente tem múltiplas dependências, basta adicioná-las como parâmetros do construtor:

type EmailService interface {
    Send(to, subject, body string) error
}

type Logger interface {
    Info(msg string)
    Error(msg string, err error)
}

type OrderService struct {
    repo        OrderRepository
    emailSvc    EmailService
    logger      Logger
    paymentSvc  PaymentService
}

func NewOrderService(
    repo OrderRepository,
    emailSvc EmailService,
    logger Logger,
    paymentSvc PaymentService,
) *OrderService {
    return &OrderService{
        repo:       repo,
        emailSvc:   emailSvc,
        logger:     logger,
        paymentSvc: paymentSvc,
    }
}

Design de Interfaces para Injeção de Dependência

Um dos princípios-chave ao implementar injeção de dependência é o Princípio da Inversão de Dependência (DIP): módulos de alto nível não devem depender de módulos de baixo nível; ambos devem depender de abstrações.

Em Go, isso significa definir interfaces pequenas e focadas que representem apenas o que seu componente precisa:

// Bom: Interface pequena e focada
type PaymentProcessor interface {
    ProcessPayment(amount float64) error
}

// Ruim: Interface grande com métodos desnecessários
type PaymentService interface {
    ProcessPayment(amount float64) error
    RefundPayment(id string) error
    GetPaymentHistory(userID int) ([]Payment, error)
    UpdatePaymentMethod(userID int, method PaymentMethod) error
    // ... muitos mais métodos
}

A interface menor segue o Princípio da Segregação de Interface—os clientes não devem depender de métodos que não usam. Isso torna seu código mais flexível e mais fácil de testar.

Exemplo do Mundo Real: Abstração de Banco de Dados

Ao trabalhar com bancos de dados em aplicações Go, você frequentemente precisará abstrair operações de banco de dados. Veja como a injeção de dependência ajuda:

// Interface de banco de dados - abstração de alto nível
type DB interface {
    Query(ctx context.Context, query string, args ...interface{}) (Rows, error)
    Exec(ctx context.Context, query string, args ...interface{}) (Result, error)
    BeginTx(ctx context.Context) (Tx, error)
}

// O repositório depende da abstração
type UserRepository struct {
    db DB
}

func NewUserRepository(db DB) *UserRepository {
    return &UserRepository{db: db}
}

func (r *UserRepository) FindByID(ctx context.Context, id int) (*User, error) {
    query := "SELECT id, name, email FROM users WHERE id = $1"
    rows, err := r.db.Query(ctx, query, id)
    if err != nil {
        return nil, err
    }
    defer rows.Close()
    
    // ... parse rows
}

Este padrão é especialmente útil ao implementar padrões de banco de dados multi-tenant, onde você pode precisar alternar entre diferentes implementações de banco de dados ou estratégias de conexão.

O Padrão Composition Root

O Composition Root é o local onde você monta todas as suas dependências no ponto de entrada da aplicação (tipicamente main). Isso centraliza a configuração de dependências e torna o grafo de dependências explícito.

func main() {
    // Inicializa dependências de infraestrutura
    db := initDatabase()
    logger := initLogger()
    
    // Inicializa repositórios
    userRepo := NewUserRepository(db)
    orderRepo := NewOrderRepository(db)
    
    // Inicializa serviços com dependências
    emailSvc := NewEmailService(logger)
    paymentSvc := NewPaymentService(logger)
    userSvc := NewUserService(userRepo, logger)
    orderSvc := NewOrderService(orderRepo, emailSvc, logger, paymentSvc)
    
    // Inicializa manipuladores HTTP
    userHandler := NewUserHandler(userSvc)
    orderHandler := NewOrderHandler(orderSvc)
    
    // Conecta as rotas
    router := setupRouter(userHandler, orderHandler)
    
    // Inicia o servidor
    log.Fatal(http.ListenAndServe(":8080", router))
}

Esta abordagem deixa claro como sua aplicação está estruturada e de onde vêm as dependências. É particularmente valioso ao construir APIs REST em Go, onde você precisa coordenar múltiplas camadas de dependências.

Frameworks de Injeção de Dependência

Para aplicações maiores com grafos de dependência complexos, gerenciar dependências manualmente pode se tornar trabalhoso. Go possui vários frameworks de DI que podem ajudar:

Google Wire (DI em Tempo de Compilação)

Wire é uma ferramenta de injeção de dependência em tempo de compilação que gera código. É segura quanto aos tipos e não tem sobrecarga em tempo de execução.

Instalação:

go install github.com/google/wire/cmd/wire@latest

Exemplo:

// wire.go
//go:build wireinject
// +build wireinject

package main

import "github.com/google/wire"

func InitializeApp() (*App, error) {
    wire.Build(
        NewDB,
        NewUserRepository,
        NewUserService,
        NewUserHandler,
        NewApp,
    )
    return &App{}, nil
}

Wire gera o código de injeção de dependência em tempo de compilação, garantindo segurança de tipos e eliminando a sobrecarga de reflexão em tempo de execução.

Uber Dig (DI em Tempo de Execução)

Dig é um framework de injeção de dependência em tempo de execução que usa reflexão. É mais flexível, mas tem algum custo em tempo de execução.

Exemplo:

import "go.uber.org/dig"

func main() {
    container := dig.New()
    
    // Registra provedores
    container.Provide(NewDB)
    container.Provide(NewUserRepository)
    container.Provide(NewUserService)
    container.Provide(NewUserHandler)
    
    // Invoca função que precisa de dependências
    err := container.Invoke(func(handler *UserHandler) {
        // Usa handler
    })
    if err != nil {
        log.Fatal(err)
    }
}

Quando Usar Frameworks

Use um framework quando:

  • Seu grafo de dependência é complexo com muitos componentes interdependentes
  • Você tem múltiplas implementações da mesma interface que precisam ser selecionadas com base na configuração
  • Você deseja resolução automática de dependências
  • Você está construindo uma aplicação grande onde o encaminhamento manual torna-se propenso a erros

Mantenha a DI manual quando:

  • Sua aplicação é pequena ou de médio porte
  • O grafo de dependência é simples e fácil de seguir
  • Você quer manter as dependências mínimas e explícitas
  • Você prefere código explícito em vez de código gerado

Testando com Injeção de Dependência

Um dos principais benefícios da injeção de dependência é a melhor testabilidade. Veja como a DI torna os testes mais fáceis:

Exemplo de Teste Unitário

// Implementação mock para testes
type mockUserRepository struct {
    users map[int]*User
    err   error
}

func (m *mockUserRepository) FindByID(id int) (*User, error) {
    if m.err != nil {
        return nil, m.err
    }
    return m.users[id], nil
}

func (m *mockUserRepository) Save(user *User) error {
    if m.err != nil {
        return m.err
    }
    m.users[user.ID] = user
    return nil
}

// Teste usando o mock
func TestUserService_GetUser(t *testing.T) {
    mockRepo := &mockUserRepository{
        users: map[int]*User{
            1: {ID: 1, Name: "John", Email: "john@example.com"},
        },
    }
    
    service := NewUserService(mockRepo)
    
    user, err := service.GetUser(1)
    assert.NoError(t, err)
    assert.Equal(t, "John", user.Name)
}

Este teste é executado rapidamente, não requer um banco de dados e testa sua lógica de negócios de forma isolada. Ao trabalhar com bibliotecas ORM em Go, você pode injetar repositórios mock para testar a lógica do serviço sem configuração de banco de dados.

Padrões Comuns e Melhores Práticas

1. Use Segregação de Interfaces

Mantenha as interfaces pequenas e focadas no que o cliente realmente precisa:

// Bom: O cliente só precisa ler usuários
type UserReader interface {
    FindByID(id int) (*User, error)
    FindByEmail(email string) (*User, error)
}

// Interface separada para escrita
type UserWriter interface {
    Save(user *User) error
    Delete(id int) error
}

2. Retorne Erros dos Construtores

Os construtores devem validar dependências e retornar erros se a inicialização falhar:

func NewUserService(repo UserRepository) (*UserService, error) {
    if repo == nil {
        return nil, errors.New("user repository cannot be nil")
    }
    return &UserService{repo: repo}, nil
}

3. Use Contexto para Dependências com Escopo de Requisição

Para dependências específicas de requisição (como transações de banco de dados), passe-as via contexto:

type ctxKey string

const dbKey ctxKey = "db"

func WithDB(ctx context.Context, db DB) context.Context {
    return context.WithValue(ctx, dbKey, db)
}

func DBFromContext(ctx context.Context) (DB, bool) {
    db, ok := ctx.Value(dbKey).(DB)
    return db, ok
}

4. Evite Injeção Excessiva

Não injete dependências que são verdadeiramente detalhes de implementação interna. Se um componente cria e gerencia seus próprios objetos auxiliares, isso está bem:

// Bom: Auxiliar interno não precisa de injeção
type UserService struct {
    repo UserRepository
    // Cache interno - não precisa de injeção
    cache map[int]*User
}

func NewUserService(repo UserRepository) *UserService {
    return &UserService{
        repo:  repo,
        cache: make(map[int]*User),
    }
}

5. Documente Dependências

Use comentários para documentar por que as dependências são necessárias e quaisquer restrições:

// UserService lida com lógica de negócios relacionada a usuários.
// Ele requer um UserRepository para acesso a dados e um Logger para
// rastreamento de erros. O repositório deve ser thread-safe se usado
// em contextos concorrentes.
func NewUserService(repo UserRepository, logger Logger) *UserService {
    // ...
}

Quando NÃO Usar Injeção de Dependência

A injeção de dependência é uma ferramenta poderosa, mas nem sempre é necessária:

Pule a DI para:

  • Objetos de valor simples ou estruturas de dados
  • Funções auxiliares internas ou utilitários
  • Scripts pontuais ou pequenos utilitários
  • Quando a instanciação direta for mais clara e simples

Exemplo de quando NÃO usar DI:

// Estrutura simples - não há necessidade de DI
type Point struct {
    X, Y float64
}

func NewPoint(x, y float64) Point {
    return Point{X: x, Y: y}
}

// Utilitário simples - não há necessidade de DI
func FormatCurrency(amount float64) string {
    return fmt.Sprintf("$%.2f", amount)
}

Integração com o Ecossistema Go

A injeção de dependência funciona perfeitamente com outros padrões e ferramentas do Go. Ao construir aplicações que usam a biblioteca padrão do Go para web scraping ou gerando relatórios em PDF, você pode injetar esses serviços em sua lógica de negócios:

type PDFGenerator interface {
    GenerateReport(data ReportData) ([]byte, error)
}

type ReportService struct {
    pdfGen PDFGenerator
    repo   ReportRepository
}

func NewReportService(pdfGen PDFGenerator, repo ReportRepository) *ReportService {
    return &ReportService{
        pdfGen: pdfGen,
        repo:   repo,
    }
}

Isso permite que você troque implementações de geração de PDF ou use mocks durante os testes.

Conclusão

A injeção de dependência é uma pedra angular para escrever código Go mantível e testável. Ao seguir os padrões descritos neste artigo—injeção via construtor, design baseado em interfaces e o padrão Composition Root—você criará aplicações que são mais fáceis de entender, testar e modificar.

Comece com a injeção de construtor manual para aplicações pequenas e de médio porte, e considere frameworks como Wire ou Dig à medida que seu grafo de dependências cresce. Lembre-se de que o objetivo é clareza e testabilidade, não complexidade por si só. Se você estiver estruturando sua aplicação em torno de manipuladores de comandos e consultas, Implementando CQRS em Go mostra como a mesma abordagem de injeção via construtor conecta limpa e sem cerimônia as structs Application, Commands e Queries.

Para mais recursos de desenvolvimento Go, confira nosso Cheat Sheet de Go para referência rápida sobre sintaxe Go e padrões comuns.

Recursos Externos

Assinar

Receba novos artigos sobre sistemas, infraestrutura e engenharia de IA.