← Voltar para o blog

Google Wire vs Uber Fx em Go: DI com Código Gerado ou Runtime

Compare Google Wire e Uber Fx em Go: compile-time vs runtime, lifecycle, módulos, testes, grafo de dependências e quando ficar só com construtores manuais.

Para a maioria dos serviços Go em 2026, comece sem framework de DI. Construtores explícitos, interfaces pequenas e um main que monta o grafo continuam sendo o caminho idiomático — e o que descrevemos no guia de dependency injection em Go sem framework. Quando o grafo passa de dezenas de componentes, a pergunta muda: Google Wire (código gerado em compile-time) ou Uber Fx (container e lifecycle em runtime)?

A resposta curta: Wire se você quer manter DI “como código Go” e falhar no build; Fx se você precisa de módulos reutilizáveis, OnStart/OnStop e composição de aplicação em escala. Este artigo compara os dois com código, critérios de decisão, armadilhas e o momento certo de não adotar nenhum deles. Complementa Clean Architecture em Go, graceful shutdown, configuração com Viper, slog e mocks em Go.

Resposta rápida: Wire, Fx ou só construtores?

SituaçãoRecomendação
API/worker com poucos serviços e 1–2 repositóriosConstrutores manuais
Grafo grande, mas time quer zero runtime magicGoogle Wire
Muitos módulos de infra compartilhados entre serviçosUber Fx
Lifecycle complexo (HTTP + workers + filas + cron)Fx (ou main manual bem estruturado)
Biblioteca open source que outros vão importarSem Wire/Fx na API pública
Time júnior ou onboarding constantePrefira manual ou Wire (mais legível no debugger)
“Preciso de DI porque vi no Spring/Nest”Não adote ainda — modele interfaces primeiro

Regra prática: se o main ainda cabe em uma tela e cada teste monta fakes com três linhas, você não tem problema de DI — tem problema de escopo de feature. Ferramenta de DI não substitui interfaces pequenas nem testes de tabela.

O que cada ferramenta realmente resolve

As duas resolvem o mesmo incômodo: montar o grafo de dependências sem copiar e colar NewService(...) em cinco mains. A diferença está em quando e como o grafo é resolvido.

CritérioConstrutores manuaisGoogle WireUber Fx
Momento da montagemCompile-time (seu código)Compile-time (código gerado)Runtime
Falha de grafoErro de compilação / nilErro no wire / buildPanic ou erro no fx.New
Lifecycle (Start/Stop)Você implementaVocê implementaEmbutido (OnStart/OnStop)
Módulos reutilizáveisPacotes + funções New*Provider setsfx.Module / fx.Options
Reflexão em runtimeNãoNãoSim (parcial)
Curva de aprendizadoBaixaMédiaAlta
Debug no IDEExcelenteBom (código gerado legível)Mais indirection
AcoplamentoZero//go:build wireinjectImport de go.uber.org/fx no bootstrap
Ideal paraServiços pequenos/médiosGrafo grande e explícitoPlataformas multi-módulo

Nenhuma linha dessa tabela autoriza pular o desenho das dependências. Wire e Fx não inventam boas interfaces; eles apenas montam o que você já declarou.

Baseline: o grafo manual que todo time deveria dominar

Antes de Wire ou Fx, o padrão saudável parece com isto:

package main

import (
	"context"
	"log/slog"
	"net/http"
	"os"
	"os/signal"
	"syscall"
	"time"

	"example.com/app/internal/config"
	"example.com/app/internal/httpapi"
	"example.com/app/internal/pedido"
	"example.com/app/internal/postgres"
)

func main() {
	cfg, err := config.Load()
	if err != nil {
		slog.Error("config", "err", err)
		os.Exit(1)
	}

	logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo}))

	db, err := postgres.Open(cfg.DatabaseURL)
	if err != nil {
		logger.Error("db", "err", err)
		os.Exit(1)
	}
	defer db.Close()

	repo := postgres.NewPedidoRepository(db)
	svc := pedido.NewService(repo, logger)
	handler := httpapi.NewHandler(svc, logger)

	srv := &http.Server{
		Addr:              cfg.HTTPAddr,
		Handler:           handler.Routes(),
		ReadHeaderTimeout: 5 * time.Second,
	}

	ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
	defer stop()

	go func() {
		logger.Info("http listening", "addr", cfg.HTTPAddr)
		if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
			logger.Error("http", "err", err)
			stop()
		}
	}()

	<-ctx.Done()
	shutdownCtx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
	defer cancel()
	_ = srv.Shutdown(shutdownCtx)
}

Esse bootstrap é legível, testável e alinhado com graceful shutdown em produção. O problema aparece quando surgem cache Redis, cliente de antifraude, publisher de eventos, worker de outbox, health checks e três repositórios — e o main vira um script de 200 linhas com ordem de criação frágil.

Google Wire: DI em compile-time

Wire é um gerador de código. Você declara providers (funções que constroem dependências) e um injector (função com corpo vazio e a diretiva wire.Build). O comando wire gera o wire_gen.go com a montagem real.

Provider e injector

//go:build wireinject
// +build wireinject

package main

import (
	"github.com/google/wire"

	"example.com/app/internal/config"
	"example.com/app/internal/httpapi"
	"example.com/app/internal/pedido"
	"example.com/app/internal/postgres"
)

func initializeApp() (*App, error) {
	wire.Build(
		config.Load,
		postgres.Open,
		postgres.NewPedidoRepository,
		pedido.NewService,
		httpapi.NewHandler,
		NewApp,
	)
	return nil, nil
}

O gerador produz algo no espírito de:

// Code generated by Wire. DO NOT EDIT.
func initializeApp() (*App, error) {
	cfg, err := config.Load()
	if err != nil {
		return nil, err
	}
	db, err := postgres.Open(cfg.DatabaseURL)
	if err != nil {
		return nil, err
	}
	repo := postgres.NewPedidoRepository(db)
	logger := newLogger() // se estiver no provider set
	svc := pedido.NewService(repo, logger)
	handler := httpapi.NewHandler(svc, logger)
	app := NewApp(cfg, db, handler)
	return app, nil
}

Provider sets reutilizáveis

var InfraSet = wire.NewSet(
	config.Load,
	newLogger,
	postgres.Open,
)

var PedidoSet = wire.NewSet(
	postgres.NewPedidoRepository,
	pedido.NewService,
	httpapi.NewHandler,
)

Você compõe sets por domínio e mantém o injector enxuto. Erros de grafo — tipo faltando, ambiguidade de provider, ciclo — aparecem antes do deploy, no go generate ou no CI.

O que Wire não faz

  • Não gerencia Start/Stop automaticamente.
  • Não registra hooks de health check.
  • Não resolve “qual implementação de Repository usar” por nome mágico — você escolhe com wire.Bind ou providers específicos.
  • Não substitui feature flags nem configuração.

Wire brilha quando o time já escreve bons construtores e só quer eliminar o boilerplate do main sem pagar o custo de um container.

Fluxo de trabalho no dia a dia

go install github.com/google/wire/cmd/wire@latest
# no módulo
wire ./...
go test ./...

No CI, falhe se wire_gen.go estiver desatualizado (git diff --exit-code). Trate o arquivo gerado como artefato versionado — a maioria dos times brasileiros versiona wire_gen.go para builds reproduzíveis sem instalar o binário Wire em todo ambiente.

Uber Fx: aplicação como grafo com lifecycle

Fx é um framework de aplicação. Você registra construtores com fx.Provide, invoca funções de bootstrap com fx.Invoke e deixa o Fx:

  1. montar o grafo;
  2. chamar hooks OnStart na ordem certa;
  3. bloquear até sinal de shutdown;
  4. chamar OnStop na ordem inversa.
package main

import (
	"context"
	"net/http"

	"go.uber.org/fx"
	"go.uber.org/fx/fxevent"

	"example.com/app/internal/config"
	"example.com/app/internal/httpapi"
	"example.com/app/internal/pedido"
	"example.com/app/internal/postgres"
)

func main() {
	fx.New(
		fx.WithLogger(func() fxevent.Logger { return fxevent.NopLogger }),
		fx.Provide(
			config.Load,
			postgres.Open,
			postgres.NewPedidoRepository,
			pedido.NewService,
			httpapi.NewHandler,
			newHTTPServer,
		),
		fx.Invoke(registerHooks),
	).Run()
}

func newHTTPServer(h *httpapi.Handler, cfg config.Config) *http.Server {
	return &http.Server{
		Addr:    cfg.HTTPAddr,
		Handler: h.Routes(),
	}
}

func registerHooks(lc fx.Lifecycle, srv *http.Server) {
	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)
		},
	})
}

Módulos: o superpoder (e a armadilha)

var Module = fx.Module("pedido",
	fx.Provide(
		postgres.NewPedidoRepository,
		pedido.NewService,
		httpapi.NewHandler,
	),
)

Serviços da empresa passam a compartilhar fx.Module de logging, tracing, banco e mensageria. Isso acelera novos microsserviços — e também espalha o acoplamento a Fx por dezenas de pacotes se você não isolar o bootstrap.

Grupos e valores nomeados

Fx permite fx.Out, fx.In, tags e grupos para coletar várias implementações (vários http.Handler de rotas, vários workers). É poderoso em plataformas grandes; em serviços pequenos, vira indirection desnecessária.

O que Fx resolve melhor que Wire

  • Lifecycle unificado para HTTP, gRPC, consumers Kafka/SQS e tickers.
  • Ordem de start/stop sem você manter uma lista manual.
  • Módulos para times de plataforma entregarem “plugue este pacote e ganhe métricas + health”.
  • Integração natural com padrões de worker pool, health checks e OpenTelemetry.

Comparativo prático: a mesma decisão em três estilos

1) Trocar implementação em teste

Manual / Wire: no teste, chame pedido.NewService(fakeRepo, logger) direto. Não suba o injector de produção.

Fx: use fx.Replace, fx.Decorate ou um fx.Options de teste com fakes. Funciona, mas o teste fica mais “framework-aware”.

2) Falha de dependência faltando

Manual: o compilador reclama do argumento.

Wire: wire falha com mensagem de provider ausente.

Fx: fx.New falha em runtime na subida do processo (ainda cedo, mas depois do build).

3) Graceful shutdown

Manual / Wire: você escreve o loop de sinal — veja o guia de graceful shutdown.

Fx: App.Run() + hooks OnStop já modelam o ciclo. Ainda assim, timeouts de shutdown e drenagem de filas são responsabilidade sua.

4) Biblioteca reutilizável

Se o pacote é importado por outros times, não force Wire nem Fx na API pública. Exporte NewService(deps) e deixe o consumidor montar como quiser. O mesmo princípio vale para middleware HTTP e clientes de API REST.

Critérios de decisão para times brasileiros

Use esta rubrica em review de arquitetura:

  1. Tamanho do grafo. Menos de ~15 construtores no bootstrap? Manual. Entre 15 e 40 com tipos estáveis? Wire. Dezenas de módulos e vários binários? Avalie Fx.
  2. Número de processos no mesmo binário. Só HTTP? Manual/Wire bastam. HTTP + consumers + cron no mesmo processo? Fx reduz erro de ordem de start.
  3. Maturidade do time. Wire se lê como Go gerado. Fx exige disciplina de módulos e cuidado para não esconder dependências.
  4. Plataforma interna. Se já existe um “kit” Fx da empresa, remar contra a maré custa mais do que aprender o framework.
  5. Observabilidade e ops. Lifecycle central ajuda a padronizar health, métricas e shutdown — desde que o time não use isso para empurrar lógica de negócio para hooks.
  6. Testabilidade. O domínio continua recebendo interfaces; o framework fica na borda. Se o caso de uso importa Fx, o desenho errou.

Armadilhas comuns (e como evitar)

“Container para tudo”

Injetar *sql.DB, *redis.Client, *http.Client e *slog.Logger em todo handler cria um god-object distribuído. Prefira interfaces no domínio (PedidoRepository, Clock, Publisher) como no guia de DI sem framework.

Esconder configuração no grafo

Configuração deve ser validada cedo — idealmente com o padrão Twelve-Factor + Viper/env. Não deixe um provider ler os.Getenv no meio do grafo sem falhar rápido.

Wire com providers ambíguos

Dois NewRepository que devolvem a mesma interface quebram o grafo. Use wire.Bind explícito ou tipos distintos por bounded context.

Fx Invoke com side effects opacos

fx.Invoke que “só registra rotas” e também abre conexão global vira caça ao tesouro. Hooks de lifecycle devem ser óbvios: abrir listener, iniciar consumer, fechar pool.

Gerar código e não rodar no CI

wire_gen.go desatualizado é bug de produção esperando o merge. Trate wire ./... como passo de lint, no mesmo espírito de staticcheck/golangci-lint.

Misturar DI framework com service locator

Passar o fx.App ou um container.Get("repo") para o domínio anula o benefício. Dependências entram pelo construtor — sempre.

Como migrar sem reescrever o monólito

Do manual para Wire

  1. Extraia cada New* para assinaturas puras (sem globals).
  2. Crie provider sets por pacote.
  3. Substitua o main por um injector; versionar wire_gen.go.
  4. Mantenha testes unitários chamando construtores diretamente — não o injector.

Do manual para Fx

  1. Isole o bootstrap em internal/app ou cmd/api.
  2. Mova Provide sem mudar o domínio.
  3. Introduza Lifecycle só onde há start/stop real (HTTP, consumers).
  4. Só depois quebre em fx.Module por bounded context.

De Fx para Wire (ou vice-versa)

Raro e caro. Só faça se o framework virou atrito mensurável (builds lentos, onboarding, incidentes de ordem de start). Migre um binário por vez; não “padronize a empresa” em um PR heróico.

O que isso tem a ver com vagas e entrevistas Go

Descrições de vagas backend plenas/sênior no Brasil pedem cada vez mais “experiência com DI”, “modularização” e “produção”. Em entrevista, o sinal de maturidade não é citar Wire ou Fx de cor — é explicar quando o main manual deixa de escalar, como testar com fakes e como o lifecycle evita vazamento de conexões no deploy. Combine isso com repertório de context e timeout, rate limiting e observabilidade.

Se você está montando portfólio, um serviço pequeno com construtores manuais bem testados vale mais do que um CRUD com Fx copiado de template. Veja ideias em projetos Go para portfólio e o roadmap Go 2026.

Checklist antes de adotar Wire ou Fx

  • Interfaces do domínio são pequenas e estão perto de quem usa?
  • O main atual é realmente o gargalo (não a modelagem)?
  • Configuração falha cedo e de forma explícita?
  • Testes unitários não precisam do container?
  • CI valida código gerado (Wire) ou sobe o app com smoke test (Fx)?
  • Shutdown drena requests e workers com timeout?
  • Logs estruturados e correlação existem independentemente do DI?
  • A escolha está documentada no README do serviço (uma página, sem dogma)?

Se a maioria das respostas for “não”, volte ao guia sem framework e ao clean architecture sem overengineering.

Perguntas frequentes

Qual a diferença entre Google Wire e Uber Fx?

Wire gera código Go em tempo de compilação e monta o grafo com funções puras, sem container em runtime. Fx monta o grafo em runtime, com módulos, lifecycle (OnStart/OnStop), grupos de dependências e hooks de inicialização. Wire é mais próximo de construtores manuais; Fx é um framework de aplicação.

Quando usar Wire em vez de Fx?

Use Wire quando o grafo cresceu demais para o main manual, mas você quer manter DI explícita, zero magia em runtime e falhas detectadas no build. É a evolução natural do padrão de construtores.

Quando Uber Fx vale a pena?

Fx vale a pena em serviços grandes com muitos módulos, inicialização ordenada, graceful shutdown centralizado e times que compartilham pacotes de infraestrutura (logger, métricas, banco, filas). O custo é acoplamento ao framework e uma curva de aprendizado maior.

Preciso de Wire ou Fx para fazer DI em Go?

Não. A forma idiomática começa com structs, interfaces pequenas e construtores no main. Ferramentas de DI só entram quando a montagem manual virou fonte real de erro ou o lifecycle da aplicação ficou complexo demais para um bootstrap legível.

Wire e Fx podem coexistir no mesmo projeto?

Tecnicamente sim, mas raramente vale a pena. Escolha um modelo de montagem por serviço. Misturar gera dois estilos de bootstrap, dois jeitos de testar e onboarding mais caro. Prefira um padrão único por repositório.

Conclusão

Google Wire e Uber Fx resolvem dores reais — mas só depois que o time já domina DI idiomática com construtores. Wire é a ponte conservadora: menos boilerplate, mesmo modelo mental, erros no build. Fx é a aposta de plataforma: módulos, lifecycle e composição em escala, com mais indirection. Na dúvida, mantenha o main explícito, invista em interfaces testáveis e só introduza ferramenta quando o grafo ou o ciclo de vida cobrarem o preço.

Continue com dependency injection sem framework, Clean Architecture em Go, mocks e fakes, graceful shutdown e as vagas Go no Brasil. Se ainda está estruturando a base da linguagem, o caminho como aprender Go e o curso gratuito fecham o funil de aprendizado.