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