Dependency injection com wire ou fx

1. Introdução à Injeção de Dependência em Go

Dependency Injection (DI) é um padrão de projeto onde as dependências de um componente são fornecidas externamente, em vez de criadas internamente. Em Go, isso significa que uma struct recebe suas dependências (como conexões de banco, serviços HTTP, loggers) através do construtor ou setters, em vez de instanciá-las diretamente.

Problemas comuns sem DI:

type UserHandler struct {
    db *sql.DB // Acoplamento direto
}

func NewUserHandler() *UserHandler {
    db, _ := sql.Open("postgres", "conn_string")
    return &UserHandler{db: db}
}
  • Acoplamento rígido: o handler conhece detalhes de implementação do banco
  • Testes difíceis: impossível mockar o banco sem refatorar
  • Inicialização manual: cada componente precisa montar suas dependências

As duas abordagens principais para DI em Go são: frameworks estáticos (Wire) e dinâmicos (Fx). Vamos explorar cada uma.

2. Wire: Injeção Estática em Tempo de Compilação

Wire do Google gera código de inicialização em tempo de compilação, eliminando overhead de runtime. Seus conceitos fundamentais são providers, injectors e sets.

Providers: funções que criam dependências.

// provider.go
package main

import "database/sql"

func NewDB() (*sql.DB, error) {
    return sql.Open("postgres", "conn_string")
}

type UserRepository struct {
    db *sql.DB
}

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

Injector: função que Wire gera automaticamente.

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

package main

import "github.com/google/wire"

func InitializeApp() (*App, error) {
    wire.Build(
        NewDB,
        NewUserRepository,
        NewUserHandler,
        wire.Struct(new(App), "*"),
    )
    return nil, nil
}

Execute wire no terminal para gerar wire_gen.go:

// wire_gen.go (gerado automaticamente)
func InitializeApp() (*App, error) {
    db, err := NewDB()
    if err != nil {
        return nil, err
    }
    userRepository := NewUserRepository(db)
    userHandler := NewUserHandler(userRepository)
    app := &App{
        UserHandler: userHandler,
    }
    return app, nil
}

Tratamento de erros e dependências opcionais:

wire.Build(
    NewDB,
    wire.Value(Config{Port: 8080}), // Valor fixo
    wire.Struct(new(App), "*"),
)

Wire é ideal para aplicações onde performance e previsibilidade são críticas, como CLIs e microsserviços simples.

3. Fx: Injeção Dinâmica com Runtime e Ciclo de Vida

Fx da Uber oferece DI em runtime com gerenciamento de ciclo de vida. A estrutura básica usa fx.New, fx.Options e fx.Provide.

Estrutura de um aplicativo Fx:

package main

import (
    "go.uber.org/fx"
    "net/http"
)

func NewHTTPServer(lc fx.Lifecycle) *http.Server {
    srv := &http.Server{Addr: ":8080"}
    lc.Append(fx.Hook{
        OnStart: func(ctx context.Context) error {
            go srv.ListenAndServe()
            return nil
        },
        OnStop: func(ctx context.Context) error {
            return srv.Shutdown(ctx)
        },
    })
    return srv
}

func main() {
    fx.New(
        fx.Provide(NewHTTPServer),
        fx.Invoke(func(srv *http.Server) {
            // O servidor já iniciou via lifecycle
        }),
    ).Run()
}

Parâmetros nomeados e grupos:

type HandlerParams struct {
    fx.In
    UserRepo  *UserRepository `name:"user"`
    AdminRepo *AdminRepository `name:"admin"`
}

fx.Provide(
    fx.Annotated{
        Name: "user",
        Target: NewUserRepository,
    },
    fx.Annotated{
        Name: "admin",
        Target: NewAdminRepository,
    },
)

Fx é excelente para aplicações complexas com lifecycle gerenciado, como servidores HTTP/gRPC e sistemas que precisam de graceful shutdown.

4. Comparação Prática: Wire vs. Fx

Característica Wire Fx
Performance Overhead zero em runtime Overhead mínimo de reflection
Tempo de erro Compilação Runtime
Lifecycle Manual Automático (OnStart/OnStop)
Complexidade Baixa (gera código simples) Média (abstrações do Fx)
Debug Fácil (código gerado legível) Médio (precisa entender lifecycle)

Casos de uso ideais:

  • Wire: CLIs, ferramentas de linha de comando, microsserviços simples, aplicações críticas onde performance importa
  • Fx: Servidores web, sistemas com graceful shutdown, aplicações que precisam de injeção dinâmica, microsserviços complexos

5. Exemplo Completo: Servidor HTTP com DI

Implementação com Wire

// main.go
package main

import (
    "net/http"
    "log"
)

type App struct {
    Handler *UserHandler
}

func main() {
    app, err := InitializeApp()
    if err != nil {
        log.Fatal(err)
    }
    http.HandleFunc("/users", app.Handler.GetUsers)
    log.Fatal(http.ListenAndServe(":8080", nil))
}

// handler.go
type UserHandler struct {
    service *UserService
}

func NewUserHandler(service *UserService) *UserHandler {
    return &UserHandler{service: service}
}

func (h *UserHandler) GetUsers(w http.ResponseWriter, r *http.Request) {
    users, _ := h.service.FindAll()
    w.Write([]byte(users))
}

// service.go
type UserService struct {
    repo *UserRepository
}

func NewUserService(repo *UserRepository) *UserService {
    return &UserService{repo: repo}
}

func (s *UserService) FindAll() (string, error) {
    return s.repo.FindAll()
}

// repository.go
type UserRepository struct {
    db *sql.DB
}

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

func (r *UserRepository) FindAll() (string, error) {
    // lógica de banco
    return "users data", nil
}

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

package main

import "github.com/google/wire"

func InitializeApp() (*App, error) {
    wire.Build(
        NewDB,
        NewUserRepository,
        NewUserService,
        NewUserHandler,
        wire.Struct(new(App), "*"),
    )
    return nil, nil
}

Implementação equivalente com Fx

// main.go
package main

import (
    "context"
    "net/http"
    "go.uber.org/fx"
    "go.uber.org/fx/fxevent"
)

func main() {
    app := fx.New(
        fx.WithLogger(func() fxevent.Logger {
            return fxevent.NopLogger
        }),
        fx.Provide(
            NewDB,
            NewUserRepository,
            NewUserService,
            NewUserHandler,
            NewHTTPServer,
        ),
        fx.Invoke(func(srv *http.Server) {}),
    )
    app.Run()
}

// server.go
func NewHTTPServer(lc fx.Lifecycle, handler *UserHandler) *http.Server {
    mux := http.NewServeMux()
    mux.HandleFunc("/users", handler.GetUsers)
    srv := &http.Server{Addr: ":8080", Handler: mux}

    lc.Append(fx.Hook{
        OnStart: func(ctx context.Context) error {
            go srv.ListenAndServe()
            return nil
        },
        OnStop: func(ctx context.Context) error {
            return srv.Shutdown(ctx)
        },
    })
    return srv
}

Estrutura de pastas recomendada:

project/
├── cmd/
│   └── server/
│       ├── main.go (Wire: wire.go + wire_gen.go)
│       └── main.go (Fx: único arquivo)
├── internal/
│   ├── handler/
│   ├── service/
│   └── repository/
└── go.mod

6. Boas Práticas e Padrões Avançados

Organização de providers por camada:

// internal/wire/provider.go
package wire

import (
    "github.com/google/wire"
    "project/internal/handler"
    "project/internal/service"
    "project/internal/repository"
)

var HandlerSet = wire.NewSet(
    handler.NewUserHandler,
    handler.NewAdminHandler,
)

var ServiceSet = wire.NewSet(
    service.NewUserService,
    service.NewAdminService,
)

var RepositorySet = wire.NewSet(
    repository.NewUserRepository,
    repository.NewAdminRepository,
)

Uso de interfaces para testabilidade:

type UserRepository interface {
    FindAll() (string, error)
}

type UserService struct {
    repo UserRepository // depende da interface
}

// Teste com mock
type mockRepo struct{}
func (m *mockRepo) FindAll() (string, error) {
    return "mock data", nil
}

Evitando anti-patterns:

  • Dependências circulares: Use interfaces ou refatore para remover ciclos
  • Providers muito genéricos: Evite func() interface{}; prefira tipos concretos
  • Injeção de configuração global: Passe configs explicitamente via providers

7. Conclusão e Próximos Passos

Wire e Fx resolvem o mesmo problema fundamental — injeção de dependência em Go — mas com filosofias diferentes:

  • Wire é para quem valoriza previsibilidade, performance e código gerado legível. Ideal para aplicações onde cada nanossegundo importa e o lifecycle é simples.
  • Fx é para quem precisa de gerenciamento automático de ciclo de vida, injeção dinâmica e estrutura modular. Perfeito para servidores, workers e sistemas complexos.

Minha recomendação: comece com Wire se você está construindo uma CLI ou microsserviço simples. Escolha Fx se seu projeto envolve servidores HTTP, graceful shutdown ou múltiplos ciclos de vida. Ambos são ferramentas maduras e bem documentadas.

Para aprofundar, explore os repositórios oficiais e exemplos reais em código aberto. A escolha entre Wire e Fx não é binária — muitos projetos usam ambos em diferentes partes.

Referências