Injeção de dependência em Go
Publicada em · 20 min de leitura
Em projetos de software que crescem além de alguns arquivos, a maior força destruidora da velocidade de entrega é a entropia do acoplamento. Quando uma struct instancia diretamente suas próprias dependências, criando conexões físicas com o PostgreSQL, abrindo sockets com servidores de e-mail ou lendo variáveis de ambiente no meio de uma regra de negócio, o sistema perde gradualmente a capacidade de ser testado de forma isolada, modificado sem efeitos colaterais e compreendido por novos engenheiros.
A em Go não exige contêineres mágicos, reflexão pesada nem anotações herdadas de ecossistemas corporativos como Java ou C#. Em Go, ela é simplesmente uma técnica direta de projeto: uma struct recebe tudo o que precisa para funcionar externamente, preferencialmente por meio de seu construtor e parametrizada por interfaces pequenas e implícitas. Esta aula ensina como estruturar sistemas desacoplados, a distinção formal entre injeção e inversão de dependência, a organização da no main.go e a comparação pragmática entre abordagens manuais e ferramentas automáticas como Wire e Fx.
| Parte | Seções | O que você vai aprender |
|---|---|---|
| 1. Fundamentos e princípios | 1 a 3 | O problema do acoplamento rígido, injeção de dependência versus inversão () |
| 2. O modelo idiomático de interfaces | 4 a 6 | Interfaces implícitas, e a regra de ouro do consumidor |
| 3. Padrões de instanciação | 7 a 9 | Construtores canônicos (NewService) e o padrão para opcionais |
| 4. A raiz de composição | 10 a 12 | O papel do main.go, grafo topológico de inicialização e encerramento gracioso |
| 5. Testabilidade e dublês | 13 a 15 | Testes unitários puros, criação de stubs manuais e mocks com testify |
| 6. Ecossistema e automação | 16 a 18 | Injeção manual versus geração estática (google/wire) versus reflexão (uber-go/fx) |
| 7. Antipadrões e fechamento | 19 a 22 | , singletons globais, exercícios com gabarito e glossário técnico |
Pré-requisitos recomendados: Go, Structs e Ponteiros.
Parte 1: Fundamentos e princípios
1. O problema do acoplamento rígido no código
Imagine um eletrodoméstico cujo cabo de energia fosse soldado diretamente nos fios de cobre que passam por dentro da parede da sua casa. O aparelho funcionaria, mas se você precisasse trocar a voltagem, pintar o quarto, levar o aparelho para a assistência técnica ou testá-lo em uma bancada de ferramentas, teria de quebrar a alvenaria e cortar fios com um alicate. Uma tomada fêmea padrão na parede e um plugue macho no cabo resolvem o problema porque introduzem uma interface desacoplada.
No desenvolvimento com Go, o erro análogo ocorre quando um handler HTTP instancia um driver de banco de dados diretamente dentro do seu corpo:
// Exemplo frágil: acoplamento rígido ao banco de dados físico
type OrderHandler struct {
// campo sem dependência explícita
}
func (h *OrderHandler) Handle(w http.ResponseWriter, r *http.Request) {
// Instanciação física dentro do método: impossível de testar sem um Postgres real rodando
db, err := sql.Open("postgres", "postgres://localhost:5432/orders")
if err != nil {
http.Error(w, "erro interno", http.StatusInternalServerError)
return
}
defer db.Close()
// executa consulta...
}
Esse trecho possui problemas graves: ele abre pools de conexão repetidas vezes por requisição, ignora o ciclo de vida da infraestrutura e torna qualquer teste automatizado refém de uma base de dados real acessível na rede local, tornando a suíte de testes lenta e instável.
A figura ilustra as duas abordagens. No bloco superior (acoplamento rígido), a struct OrderHandler decide concretamente qual banco usar e cria a conexão por conta própria. No bloco inferior (injeção por construtor), a entidade externa main() instancia o driver físico de banco de dados e o injeta no OrderHandler através do contrato abstrato definido pela interface OrderRepository. O handler desconhece se o dado vem de um PostgreSQL real, de um SQLite em memória ou de um mock de testes.
2. Injeção de dependência versus princípio da inversão de dependência
Existe uma confusão comum entre dois conceitos relacionados, mas situados em níveis de abstração distintos:
-
Injeção de Dependências (Dependency Injection - DI): É um padrão mecânico e operacional. Trata-se do ato de fornecer dependências prontas a um componente de fora para dentro (via parâmetros de função, construtor ou campos), em vez de permitir que o próprio componente as construa internamente. Você pode praticar injeção de dependências passando uma struct concreta sem usar nenhuma interface.
-
Princípio da Inversão de Dependência (Dependency Inversion Principle - DIP): É o quinto princípio do acrônimo SOLID, cunhado por Robert C. Martin. Ele determina duas regras arquiteturais:
- Módulos de alto nível não devem depender de módulos de baixo nível; ambos devem depender de abstrações.
- Abstrações não devem depender de detalhes de implementação; detalhes de implementação devem depender de abstrações.
Quando você une a inversão arquitetural (interfaces) com o mecanismo de entrega (injeção), atinge o desacoplamento ideal: a camada de regras de negócio dita o que precisa para funcionar e as camadas externas de infraestrutura atendem a essa demanda sem que a regra conheça os detalhes técnicos do armazenamento ou do transporte.
3. Os tipos de injeção e por que Go prefere construtores
Na literatura de engenharia de software existem três formas clássicas de injetar dependências:
- Injeção por construtor (Constructor Injection): As dependências obrigatórias são passadas como argumentos para a função fábrica que aloca e inicializa a struct (em Go, convencionalmente chamada de
New...). - Injeção por método ou método setter (Method/Setter Injection): A struct é criada vazia e as dependências são atribuídas chamando métodos subsequentes (ex:
svc.SetRepository(repo)). - Injeção por campo direto (Field Injection): Os campos exportados da struct são preenchidos diretamente por atribuição manual ou via pacotes de reflexão.
Em Go, a injeção por construtor é amplamente soberana e preferida pela comunidade técnica. O motivo é a garantia de integridade: ao exigir todas as dependências no momento da criação, torna-se impossível instanciar um serviço em estado incompleto ou inválido que resulte em pânico por ponteiro nulo (nil pointer dereference) ao chamar métodos operacionais em tempo de execução.
Parte 2: O modelo idiomático de interfaces
4. Interfaces implícitas e duck typing estrutural
Em muitas linguagens orientadas a objetos tradicionais, uma classe precisa declarar explicitamente que implementa um contrato através de palavras-chave dedicadas (como class PostgresRepo implements OrderRepository). Isso cria um acoplamento reverso incômodo: o desenvolvedor da infraestrutura precisa conhecer e importar o pacote da interface.
Go adota o sistema de tipagem estrutural, informalmente chamado de duck typing estático: se uma struct possui métodos cujos nomes, parâmetros e retornos coincidem exatamente com a assinatura da interface, o compilador considera que ela satisfaz a interface automaticamente. Nenhuma declaração explícita ou importação reversa é necessária.
package domain
import "context"
// A interface é definida apenas com o método estritamente necessário
type OrderSaver interface {
SaveOrder(ctx context.Context, id string, total float64) error
}
type OrderService struct {
repo OrderSaver
}
func NewOrderService(repo OrderSaver) *OrderService {
return &OrderService{repo: repo}
}
Se em outro pacote qualquer existir a struct a seguir:
package postgres
import (
"context"
"database/sql"
)
type Repository struct {
db *sql.DB
}
// Satisfaz implicitamente a interface OrderSaver sem importar o pacote domain
func (r *Repository) SaveOrder(ctx context.Context, id string, total float64) error {
_, err := r.db.ExecContext(ctx, "INSERT INTO orders (id, total) VALUES ($1, $2)", id, total)
return err
}
O compilador aceita passar &postgres.Repository{} para NewOrderService sem qualquer atrito ou amarração entre os pacotes.
5. A regra de ouro: interfaces pertencem ao consumidor
O erro mais comum cometido por desenvolvedores que migram de outras linguagens para o Go é criar interfaces gigantescas no mesmo pacote onde a implementação reside (por exemplo, criar uma interface UserRepository com 30 métodos dentro do pacote infra/database).
Em Go, a regra de ouro do design idiomático diz:
Defina interfaces no pacote que as consome, não no pacote que as implementa.
O diagrama ilustra esse fluxo. O pacote de caso de uso (CreateOrderUseCase) define localmente uma interface mínima contendo apenas o método Save (OrderSaver). O repositório PostgreSQL pode implementar quarenta métodos diferentes para auditoria, relatórios e exclusões em lote; o caso de uso não precisa saber disso. Ele enxerga apenas o recorte exato do comportamento de que necessita.
Essa abordagem traz vantagens fundamentais:
- Reduz drasticamente a superfície de contato entre módulos.
- Facilita os testes de unidade: para testar o caso de uso, o precisa simular apenas um único método, e não dezenas de operações irrelevantes.
- Evita ciclos de dependência (
import cycle not allowed), um erro comum que o compilador do Go rejeita imediatamente.
6. Granularidade: pequenas, focadas e composíveis
A biblioteca padrão do Go é a maior referência mundial de design de interfaces. O pacote io, por exemplo, define abstrações universais com um único método:
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
Seguindo esse modelo em seus sistemas de negócio, dê preferência a interfaces de um ou dois métodos. Se um serviço necessita de operações de leitura e gravação, componha-as a partir de interfaces menores em vez de criar um monolito abstrato:
type Reader interface {
FindByID(ctx context.Context, id string) (*Order, error)
}
type Writer interface {
Save(ctx context.Context, order *Order) error
}
// Composição limpa de contratos
type ReadWriter interface {
Reader
Writer
}
Parte 3: Padrões de instanciação
7. Construtores idiomáticos com New
Como Go não possui a palavra-chave constructor, a convenção universal é definir uma função exportada cujo nome se inicia com New (ou NewNomeDaStruct quando há múltiplas no mesmo pacote). Essa função atua como porta de entrada obrigatória, garantindo validações de integridade no momento da alocação.
package service
import (
"context"
"errors"
"log/slog"
)
type AccountRepository interface {
GetBalance(ctx context.Context, accountID string) (float64, error)
}
type PaymentService struct {
repo AccountRepository
logger *slog.Logger
}
// Construtor canônico: validação de ponteiros nulos antes do retorno
func NewPaymentService(repo AccountRepository, logger *slog.Logger) (*PaymentService, error) {
if repo == nil {
return nil, errors.New("AccountRepository é obrigatório")
}
if logger == nil {
return nil, errors.New("slog.Logger é obrigatório")
}
return &PaymentService{
repo: repo,
logger: logger,
}, nil
}
A checagem explícita de nil impede que o sistema inicie com dependências esquecidas, transformando potenciais pânicos em falhas imediatas de compilação ou de inicialização (fail fast).
8. O padrão Functional Options para dependências opcionais
Quando uma struct possui muitas dependências secundárias (como timeouts customizados, métricas, caches locais ou opções de telemetria), passar dez argumentos para a função construtora polui a assinatura e quebra o código cliente sempre que uma nova opção for adicionada.
O padrão Functional Options, popularizado por Dave Cheney e Rob Pike, resolve essa situação de maneira expansível e limpa:
package service
import "time"
type Client struct {
endpoint string
timeout time.Duration
retries int
}
// Tipo de função que aplica uma mutação controlada na struct
type Option func(*Client)
func WithTimeout(d time.Duration) Option {
return func(c *Client) {
if d > 0 {
c.timeout = d
}
}
}
func WithRetries(retries int) Option {
return func(c *Client) {
if retries >= 0 {
c.retries = retries
}
}
}
// Construtor flexível com valores padrão seguros
func NewClient(endpoint string, opts ...Option) *Client {
c := &Client{
endpoint: endpoint,
timeout: 5 * time.Second, // valor padrão razoável
retries: 3, // valor padrão razoável
}
// Aplica todas as opções fornecidas pelo usuário
for _, opt := range opts {
opt(c)
}
return c
}
O consumidor pode agora instanciar o cliente informando apenas o endpoint obrigatório ou customizar detalhes com clareza:
// Uso básico
c1 := NewClient("https://api.pagamentos.com")
// Uso avançado com parâmetros nomeados e limpos
c2 := NewClient(
"https://api.pagamentos.com",
WithTimeout(10*time.Second),
WithRetries(5),
)
9. Imutabilidade e ponteiros de structs
Na injeção de dependências em Go, uma decisão crucial diz respeito ao uso de ponteiros versus valores. Por convenção estabelecida no ecossistema:
- Dependências de serviço e infraestrutura: Devem ser injetadas sempre por ponteiro (ex:
*sql.DB,*slog.Logger,*OrderService). Serviços mantêm estado de conexão, pools de concorrência ou travas internas que não podem ser copiadas na memória. - Entidades de domínio e valores puros: Se uma dependência representa apenas dados estáticos ou parâmetros de configuração (como limites numéricos ou flags), ela pode ser repassada por valor para reforçar a imutabilidade.
Parte 4: A raiz de composição
5. O papel centralizador do main.go
A Raiz de Composição (Composition Root) é o único local do aplicativo onde o grafo de dependências é completamente conhecido e amarrado. Em aplicações Go bem desenhadas, esse ponto é a função main() (ou uma função de bootstrap chamada diretamente por ela).
Nenhum use case, handler ou repositório deve instanciar seus pares ou conhecer o contêiner geral do sistema. O main.go lê configurações, abre pools de banco de dados, instancia adaptadores, conecta regras de negócio e liga o servidor de transporte (HTTP ou gRPC).
O diagrama exibe a ordem topológica rigorosa exigida pelo compilador. A montagem segue uma escada de cinco camadas da esquerda para a direita:
- Camada 1 (Configuração e Drivers): Leitura de variáveis de ambiente e abertura dos pools físicos de conexão (
*sql.DB, Redis). - Camada 2 (Adaptadores de Infraestrutura): Conexão dos drivers nas structs de persistência (
OrderPostgresRepo). - Camada 3 (Casos de Uso e Domínio): Injeção dos repositórios nas regras de negócio puras (
CreateOrderUseCase). - Camada 4 (Apresentação e Roteamento): Injeção dos casos de uso nos handlers web e registro nos roteadores HTTP.
- Camada 5 (Servidor e Ciclo de Vida): Inicialização do
http.Servere orquestração do encerramento gracioso.
11. Montagem manual passo a passo
Veja a implementação real dessa cadeia de montagem sem o uso de qualquer biblioteca externa:
package main
import (
"context"
"database/sql"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
_ "github.com/lib/pq"
)
func main() {
// 1. Configurações e infraestrutura básica
connStr := os.Getenv("DATABASE_URL")
if connStr == "" {
connStr = "postgres://user:pass@localhost:5432/app?sslmode=disable"
}
db, err := sql.Open("postgres", connStr)
if err != nil {
log.Fatalf("falha ao abrir conexão com banco: %v", err)
}
defer db.Close()
db.SetMaxOpenConns(25)
db.SetMaxIdleConns(5)
db.SetConnMaxLifetime(5 * time.Minute)
// 2. Adaptadores de repositório
orderRepo := postgres.NewOrderRepository(db)
// 3. Regras de negócio / Use Cases
createOrderUC := usecase.NewCreateOrderUseCase(orderRepo)
// 4. Handlers e roteador
orderHandler := handler.NewOrderHandler(createOrderUC)
mux := http.NewServeMux()
mux.HandleFunc("/orders", orderHandler.Create)
// 5. Servidor HTTP
server := &http.Server{
Addr: ":8080",
Handler: mux,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
}
// Gerenciamento de ciclo de vida com encerramento gracioso
go func() {
log.Println("servidor escutando na porta :8080")
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("falha no servidor HTTP: %v", err)
}
}()
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Println("sinal de interrupção recebido, encerrando servidor com graça...")
ctxShutdown, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := server.Shutdown(ctxShutdown); err != nil {
log.Fatalf("falha ao forçar encerramento: %v", err)
}
log.Println("servidor finalizado com sucesso")
}
A maior vantagem dessa montagem manual é a transparência: qualquer desenvolvedor consegue rastrear exatamente de onde vem cada dependência usando apenas a ferramenta de busca de definições do seu editor, sem precisar adivinhar regras mágicas de frameworks.
Parte 5: Testabilidade e dublês
12. Por que injeção torna testes triviais
O benefício prático mais imediato de adotar injeção de dependências baseada em interfaces é o ganho na velocidade e confiabilidade da suíte de testes de unidade. Quando um caso de uso depende apenas de uma interface abstrata, você pode executá-lo contra estruturas em memória que respondem em microssegundos, sem depender de banco de dados, rede física ou sistemas de terceiros.
A figura ilustra o isolamento obtido em um comando go test. A regra de negócio sob teste (OrderService) recebe stubs em memória que simulam persistência e envio de mensagens. Os componentes de produção reais (banco de dados físico PostgreSQL, servidor SMTP e gateways de cartões) ficam totalmente desconectados e desligados durante a execução da suíte de testes unitários.
13. Criando dublês manuais (stubs em memória)
Ao contrário de ecossistemas que dependem de bibliotecas complexas de mock por padrão, em Go é frequentemente mais simples e legível escrever pequenas structs falsas diretamente dentro do arquivo _test.go:
package usecase_test
import (
"context"
"errors"
"testing"
)
// Definição da interface consumida pela regra
type OrderRepository interface {
Save(ctx context.Context, id string, amount float64) error
}
// Dublê de teste manual (stub)
type mockRepo struct {
saveFunc func(ctx context.Context, id string, amount float64) error
}
func (m *mockRepo) Save(ctx context.Context, id string, amount float64) error {
if m.saveFunc != nil {
return m.saveFunc(ctx, id, amount)
}
return nil
}
func TestProcessOrder_FalhaAoSalvar(t *testing.T) {
// Configura o comportamento esperado do dublê para simular falha
stub := &mockRepo{
saveFunc: func(ctx context.Context, id string, amount float64) error {
return errors.New("disco cheio")
},
}
// Injeta o dublê no caso de uso
uc := NewProcessOrderUseCase(stub)
err := uc.Execute(context.Background(), "ord-123", 150.00)
if err == nil {
t.Fatal("era esperado erro de persistência, mas retornou nil")
}
if err.Error() != "falha na gravação: disco cheio" {
t.Errorf("mensagem de erro inesperada: %v", err)
}
}
Essa abordagem é estável, não sofre com alterações de versões de ferramentas de terceiros e permite controlar o retorno exato de cada chamada em testes paralelos com facilidade.
14. Geração de mocks com testify e mockgen
Em sistemas corporativos com centenas de interfaces e dezenas de cenários de teste, escrever stubs manuais repetitivamente pode gerar código redundante. Para esses cenários, existem bibliotecas consagradas no ecossistema Go:
testify/mock: Permite registrar expectativas e validar quantidade de chamadas diretamente no código de teste.mockgen(GoMock): Analisa interfaces existentes e gera arquivos Go com implementações de mock completas via linha de comando (mockgen -source=order.go -destination=mock_order.go).
Exemplo prático utilizando o pacote de asserções do testify:
package service_test
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
type MockNotifier struct {
mock.Mock
}
func (m *MockNotifier) Notify(ctx context.Context, email string, msg string) error {
args := m.Called(ctx, email, msg)
return args.Error(0)
}
func TestEnvioNotificacao(t *testing.T) {
mockNotif := new(MockNotifier)
ctx := context.Background()
// Define a expectativa exata de chamada
mockNotif.On("Notify", ctx, "cliente@exemplo.com", "Pedido Aprovado").Return(nil).Once()
svc := NewOrderService(mockNotif)
err := svc.Approve(ctx, "cliente@exemplo.com")
assert.NoError(t, err)
mockNotif.AssertExpectations(t)
}
Parte 6: Ecossistema e automação
15. Comparativo arquitetural: manual, Wire e Fx
Conforme a aplicação ultrapassa a marca de dezenas de microsserviços e centenas de structs, a função main() pode atingir centenas de linhas dedicadas apenas a instanciar construtores na ordem correta. Nesse estágio, os times de engenharia avaliam soluções de automação.
O diagrama contrasta as três principais abordagens adotadas pela indústria em Go:
- Injeção Manual: Escrita pura em Go. Zero reflexão, zero dependência externa, velocidade máxima de compilação e inspeção imediata no editor. Recomendada para a esmagadora maioria dos projetos em Go.
- Geração de Código Estático (
google/wire): Ferramenta desenvolvida pelo time do Google. Analisa declarações de provedores em um arquivo de especificação (wire.go) e gera código Go nativo (wire_gen.go) em tempo de compilação. Preserva a checagem estática de tipos e evita qualquer overhead em runtime. - Contêiner com Reflexão Dinâmica (
uber-go/fxeuber-go/dig): Frameworks desenvolvidos pela Uber. Utilizam o pacotereflectem tempo de inicialização do processo para inspecionar parâmetros de construtores, montar o grafo de objetos em memória e gerenciar ganchos de ciclo de vida (startup e shutdown).
A tabela a seguir resume os pontos de troca (trade-offs) entre as três filosofias:
| Critério | Injeção Manual | Google Wire | Uber Fx / Dig |
|---|---|---|---|
| Mecanismo | Go explícito no main.go | Gerador CLI estático | Reflexão em tempo de execução (reflect) |
| Erros de montagem | Tempo de compilação (go build) | Tempo de geração (wire) | Inicialização do binário (boot time) |
| Custo de runtime | Zero | Zero | Sobrecarga de milissegundos no startup |
| Facilidade de debug | Trivial (jump to definition) | Boa (inspeciona wire_gen.go) | Difícil (requer análise de grafos internos) |
| Curva de aprendizado | Praticamente nula | Moderada (sintaxe de sets) | Alta (ciclo de vida e módulos Fx) |
16. Google Wire na prática
O Wire opera a partir de conjuntos de provedores (Provider Sets). Um provedor é simplesmente uma função construtora comum:
// Arquivo: wire.go (ignorado na compilação final via build tag)
//go:build wireinject
// +build wireinject
package main
import (
"github.com/google/wire"
"meuprojeto/handler"
"meuprojeto/postgres"
"meuprojeto/usecase"
)
func InitializeApp() (*handler.OrderHandler, error) {
wire.Build(
postgres.NewDatabaseConnection,
postgres.NewOrderRepository,
wire.Bind(new(usecase.OrderRepository), new(*postgres.OrderRepository)),
usecase.NewCreateOrderUseCase,
handler.NewOrderHandler,
)
return nil, nil
}
Ao rodar o comando wire no terminal, a ferramenta gera o arquivo wire_gen.go com todo o código de instanciação encadeado em Go puro, acusando erro imediato se faltar algum argumento no grafo.
17. Uber Fx na prática
O Uber Fx é particularmente poderoso quando sua aplicação possui dezenas de servidores de fundo, ganchos de telemetria e precisa de uma máquina de estados robusta para inicialização e desligamento:
package main
import (
"context"
"net/http"
"go.uber.org/fx"
)
func NewHTTPServer(lc fx.Lifecycle, mux *http.ServeMux) *http.Server {
srv := &http.Server{Addr: ":8080", Handler: mux}
// Registra ganchos de ciclo de vida gerenciados pelo contêiner
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,
http.NewServeMux,
// outros construtores...
),
fx.Invoke(func(*http.Server) {
// força inicialização do servidor
}),
).Run()
}
Parte 7: Antipadrões e fechamento
18. Os erros fatais que destroem sua arquitetura
A injeção de dependências pode ser distorcida se aplicada sem critério. Evite rigorosamente as seguintes falhas:
- O antipadrão Service Locator: Passar uma struct genérica de contêiner ou um mapa de serviços como argumento para as funções (ex:
func Process(c *Container)). Isso esconde as verdadeiras dependências do componente, impossibilita checagem pelo compilador e reintroduz acoplamento global disfarçado. - Variáveis globais e Singletons acessíveis de qualquer lugar: Usar ponteiros de banco ou clientes HTTP globais (
var DB *sql.DB). Isso impossibilita rodar testes concorrentes com segurança (t.Parallel()) e cria dependências invisíveis no código. - Interfaces antecipadas para tudo: Criar interfaces para structs puramente internas que nunca serão substituídas ou testadas com dublês. Em Go, adie a criação da interface até o momento em que você realmente precisar de um dublê de teste ou de uma segunda implementação física.
- Dependência circular de pacotes: Se o pacote
Aprecisa do pacoteBe o pacoteBprecisa do pacoteA, a arquitetura violou o princípio de estratificação. A solução é extrair a interface comum para o consumidor ou inverter o sentido da injeção.
19. Exercícios práticos
Exercício 1: Injeção por construtor manual
Cenário: Você tem uma struct UserService que precisa consultar um repositório e registrar logs via slog.Logger. Escreva o contrato mínimo consumido pela struct e a função construtora NewUserService com validação de ponteiros nulos.
Solução comentada:
package user
import (
"context"
"errors"
"log/slog"
)
type User struct {
ID string
Name string
}
// Interface local definida exatamente onde é consumida
type UserFinder interface {
FindUserByID(ctx context.Context, id string) (*User, error)
}
type UserService struct {
finder UserFinder
logger *slog.Logger
}
func NewUserService(finder UserFinder, logger *slog.Logger) (*UserService, error) {
if finder == nil {
return nil, errors.New("UserFinder não pode ser nil")
}
if logger == nil {
return nil, errors.New("slog.Logger não pode ser nil")
}
return &UserService{
finder: finder,
logger: logger,
}, nil
}
Exercício 2: Configuração com Functional Options
Cenário: Construa uma struct EmailSender com endereço de host obrigatório e duas opções funcionais: WithTimeout(time.Duration) e WithMaxAttempts(int).
Solução comentada:
package mail
import "time"
type EmailSender struct {
host string
timeout time.Duration
maxAttempts int
}
type Option func(*EmailSender)
func WithTimeout(t time.Duration) Option {
return func(s *EmailSender) {
if t > 0 {
s.timeout = t
}
}
}
func WithMaxAttempts(attempts int) Option {
return func(s *EmailSender) {
if attempts > 0 {
s.maxAttempts = attempts
}
}
}
func NewEmailSender(host string, opts ...Option) *EmailSender {
sender := &EmailSender{
host: host,
timeout: 3 * time.Second,
maxAttempts: 1,
}
for _, opt := range opts {
opt(sender)
}
return sender
}
Exercício 3: Teste de use case com stub manual
Cenário: Escreva o teste de unidade para uma função de transferência bancária TransferService.Transfer(ctx, from, to, amount). Utilize um stub em memória para simular que a conta de origem não possui saldo suficiente e verifique se o erro correto é retornado.
Solução comentada:
package bank_test
import (
"context"
"errors"
"testing"
)
var ErrSaldoInsuficiente = errors.New("saldo insuficiente na conta de débito")
type AccountRepo interface {
Debit(ctx context.Context, accountID string, amount float64) error
Credit(ctx context.Context, accountID string, amount float64) error
}
type mockAccountRepo struct {
debitFunc func(ctx context.Context, accountID string, amount float64) error
}
func (m *mockAccountRepo) Debit(ctx context.Context, id string, amount float64) error {
if m.debitFunc != nil {
return m.debitFunc(ctx, id, amount)
}
return nil
}
func (m *mockAccountRepo) Credit(ctx context.Context, id string, amount float64) error {
return nil
}
func TestTransfer_SaldoInsuficiente(t *testing.T) {
stub := &mockAccountRepo{
debitFunc: func(ctx context.Context, id string, amount float64) error {
return ErrSaldoInsuficiente
},
}
svc := NewTransferService(stub)
err := svc.Transfer(context.Background(), "conta-A", "conta-B", 500.0)
if !errors.Is(err, ErrSaldoInsuficiente) {
t.Fatalf("esperava erro %v, obteve %v", ErrSaldoInsuficiente, err)
}
}
Exercício 4: Eliminação de dependência global
Cenário: O código a seguir utiliza uma variável global perigosa. Refatore-o para utilizar injeção por construtor com uma interface.
// Código legado problemático
var GlobalDB *sql.DB
func BuscarStatus(id string) string {
row := GlobalDB.QueryRow("SELECT status FROM pedidos WHERE id = $1", id)
var st string
row.Scan(&st)
return st
}
Solução comentada:
package orders
import (
"context"
"database/sql"
)
type StatusQuerier interface {
QueryStatus(ctx context.Context, id string) (string, error)
}
// Adaptador concreto
type DBQuerier struct {
db *sql.DB
}
func NewDBQuerier(db *sql.DB) *DBQuerier {
return &DBQuerier{db: db}
}
func (q *DBQuerier) QueryStatus(ctx context.Context, id string) (string, error) {
var st string
err := q.db.QueryRowContext(ctx, "SELECT status FROM pedidos WHERE id = $1", id).Scan(&st)
return st, err
}
// Serviço desacoplado
type StatusService struct {
querier StatusQuerier
}
func NewStatusService(querier StatusQuerier) *StatusService {
return &StatusService{querier: querier}
}
func (s *StatusService) BuscarStatus(ctx context.Context, id string) (string, error) {
return s.querier.QueryStatus(ctx, id)
}
Exercício 5: Grafo de montagem com graceful shutdown
Cenário: Escreva uma função RunApp compacta que monte manualmente um repositório, um serviço e um handler, retornando o ponteiro do servidor HTTP pronto para escutar conexões e aceitar cancelamento de contexto.
Solução comentada:
package bootstrap
import (
"database/sql"
"net/http"
"time"
)
func BuildServer(db *sql.DB, addr string) *http.Server {
repo := postgres.NewOrderRepository(db)
service := usecase.NewOrderService(repo)
handler := api.NewOrderHandler(service)
mux := http.NewServeMux()
mux.HandleFunc("/v1/orders", handler.ServeHTTP)
return &http.Server{
Addr: addr,
Handler: mux,
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
}
}
20. Glossário técnico
- Injeção de Dependências (DI): Padrão de projeto no qual os componentes recebem suas dependências prontas de uma fonte externa em vez de instanciá-las diretamente.
- Inversão de Dependências (DIP): Princípio arquitetural do SOLID que dita que módulos de alto nível e módulos de baixo nível devem depender ambos de abstrações.
- Duck Typing Estrutural: Sistema de tipagem no qual a compatibilidade de tipos é determinada pela presença e assinatura dos métodos, e não por declarações explícitas.
- Composition Root (Raiz de Composição): O local centralizado do programa (geralmente
main.go) onde todas as instâncias são criadas e o grafo de dependências é montado. - Functional Options: Idioma em Go que utiliza funções como argumentos variáveis para configurar structs de forma flexível e segura contra quebras de compatibilidade.
- Test Double (Dublê de Teste): Termo genérico que engloba qualquer implementação substituta usada em testes (mocks, stubs, fakes, dummies e spies).
- Service Locator: Antipadrão arquitetural que utiliza um registro central para resolver dependências dinamicamente, ocultando o acoplamento real dos componentes.
- Code Generation DI: Ferramenta que gera código de injeção em Go estático durante a compilação (ex: Google Wire), garantindo segurança de tipos sem custo de runtime.
21. Fontes consultadas e leituras recomendadas
- Martin, Robert C. “Clean Architecture: A Craftsman’s Guide to Software Structure and Design”. Prentice Hall.
- Cheney, Dave. “Functional options for friendly APIs”. Artigo técnico publicado no blog oficial do autor.
- Google Wire Documentation: “Compile-time Dependency Injection for Go”. Consulta realizada em 18 de outubro de 2026.
- Uber Go: “Fx Application Framework Guidelines and Architecture Patterns”. Consulta realizada em 18 de outubro de 2026.
- Effective Go: “Interfaces and type assertions”.