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 cache == nil {
panic("cache 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) {
// Atalho pelo cache: evita ir ao banco para um e-mail recém-cadastrado.
// A garantia de unicidade é a restrição UNIQUE no banco, e não o cache
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)
}
// Registra no cache e envia o e-mail — falhas aqui não propagam
s.cache.Set(ctx, "email:"+input.Email, "1", 24*time.Hour)
if s.mailer != nil {
// O ctx da requisição é cancelado quando o handler responde, e a
// goroutine ainda estaria enviando: WithoutCancel mantém os valores
// do contexto sem o cancelamento, e o WithTimeout dá um limite próprio
ctxEmail, cancelar := context.WithTimeout(context.WithoutCancel(ctx), 10*time.Second)
go func() {
defer cancelar()
if err := s.mailer.EnviarBoasVindas(ctxEmail, 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 (
"context"
"errors"
"fmt"
"log/slog"
"net/http"
"time"
)
type TLSConfig struct {
CertFile string
KeyFile string
}
type Servidor struct {
handler http.Handler
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{
handler: handler,
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
}
// Iniciar sobe o servidor e o encerra com Shutdown quando ctx é cancelado.
func (s *Servidor) Iniciar(ctx context.Context) error {
// O primeiro middleware registrado fica por fora, e roda primeiro
h := s.handler
for i := len(s.middlewares) - 1; i >= 0; i-- {
h = s.middlewares[i](h)
}
srv := &http.Server{
Addr: fmt.Sprintf("%s:%d", s.host, s.porta),
Handler: h,
ReadTimeout: s.readTimeout,
WriteTimeout: s.writeTimeout,
}
erros := make(chan error, 1)
go func() {
s.logger.Info("servidor iniciado", "addr", srv.Addr)
if s.tls != nil {
erros <- srv.ListenAndServeTLS(s.tls.CertFile, s.tls.KeyFile)
} else {
erros <- srv.ListenAndServe()
}
}()
select {
case err := <-erros:
return err
case <-ctx.Done():
desligar, cancelar := context.WithTimeout(context.Background(), 30*time.Second)
defer cancelar()
if err := srv.Shutdown(desligar); err != nil && !errors.Is(err, http.ErrServerClosed) {
return err
}
return nil
}
}
// // 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"
domainservico "loja/internal/domain/servico"
"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 {
enviados chan string // o serviço envia de outra goroutine: o canal sincroniza
erroEnviar error
}
func (m *mockMailer) EnviarBoasVindas(_ context.Context, email, _ string) error {
if m.enviados != nil {
m.enviados <- 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{enviados: make(chan string, 1)}
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)
}
// Espera o e-mail da goroutine; um time.Sleep aqui seria uma corrida
// de dados, acusada pelo go test -race
select {
case email := <-mailer.enviados:
if email != "ricardo@exemplo.com" {
t.Errorf("e-mail enviado para %s", email)
}
case <-time.After(time.Second):
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/config"
"loja/internal/handler"
"loja/internal/infra/cache/redis"
"loja/internal/infra/email/sendgrid"
"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. Um aviso antes de adotar: o repositório google/wire foi arquivado pelo Google em agosto de 2025. A ferramenta continua instalável e o código que ela gera continua sendo Go comum, mas não há mais correções nem versões novas; para um projeto novo, isso pesa a favor da composição manual.
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 | Na compilação (tipos); nil só ao rodar | Na geração do código |
| 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.
Injeção de dependência em Go dispensa framework: cada componente recebe o que precisa pelo construtor, as dependências ficam visíveis na assinatura, e o main é o único lugar que monta o grafo. Opções funcionais resolvem construtores com muitos parâmetros opcionais, e mocks escritos à mão bastam para testar. A explicitude, porém, só protege o que o compilador enxerga. Um parâmetro recebido e esquecido, uma dependência nil aceita pelo construtor e um contexto de requisição levado para uma goroutine que vive mais que ela são defeitos que compilam. Validar as dependências obrigatórias no construtor e rodar os testes com -race pegam boa parte deles.
Fontes e leituras recomendadas
-
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
Exercícios
Exercício 1
Na primeira versão do ServicoUsuario.Criar, a goroutine do e-mail de boas-vindas recebia o mesmo ctx da requisição. Em produção, quase todos os envios falham com context canceled, mas o cadastro funciona. Por quê?
Ver resposta
✓ Resposta: O net/http cancela o contexto de uma requisição assim que o handler retorna. O Criar dispara a goroutine e volta logo, o handler responde ao cliente, e o contexto é cancelado enquanto o e-mail ainda está sendo enviado. Qualquer chamada de rede que respeite o contexto falha nesse ponto. Num teste que cancelava o contexto logo depois de Criar retornar, como faz o servidor HTTP, o mailer recebeu context canceled. A versão do artigo usa context.WithoutCancel(ctx), do Go 1.21, que preserva os valores do contexto, como um ID de requisição para os logs, mas não herda o cancelamento; e acrescenta um WithTimeout próprio, para que o envio não fique preso para sempre. Com ela, o mesmo teste entregou um contexto válido ao mailer.
Exercício 2
O teste TestCriarUsuario_Sucesso original esperava o e-mail com time.Sleep(10 * time.Millisecond) e depois conferia o slice do mock. Passava em todas as execuções. O que acontece com go test -race, e por que o sleep não resolve?
Ver resposta
✓ Resposta: A goroutine do serviço faz append no slice do mock, e o teste lê o mesmo slice. Para o modelo de memória do Go, só uma operação de sincronização, como um canal, um mutex ou um WaitGroup, garante que a escrita de uma goroutine seja vista pela outra; esperar um tempo não estabelece essa ordem, apenas a torna provável. Num teste, o go test -race acusou DATA RACE e marcou o teste como falho, mesmo com o resultado certo. Numa máquina de CI sobrecarregada, os 10 ms também podem não bastar, e o teste falha de forma intermitente. A versão do artigo troca o slice por um canal: o mock envia o e-mail recebido, e o teste espera com select e um time.After de um segundo como limite. Com ela, o teste passou com -race.
Exercício 3
Na primeira versão do pacote servidor, NovoServidor(handler, opts...) compilava, mas a struct não tinha campo para o handler. O que esse erro mostra sobre os limites da DI "explícita", e como ele passaria despercebido?
Ver resposta
✓ Resposta: O construtor recebia o handler e simplesmente não o guardava. O Go acusa variáveis locais não usadas, mas não parâmetros, porque um parâmetro sem uso é comum, por exemplo para satisfazer uma interface; nem o go vet reclama. Como o pacote também não tinha o método que sobe o servidor, ninguém chegava a perceber que não havia o que servir. A versão do artigo guarda o handler, aplica os middlewares e sobe o servidor em Iniciar; num teste com dois middlewares, a requisição passou por eles na ordem em que foram registrados e chegou ao handler, e o servidor encerrou sem erro quando o contexto foi cancelado. A DI explícita deixa as dependências visíveis na assinatura, mas não garante que sejam usadas: só um teste que atravessa o componente, como esse, comprova a ligação. O linter unparam, do golangci-lint, aponta parâmetros que nunca são usados.
Exercício 4
O construtor original de ServicoUsuario recusava repo nil com um panic, mas aceitava cache nil. Um ambiente de testes sem Redis passa nil no cache. Quando o problema aparece, e por que vale falhar no construtor?
Ver resposta
✓ Resposta: O serviço chama s.cache.Get logo no começo de Criar. Com o cache nil, a primeira chamada a um método da interface causa um panic de ponteiro nulo: num teste, foi runtime error: invalid memory address or nil pointer dereference, durante o primeiro cadastro, e não na subida da aplicação. Num servidor, esse panic aparece numa requisição de usuário, talvez horas depois do deploy. A versão do artigo trata o cache como obrigatório e recusa o nil no construtor, como já fazia com o repositório: no mesmo teste, a falha passou a acontecer na montagem, com a mensagem cache não pode ser nil. Quando uma dependência é de fato opcional, como o mailer, o código precisa tratar a ausência explicitamente, como faz o if s.mailer != nil. Outra opção é um objeto nulo, um cache que nunca encontra nada, para que o resto do código não precise saber da diferença.
Exercício 5
O ServicoUsuario considera um e-mail duplicado quando encontra a chave no cache. Um usuário se cadastra, o cache expira 24 horas depois, e o mesmo e-mail é cadastrado de novo. Outro cenário: duas instâncias do serviço, cada uma com cache local. O que garante de fato a unicidade?
Ver resposta
✓ Resposta: Um cache é uma cópia que pode estar ausente, expirada ou ser diferente em cada instância, e por isso serve como atalho, nunca como fonte da verdade. Nos dois cenários, a consulta ao cache não encontra a chave, e o serviço segue para o repo.Criar. Quem tem a palavra final é o banco: uma restrição UNIQUE na coluna de e-mail faz o segundo INSERT falhar, mesmo que duas requisições cheguem ao mesmo tempo. O repositório traduz essa violação, o código 23505 no PostgreSQL, por exemplo, para ErrEmailJaUsado, e o caso de uso responde do mesmo jeito que respondia pelo cache. A consulta com BuscarPorEmail antes de criar melhora a mensagem para o caso comum, mas tem a mesma corrida entre consultar e inserir; a restrição no banco é o que fecha essa janela.