Aula 02 · 90–120 minutos
O contrato pertence ao consumidor
Segunda aula: descubra o contrato mínimo a partir da necessidade da regra, declare-o no pacote consumidor e prove que o compilador, o teste e a produção concordam.
Resultado e mapa da aula
Na Aula 01 você separou duas ideias que costumam ser tratadas como sinônimo: injeção de dependência é o mecanismo de entregar um colaborador; inversão de dependência é a decisão sobre a direção do contrato. Esta aula responde à pergunta que vem logo depois: se a regra deve depender de uma abstração, quem escreve essa abstração, e com qual tamanho?
A resposta idiomática em Go é contraintuitiva para quem vem de linguagens com implements explícito: a interface não pertence a quem a implementa. Ela pertence a quem a consome.
Ao final desta aula você vai conseguir fazer quatro coisas.
A primeira: olhar para um serviço Go que importa um pacote de infraestrutura e nomear com precisão o que esse import custa — não "acoplamento" genérico, mas quais mudanças passam a atravessar a regra e quais testes se tornam impossíveis de escrever rápido.
A segunda: derivar o contrato a partir do verbo que a regra usa, não a partir da API que o adaptador oferece. São dois pontos de partida diferentes e produzem interfaces diferentes.
A terceira: prever o que o compilador Go vai aceitar antes de compilar, usando a regra de conjuntos de métodos — incluindo o caso em que o adaptador claramente tem o método e o compilador ainda assim recusa o valor.
A quarta: defender a decisão em revisão de código ou entrevista, com o trade-off explícito, e reconhecer as situações em que a resposta correta é não criar interface nenhuma.
O problema: quando a regra importa o banco
Não vamos partir de um exemplo abstrato. Partimos de um arquivo que existe em quase todo serviço Go de porte médio.
// package checkout
package checkout
import (
"context"
"github.com/acme/loja/internal/postgres"
)
type Service struct {
orders *postgres.OrderRepository
}
func NewService(orders *postgres.OrderRepository) *Service {
return &Service{orders: orders}
}
func (s *Service) Place(ctx context.Context, order Order) error {
if len(order.Items) == 0 {
return ErrEmptyOrder
}
if order.Total <= 0 {
return ErrInvalidTotal
}
return s.orders.Save(ctx, order)
}
Repare que esse código já faz injeção de dependência. NewService recebe o repositório em vez de criá-lo. Se o critério fosse "a dependência vem de fora", este código passaria. E ainda assim ele tem o problema que esta aula resolve.
A regra Place toma duas decisões de negócio: um pedido sem itens é inválido, e um total não positivo é inválido. Essas duas linhas são a razão de o arquivo existir. Tudo o mais é transporte.
Mas o pacote checkout importa postgres. E é isso que muda o comportamento do sistema inteiro sob manutenção.
O que o import concreto custa na prática
O custo não é estético. Ele aparece em quatro momentos concretos, e vale a pena enumerá-los porque é exatamente essa lista que você usa para justificar a mudança em uma revisão de código.
Primeiro, o teste da regra passa a exigir infraestrutura. Para verificar que um pedido sem itens devolve ErrEmptyOrder, você precisa de um *postgres.OrderRepository. Para construí-lo, precisa de uma conexão. Para ter uma conexão, precisa de um banco de pé, com o schema migrado. Uma decisão de negócio que não toca no banco agora depende de rede, de migração e de dados compartilhados entre testes. O ciclo de feedback sai de milissegundos para segundos ou dezenas de segundos.
Segundo, mudanças no detalhe atravessam a regra. Se o adaptador troca a assinatura de Save — acrescenta um parâmetro de transação, devolve o ID gerado, passa a receber um tipo próprio em vez de Order — o pacote checkout deixa de compilar. A política foi alterada por uma decisão que pertencia inteiramente à persistência.
Terceiro, o pacote de negócio carrega a árvore de dependências da infraestrutura. checkout agora depende transitivamente do driver, do pool de conexões e de tudo que postgres importar. Qualquer pessoa que queira entender a regra de checkout precisa carregar esse contexto junto.
Quarto, o risco de ciclo de import aumenta. Se algum dia postgres precisar do tipo checkout.Order para mapear colunas — o que é natural — você tem checkout → postgres → checkout. Go recusa ciclos de import na compilação. Nesse ponto a saída costuma ser um terceiro pacote criado às pressas, com um nome vago como models ou types, que vira depósito de tudo.
Três sintomas que aparecem antes do dano
Na prática você raramente encontra esse problema enunciado. Encontra os sintomas. Vale reconhecê-los:
- O arquivo de teste da regra tem
setupeteardownde banco. Se o teste de uma condiçãoifprecisa deTRUNCATE TABLE, a fronteira está no lugar errado. - O pacote de negócio tem imports de bibliotecas de infraestrutura no topo. Abra o arquivo e leia só o bloco
import. Ele deveria descrever o domínio, não a stack. - Ninguém consegue responder "o que esta regra precisa?" sem abrir o repositório. Se a resposta é "ela usa o repositório", a necessidade não está declarada em lugar nenhum — está implícita no tipo concreto.
Arquitetura: onde a seta aponta
Antes de escrever a solução, é preciso ser preciso sobre o que exatamente será invertido — porque a palavra "inversão" engana quem lê rápido.
Dependência de código e chamada em execução
Existem duas setas diferentes no mesmo sistema, e elas não apontam para o mesmo lado depois da mudança.
ANTES
código: checkout ──importa──→ postgres
execução: checkout ──chama────→ postgres
────────────────────────────────────────────────────────
DEPOIS
código: checkout ←──importa── postgres
checkout ──declara──→ OrderSaver (no próprio pacote)
execução: checkout ──chama────→ postgres (via contrato)A seta de execução não muda. Em produção, continua sendo o checkout que chama o código do PostgreSQL. Nenhum truque inverte isso: alguém precisa gravar a linha no banco.
A seta de código é a que inverte. Antes, checkout mencionava postgres no seu texto-fonte. Depois, checkout não menciona ninguém: ele declara o que precisa. E postgres passa a ser o lado que se adapta.
Essa distinção é exatamente a formulação original de Robert C. Martin: o princípio trata de qual módulo depende de qual no código-fonte, e da observação de que módulos de alto nível ficam sensíveis a detalhes quando a dependência aponta na direção natural da chamada (Martin, The Dependency Inversion Principle, 1996, pp. 5–6).
As quatro fases da mesma linha de código
A linha s.orders.Save(ctx, order) é a mesma nos quatro momentos abaixo, mas significa algo diferente em cada um. Entender isso é o que permite prever o comportamento sem executar.
1. COMPILAÇÃO
O compilador verifica: o tipo entregue a NewService tem o
método Save com a assinatura exigida? Se sim, compila.
Nenhuma decisão de runtime acontece aqui.
────────────────────────────────────────────────────────
2. INICIALIZAÇÃO (main, uma vez)
main abre a conexão, constrói *postgres.OrderRepository
e o entrega a checkout.NewService. A escolha concreta
acontece aqui e em nenhum outro lugar.
────────────────────────────────────────────────────────
3. OPERAÇÃO (a cada requisição)
Service.Place valida e chama s.orders.Save. O valor
dinâmico por trás do contrato é *postgres.OrderRepository.
Service não sabe disso e não pode descobrir.
────────────────────────────────────────────────────────
4. TESTE (a cada go test)
O mesmo Service.Place roda. O valor dinâmico é um fake
em memória. A linha de código é idêntica; o colaborador
mudou sem que a regra fosse recompilada de outro jeito.
A propriedade importante está na comparação entre 3 e 4: a regra executa o mesmo caminho nos dois casos. Não há ramo if testing, nem flag, nem construtor alternativo. É isso que torna o teste uma evidência sobre o código que roda em produção, e não sobre uma variante dele.
Vocabulário preciso
Três termos que serão usados com sentido estrito no resto do curso. Vale fixá-los agora porque entrevistas e revisões de código tratam-nos como intercambiáveis, e eles não são.
Política e detalhe
Política é a regra que expressa a decisão de negócio: pedido vazio é inválido. Ela é a razão de o sistema existir e tende a mudar por motivos de negócio.
Detalhe é a escolha de como a política é realizada: PostgreSQL, HTTP, JSON. Tende a mudar por motivos técnicos — desempenho, custo, fornecedor.
O critério de separação não é "código meu" versus "biblioteca". É por qual motivo isso muda?. Se duas coisas mudam por razões diferentes, elas não deveriam estar presas uma à outra.
Conjunto de métodos e satisfação implícita
Este é o mecanismo de Go que torna tudo o mais possível, e ele é definido normativamente na especificação.
Um tipo T implementa uma interface I quando T é elemento do conjunto de tipos de I; para uma interface básica, isso significa que o conjunto de métodos de T contém os métodos exigidos (Go Language Specification, Implementing an interface).
Não existe implements. O tipo concreto não precisa conhecer a interface, não a importa e pode ter sido escrito anos antes dela. É essa propriedade que permite ao consumidor declarar o contrato depois, sem tocar no adaptador (Go FAQ, Why are there no implements declarations?).
Contrato do consumidor
A interface declarada no pacote que usa o comportamento, dimensionada pela necessidade desse pacote — não pela capacidade do adaptador.
O guia oficial de revisão de código de Go recomenda exatamente isso: interfaces pertencem ao pacote que usa os valores, e o pacote que implementa deve devolver tipos concretos, para poder ganhar métodos novos sem forçar refatoração nos consumidores (Go Wiki, Code Review Comments — Interfaces).
Do verbo ao contrato, linha a linha
Agora a construção. Quatro passos, cada um com uma pergunta que você pode fazer no seu próprio código.
Passo 1 — encontrar o verbo da regra
A pergunta é: quais métodos do colaborador esta regra realmente chama?
Não "quais métodos existem". Quais são chamados. Volte ao Place original: há exatamente uma chamada, s.orders.Save(ctx, order).
Suponha que *postgres.OrderRepository ofereça isto:
// package postgres — a capacidade completa do adaptador
func (r *OrderRepository) Save(ctx context.Context, o checkout.Order) error
func (r *OrderRepository) FindByID(ctx context.Context, id string) (checkout.Order, error)
func (r *OrderRepository) ListByCustomer(ctx context.Context, c string) ([]checkout.Order, error)
func (r *OrderRepository) Delete(ctx context.Context, id string) error
func (r *OrderRepository) BeginTx(ctx context.Context) (*sql.Tx, error)
Cinco métodos disponíveis. Um usado. Essa diferença é a medida do acoplamento acidental: hoje o checkout está preso a quatro comportamentos que não consome.
Passo 2 — declarar a interface no consumidor
O contrato descreve o verbo encontrado, e mora no pacote da regra:
// package checkout — o contrato pertence a quem precisa dele
package checkout
import "context"
// OrderSaver é o que o checkout precisa do mundo para registrar um pedido.
type OrderSaver interface {
Save(ctx context.Context, order Order) error
}
type Service struct {
orders OrderSaver
}
func NewService(orders OrderSaver) *Service {
return &Service{orders: orders}
}
func (s *Service) Place(ctx context.Context, order Order) error {
if len(order.Items) == 0 {
return ErrEmptyOrder
}
if order.Total <= 0 {
return ErrInvalidTotal
}
return s.orders.Save(ctx, order)
}
Três coisas mudaram, e vale ler cada uma:
O bloco import perdeu postgres. Agora importa só context. Esse é o resultado observável da inversão — e é verificável mecanicamente, o que veremos adiante.
O campo passou de *postgres.OrderRepository para OrderSaver. O tipo do campo agora é uma abstração que o próprio pacote possui.
O corpo de Place não mudou nenhum caractere. Isso é importante: a inversão de dependência é uma mudança na fronteira, não na regra. Se você se viu reescrevendo a lógica de negócio, algo mais estava acontecendo.
Passo 3 — o adaptador satisfaz sem saber
E o que muda no pacote postgres? Nada.
// package postgres — inalterado
package postgres
type OrderRepository struct {
db *sql.DB
}
func NewOrderRepository(db *sql.DB) *OrderRepository {
return &OrderRepository{db: db}
}
func (r *OrderRepository) Save(ctx context.Context, o checkout.Order) error {
_, err := r.db.ExecContext(ctx,
`INSERT INTO orders (id, customer_id, total) VALUES ($1, $2, $3)`,
o.ID, o.CustomerID, o.Total,
)
if err != nil {
return fmt.Errorf("postgres: salvar pedido %s: %w", o.ID, err)
}
return nil
}
*OrderRepository já tem Save com a assinatura exigida. Pela regra da especificação, ele pertence ao conjunto de tipos de checkout.OrderSaver — sem declaração, sem anotação, sem import da interface.
Repare que postgres importa checkout (para o tipo Order). Essa é a seta invertida. O detalhe conhece o domínio; o domínio não conhece o detalhe. E não há ciclo, porque checkout não importa postgres.
Note também que a query é parametrizada com $1, $2, $3. Isso não é detalhe de estilo: concatenar valores em SQL abriria injeção. O contrato do consumidor não protege contra isso — cada controle de segurança permanece na borda responsável por ele.
Passo 4 — a composição na borda
Alguém ainda precisa decidir que em produção o OrderSaver é o PostgreSQL. Esse alguém é main:
// package main — o único lugar que conhece os dois lados
func main() {
db, err := sql.Open("pgx", os.Getenv("DATABASE_URL"))
if err != nil {
log.Fatalf("abrir banco: %v", err)
}
defer db.Close()
orders := postgres.NewOrderRepository(db) // detalhe concreto
service := checkout.NewService(orders) // satisfaz OrderSaver
if err := runHTTPServer(service); err != nil {
log.Fatalf("servidor: %v", err)
}
}
A linha checkout.NewService(orders) é onde o compilador faz a verificação. Se *postgres.OrderRepository não tivesse Save com aquela assinatura exata, a compilação falharia aqui, na borda — não dentro da regra.
A Aula 03 trata desse ponto de composição em profundidade. Por ora basta reconhecer que ele existe e que é deliberadamente o único lugar que conhece os dois lados.
Percorra o diagrama da necessidade de negócio até os dois adaptadores. Observe que o nó do contrato está dentro da fronteira do pacote checkout, e que as duas implementações — PostgreSQL e fake — apontam para ele de fora. Abrir em tela cheia.
Verifique o modelo mental
No código final, qual pacote importa qual?
Quatro abordagens que parecem certas
Estas quatro aparecem em código real, e cada uma é defendida com um argumento razoável. Entender por que falham é mais útil do que memorizar a forma correta.
Interface no pacote do adaptador
// package postgres — contrato no lugar errado
package postgres
type OrderRepository interface {
Save(ctx context.Context, o checkout.Order) error
}
O argumento: "a interface fica perto da implementação, é mais fácil de achar."
Por que falha: checkout continua precisando importar postgres — agora para pegar a interface em vez do struct. O bloco import da regra não mudou. Você adicionou uma indireção e não removeu acoplamento nenhum. Além disso, quem passa a decidir a forma do contrato é a infraestrutura: se amanhã o adaptador achar conveniente mudar a assinatura, ele muda a interface que a regra consome.
Esse é o ponto exato em que o guia oficial de Go recomenda o contrário, colocando a interface no pacote que usa os valores (Go Wiki, Code Review Comments — Interfaces).
Interface espelhando o repositório inteiro
// package checkout — lugar certo, tamanho errado
type OrderRepository interface {
Save(ctx context.Context, o Order) error
FindByID(ctx context.Context, id string) (Order, error)
ListByCustomer(ctx context.Context, c string) ([]Order, error)
Delete(ctx context.Context, id string) error
BeginTx(ctx context.Context) (*sql.Tx, error)
}
O argumento: "assim o contrato já cobre tudo que vamos precisar."
Por que falha, e esta é a mais instrutiva: o contrato deixou de descrever a necessidade e passou a descrever a capacidade do banco. Três consequências concretas:
O fake do teste agora precisa implementar cinco métodos para testar uma regra que usa um. Quatro deles serão corpos vazios — código morto que precisa ser mantido e que mente sobre o que a regra faz.
BeginTx devolve *sql.Tx. O tipo do banco vazou para dentro do contrato do domínio. O pacote checkout agora importa database/sql. A inversão falhou: você moveu o acoplamento de um import para uma assinatura.
Quando um segundo consumidor aparecer e precisar só de leitura, ele herdará a obrigação dos cinco métodos ou você duplicará a interface.
O guia de Go é explícito sobre não definir interfaces antes de existir um uso real, justamente porque sem o uso não dá para saber quais métodos ela deveria ter (Go Wiki, Code Review Comments — Interfaces).
Interface criada só para permitir mock
// package postgres
type Repository interface { /* ... */ } // criada porque "precisamos mockar"
O argumento: "sem interface não dá para testar."
Por que falha: a testabilidade é uma consequência de a fronteira estar no lugar certo, não a justificativa para criar a fronteira. Quando o mock é o motivo, a interface tende a nascer no pacote do implementador e com a forma da implementação — que é o problema das duas abordagens anteriores combinadas.
O mesmo guia oficial desaconselha definir a interface no lado do implementador apenas para mocking, recomendando projetar a API para ser testada pela superfície pública real (Go Wiki, Code Review Comments — Interfaces).
A distinção prática: se você remover todos os testes, a interface ainda se justifica? Se sim, a fronteira é real. Se não, provavelmente você está abstraindo cedo demais.
Construtor que devolve interface
// package postgres
func NewOrderRepository(db *sql.DB) checkout.OrderSaver { // devolve interface
return &OrderRepository{db: db}
}
O argumento: "garante que a implementação satisfaz o contrato."
Por que falha: o adaptador passa a importar checkout só por causa da assinatura, e o chamador perde acesso aos outros métodos do repositório — inclusive os que a camada de composição poderia precisar, como um Close ou um Ping de health check.
A recomendação oficial é a inversa: pacotes que implementam devem retornar tipos concretos, o que permite acrescentar métodos sem quebrar consumidores (Go Wiki, Code Review Comments — Interfaces). A conversão para a interface acontece naturalmente na atribuição, em main.
Se quiser a garantia em tempo de compilação sem devolver a interface, existe o idioma da asserção vazia:
// package postgres
var _ checkout.OrderSaver = (*OrderRepository)(nil)
Essa linha não gera código; ela falha a compilação se o tipo deixar de satisfazer o contrato. Use com parcimônia — ela reintroduz o import de checkout no adaptador, o que aqui já existe de qualquer forma por causa de Order.
Verifique o diagnóstico
Um contrato do consumidor inclui BeginTx(ctx) (*sql.Tx, error). Qual é o problema mais grave?
O contrato correto e seus limites
Nomear pelo comportamento
A convenção de Go é derivar o nome do método, com sufixo -er: Reader, Writer, Saver. Interfaces de um ou dois métodos são o caso comum na linguagem (Pike, Expressiveness of Go, 2010), e Effective Go usa Write como contrato suficiente para fmt.Fprintf escrever em destinos diferentes (Effective Go — Interfaces).
Nomes a evitar, e o que eles revelam:
OrderRepository— descreve um padrão de persistência, não a necessidade do checkout. Empurra para incluir CRUD completo.OrderService— descreve uma camada, não um comportamento. Costuma virar depósito.IOrderSaver— prefixoIé convenção de C# e Java; em Go destoa e não agrega informação.OrderManager,OrderHandler— verbos vagos que não restringem nada.
O teste do nome: ele responde "o que a regra precisa fazer?". OrderSaver responde: salvar pedido. OrderRepository não responde.
context.Context e erro atravessam a fronteira
Duas coisas devem estar no contrato, e é fácil errar por omissão.
context.Context como primeiro parâmetro permite que um cancelamento — cliente desconectou, timeout do servidor — chegue ao adaptador e aborte a query. Se o contrato não tiver ctx, o cancelamento morre na fronteira e o banco continua trabalhando por uma requisição que ninguém espera mais.
error como último retorno permite que a falha do detalhe suba. A questão interessante é o que a regra faz com esse erro. Ela não deve inspecionar erros do PostgreSQL — isso reintroduziria o acoplamento pela porta dos fundos:
// ERRADO: a regra volta a conhecer o detalhe
if strings.Contains(err.Error(), "duplicate key") { ... }
// ERRADO: import de infraestrutura de volta no domínio
var pgErr *pgconn.PgError
if errors.As(err, &pgErr) && pgErr.Code == "23505" { ... }
A tradução pertence ao adaptador, que converte a condição técnica em um erro do domínio:
// package checkout — erro sentinela do domínio
var ErrDuplicateOrder = errors.New("checkout: pedido já registrado")
// package postgres — o adaptador traduz
func (r *OrderRepository) Save(ctx context.Context, o checkout.Order) error {
_, err := r.db.ExecContext(ctx, `INSERT INTO orders ...`, o.ID, o.CustomerID, o.Total)
var pgErr *pgconn.PgError
if errors.As(err, &pgErr) && pgErr.Code == "23505" {
return fmt.Errorf("%w: %s", checkout.ErrDuplicateOrder, o.ID)
}
if err != nil {
return fmt.Errorf("postgres: salvar pedido %s: %w", o.ID, err)
}
return nil
}
Agora a regra pode decidir sobre checkout.ErrDuplicateOrder com errors.Is, sem saber que existe um código 23505 no mundo.
Quando o contrato pode crescer
O contrato mínimo não é dogma de "um método por interface". O critério é: cresce quando o mesmo consumidor passa a precisar de outro comportamento.
Se checkout.Service precisar verificar duplicidade antes de salvar, OrderSaver pode ganhar um segundo método — ou, melhor, o serviço passa a receber dois contratos pequenos.
O que não justifica crescer: outro consumidor, em outro pacote, precisar de leitura. Esse consumidor declara o próprio contrato:
// package invoicing — outro consumidor, outro contrato
type OrderFinder interface {
FindByID(ctx context.Context, id string) (checkout.Order, error)
}
O mesmo *postgres.OrderRepository satisfaz checkout.OrderSaver e invoicing.OrderFinder ao mesmo tempo, porque tem os dois métodos. Cada consumidor recebe a menor capacidade que usa, e main entrega o mesmo valor aos dois. Esse tema é o assunto da pesquisa que fundamenta a continuação da trilha.
Quando não criar interface nenhuma
Esta seção existe porque a falha mais comum depois de aprender DIP é aplicá-lo em tudo.
Não crie o contrato quando:
- Não há variação real nem necessidade de teste isolado. Um
strings.Builder, um validador puro, um cálculo — use o tipo concreto. - O colaborador é estável e interno. Um helper do mesmo módulo que muda junto com a regra não é uma fronteira.
- Existe um único implementador e nenhuma pressão de teste. Uma interface com uma implementação é uma indireção que o leitor precisa atravessar sem ganho.
O custo de uma interface é real: mais um nome, mais um salto para quem lê, e a perda do "ir para a definição" direto no editor. A Aula 06 dedica-se a esse critério de decisão.
Verifique o limite
Um segundo pacote, invoicing, precisa ler pedidos. Qual é a resposta idiomática?
A armadilha do receiver: T ou *T
Este é o erro de compilação mais comum ao conectar um adaptador a um contrato do consumidor. Ele confunde porque o método claramente existe.
Por que o compilador recusa o valor
A especificação de Go define conjuntos de métodos assim: o conjunto de métodos de um tipo definido T consiste nos métodos declarados com receiver T; o conjunto de métodos de *T consiste nos métodos declarados com receiver *T ou T (Go Language Specification, Method sets).
A assimetria é o ponto: *T tem tudo que T tem, mas T não tem os métodos de ponteiro.
// Save é declarado com receiver de ponteiro
func (r *OrderRepository) Save(ctx context.Context, o checkout.Order) error { ... }
repo := postgres.OrderRepository{} // valor, tipo OrderRepository
checkout.NewService(repo) // NÃO COMPILA
checkout.NewService(&repo) // compila
O conjunto de métodos de OrderRepository (valor) não contém Save, porque Save foi declarado com receiver *OrderRepository. O conjunto de métodos de *OrderRepository contém.
Como NewOrderRepository devolve *OrderRepository, o caminho normal funciona sem que você pense nisso. O erro aparece quando alguém constrói o valor diretamente — o que é muito comum em fakes de teste:
// package checkout_test — o mesmo erro, no lugar onde mais aparece
type fakeSaver struct{ saved Order }
func (f *fakeSaver) Save(_ context.Context, o Order) error { // ponteiro
f.saved = o
return nil
}
func TestPlace(t *testing.T) {
fake := fakeSaver{} // valor
svc := checkout.NewService(fake) // NÃO COMPILA
svc = checkout.NewService(&fake) // compila
_ = svc
}
E há uma razão para o fake usar receiver de ponteiro: ele precisa mutar o campo saved para o teste poder observá-lo. Com receiver de valor, a mutação aconteceria numa cópia e o teste sempre veria o zero value — um fake com receiver de valor compila e passa a falhar silenciosamente de um jeito difícil de diagnosticar.
Como ler a mensagem de erro
O compilador é específico, e vale aprender a ler:
cannot use fake (variable of type fakeSaver) as checkout.OrderSaver
value in argument to checkout.NewService: fakeSaver does not implement
checkout.OrderSaver (method Save has pointer receiver)
O trecho entre parênteses no fim — method Save has pointer receiver — é o diagnóstico completo. Quando ele aparece, a correção é quase sempre passar &.
Verifique a regra de tipos
Save tem receiver *OrderRepository. Qual afirmação é correta?
Exercício guiado
Abaixo, um serviço de notificações com o mesmo problema da abertura da aula, em outro domínio.
// package notify
package notify
import (
"context"
"github.com/acme/loja/internal/sendgrid"
)
type Notifier struct {
mail *sendgrid.Client
}
func NewNotifier(mail *sendgrid.Client) *Notifier {
return &Notifier{mail: mail}
}
func (n *Notifier) NotifyShipped(ctx context.Context, o Order) error {
if o.CustomerEmail == "" {
return ErrNoEmail
}
subject := "Seu pedido " + o.ID + " foi enviado"
return n.mail.SendEmail(ctx, o.CustomerEmail, subject, renderBody(o))
}
O *sendgrid.Client oferece SendEmail, SendTemplate, AddContact, RemoveContact, ValidateAddress e GetQuota.
Sua tarefa, nesta ordem e sem consultar a solução:
- Identifique o verbo que
notifyrealmente consome. - Escreva a interface mínima, no pacote certo, com o nome certo.
- Reescreva a
structe o construtor. - Escreva a linha de composição em
main. - Escreva o fake do teste, com o receiver correto, e uma asserção sobre o efeito.
- Responda em uma frase: o que passou a ser verdade sobre o bloco
importdenotify?
Guia de solução do exercício
O raciocínio antes do código
A pergunta que abre tudo não é "qual interface criar", é "o que este código faz que é decisão de negócio?". Em NotifyShipped há uma: e-mail vazio é erro. O resto — montar o assunto, chamar o provedor — é transporte.
Feito isso, conte as chamadas ao colaborador. Há exatamente uma: n.mail.SendEmail(...). Seis métodos disponíveis, um consumido. O contrato tem um método.
Solução incorreta 1 — contrato do produtor
// package sendgrid
type EmailClient interface {
SendEmail(ctx context.Context, to, subject, body string) error
}
Por que parece certo: a interface está declarada, o serviço deixa de depender do struct concreto, e ela fica junto de quem a implementa — o que muitos times acham mais organizado.
Por que falha: notify continua com import ".../sendgrid" no topo. O acoplamento estático permanece intacto; só mudou o símbolo importado. Teste você mesmo o critério: liste os imports de notify antes e depois. Se sendgrid continua lá, não houve inversão.
Consequência em manutenção: trocar de provedor de e-mail continua exigindo tocar no pacote de domínio, porque é lá que está o import. E o pacote sendgrid agora dita a forma do contrato — se ele decidir que SendEmail recebe um struct de opções, o domínio muda junto.
Solução incorreta 2 — contrato inflado
// package notify
type EmailClient interface {
SendEmail(ctx context.Context, to, subject, body string) error
SendTemplate(ctx context.Context, to, templateID string, data map[string]any) error
AddContact(ctx context.Context, email string) error
RemoveContact(ctx context.Context, email string) error
ValidateAddress(ctx context.Context, email string) (bool, error)
GetQuota(ctx context.Context) (int, error)
}
Por que parece certo: o pacote está certo — notify, o consumidor. E "já deixa pronto" soa como previdência.
Por que falha: o contrato descreve a capacidade do SendGrid, não a necessidade do notify. O fake do teste precisa de seis métodos para testar um. Cinco serão assim:
func (f *fakeMailer) AddContact(context.Context, string) error { return nil }
func (f *fakeMailer) RemoveContact(context.Context, string) error { return nil }
// ...mais três
Consequência: esses corpos vazios são código morto que precisa ser mantido e que mente para quem lê o teste — sugerem que o notify gerencia contatos, o que não é verdade. Pior: quando o provedor acrescentar SendSMS, alguém sentirá que deve adicioná-lo ao contrato, e todo fake do repositório quebra de uma vez.
Solução correta, passo a passo
Passo 1 — o contrato, no consumidor:
// package notify
package notify
import "context"
// ShipmentMailer é o que notify precisa do mundo para avisar o cliente.
type ShipmentMailer interface {
SendEmail(ctx context.Context, to, subject, body string) error
}
O nome merece atenção. EmailClient descreveria o adaptador; ShipmentMailer descreve o papel que ele cumpre para este consumidor. Se amanhã o envio passar a ser por SMS, o nome do contrato continua honesto e só o adaptador muda.
Passo 2 — a struct e o construtor:
type Notifier struct {
mail ShipmentMailer
}
func NewNotifier(mail ShipmentMailer) *Notifier {
return &Notifier{mail: mail}
}
NotifyShipped não muda nenhum caractere. Isso é o esperado: a mudança é de fronteira.
Passo 3 — a composição:
// package main
client := sendgrid.NewClient(os.Getenv("SENDGRID_API_KEY"))
notifier := notify.NewNotifier(client)
A chave de API é lida da configuração da borda e nunca atravessa para o pacote de domínio. Um segredo em código, ou passado ao notify, seria uma falha independente da que estamos corrigindo — e o desenho correto torna o erro mais difícil de cometer, porque notify não tem onde guardar uma chave.
Passo 4 — o fake e a asserção:
// package notify_test
type fakeMailer struct {
to, subject, body string
calls int
err error
}
func (f *fakeMailer) SendEmail(_ context.Context, to, subject, body string) error {
f.calls++
f.to, f.subject, f.body = to, subject, body
return f.err
}
func TestNotifyShippedEnviaParaOCliente(t *testing.T) {
mailer := &fakeMailer{}
notifier := notify.NewNotifier(mailer)
order := notify.Order{ID: "o-1", CustomerEmail: "ana@exemplo.com"}
if err := notifier.NotifyShipped(context.Background(), order); err != nil {
t.Fatalf("NotifyShipped devolveu erro: %v", err)
}
if mailer.calls != 1 {
t.Errorf("SendEmail chamado %d vezes, queria 1", mailer.calls)
}
if mailer.to != "ana@exemplo.com" {
t.Errorf("destinatário = %q, queria %q", mailer.to, "ana@exemplo.com")
}
}
func TestNotifyShippedRecusaEmailVazio(t *testing.T) {
mailer := &fakeMailer{}
notifier := notify.NewNotifier(mailer)
err := notifier.NotifyShipped(context.Background(), notify.Order{ID: "o-2"})
if !errors.Is(err, notify.ErrNoEmail) {
t.Fatalf("erro = %v, queria ErrNoEmail", err)
}
if mailer.calls != 0 {
t.Errorf("SendEmail foi chamado %d vezes; a regra deveria barrar antes", mailer.calls)
}
}
Dois detalhes que valem mais que o resto do teste:
O receiver de SendEmail é *fakeMailer, e a variável é criada com &. Se fosse receiver de valor, os campos gravados morreriam na cópia e a asserção veria strings vazias — falha silenciosa, difícil de diagnosticar.
O segundo teste verifica calls != 0. Essa asserção afirma algo que a regra promete e que a asserção de erro sozinha não cobre: que a validação acontece antes do efeito colateral. É o tipo de evidência que só fica barato de obter quando a fronteira está no lugar certo.
Passo 5 — a resposta da pergunta 6: o bloco import de notify passou a conter apenas context. Nenhuma referência a provedor de e-mail. Esse é o resultado observável da inversão.
Como verificar o resultado
Três verificações, da mais barata à mais completa:
# 1. O domínio não conhece mais a infraestrutura.
# Saída vazia é o resultado desejado.
go list -deps ./internal/notify | grep -i sendgrid
# 2. Tudo compila e os testes da regra passam sem serviço externo.
go test ./internal/notify/...
# 3. O teste da regra não depende de rede: rode sem acesso externo
# e confirme que continua verde e determinístico.
go test -count=5 ./internal/notify/...
A primeira é a mais valiosa e a mais esquecida: go list -deps imprime o fecho transitivo de dependências do pacote. Se sendgrid aparecer ali, o import voltou por algum caminho indireto — normalmente um tipo que vazou para uma assinatura.
Perguntas de entrevista e mercado
Seis perguntas que aparecem em processos seletivos para vagas Go, com o que uma resposta forte contém.
1. Onde você declara uma interface em Go, e por quê?
No pacote que consome o comportamento, depois de existir um uso real. Cite a implementação implícita como o mecanismo que torna isso possível: o adaptador não precisa conhecer a interface. Feche com a recomendação oficial de que o pacote produtor devolva tipos concretos. O erro comum é responder "junto da implementação, para ficar organizado" — revela que a pessoa não distingue organização de direção de dependência.
2. Qual a diferença entre injeção e inversão de dependência?
Injeção é o mecanismo de entrega; inversão é a direção do contrato. O exemplo que encerra a discussão é o desta aula: NewService(orders *postgres.OrderRepository) é injeção de dependência e não é inversão, porque o domínio continua importando a infraestrutura.
3. A regra não continua chamando o banco? O que exatamente inverteu?
Separe as duas setas. Em execução, o checkout continua chamando o PostgreSQL. No código-fonte, a direção inverteu: o detalhe passou a depender do contrato do domínio. É uma pergunta-armadilha que testa se você entende o princípio ou decorou o diagrama.
4. Por que interfaces pequenas?
Porque o contrato expressa a necessidade do consumidor, não a capacidade do adaptador. Dê a consequência concreta: cada método a mais é um método a mais em todo fake, e um risco a mais de vazar um tipo de infraestrutura para dentro da assinatura. Mencione que interfaces de um ou dois métodos são o caso comum em Go.
5. Quando você não criaria uma interface?
Esta pergunta separa quem entendeu o trade-off de quem aprendeu uma regra. Responda com o critério: sem variação real, sem fronteira e sem necessidade de teste isolado, o tipo concreto é mais claro. Dizer "sempre crio interface" é resposta fraca — indica abstração especulativa.
6. Como você provaria que o desacoplamento aconteceu?
go list -deps no pacote de domínio, procurando o pacote de infraestrutura. Poucos candidatos dão uma resposta verificável aqui, e é uma forma barata de se diferenciar.
Recuperação: 12 questões
Responda sem voltar ao texto. Errar aqui é mais útil do que acertar relendo — o esforço de tentar recuperar da memória é o que constrói retenção durável.
Em Go, um tipo satisfaz uma interface quando:
checkout importa postgres e recebe o repositório pelo construtor. Isso é:
Save tem receiver *OrderRepository. Passar o valor, e não o ponteiro, ao construtor:
Qual é a consequência mais concreta de um contrato com métodos que a regra não usa?
Qual assinatura em um contrato de domínio deve acender um alerta?
Por que um fake de teste normalmente usa receiver de ponteiro?
A regra precisa tratar violação de chave única do PostgreSQL. Onde isso é traduzido?
Qual comando verifica que o domínio não conhece mais a infraestrutura?
Outro pacote precisa apenas ler pedidos. A resposta idiomática é:
O que NÃO muda quando se aplica a inversão como nesta aula?
Quando a resposta correta é NÃO criar uma interface?
Qual nome descreve melhor um contrato do consumidor?
Próximos passos e fontes
Você agora sabe descobrir e posicionar o contrato. Duas perguntas ficaram abertas de propósito.
A primeira: quem constrói o adaptador concreto e quando? A Aula 03 trata do ponto de composição — por que ele existe, por que roda só na partida e o que acontece quando a regra cria a própria dependência.
A segunda: como transformar isso em teste rápido de verdade? A Aula 04 aprofunda o fake, a diferença entre teste unitário e de integração, e como escolher o menor dublê que produz a evidência necessária.
Fontes primárias
- Go Wiki — Code Review Comments: Interfaces. A fonte central desta aula: interfaces no pacote consumidor, retorno de tipos concretos e a advertência contra criar interfaces antes do uso real ou apenas para mocking.
- Go Language Specification — Method sets e Implementing an interface. O texto normativo por trás da armadilha
Tversus*T. - Go FAQ — Why are there no implements declarations?. Por que a satisfação implícita permite declarar a interface depois, sem tocar no adaptador.
- Effective Go — Interfaces. Contratos mínimos e o exemplo canônico de
io.Writer. - Robert C. Martin — The Dependency Inversion Principle (1996). A formulação original; pp. 5–6 para a sensibilidade dos módulos de alto nível aos detalhes.
- Martin Fowler — Inversion of Control Containers and the Dependency Injection pattern. Distingue o mecanismo de injeção do princípio, e separa configuração de uso.
Material de apoio deste curso
- Glossário do curso — política, detalhe, contrato do consumidor, ponto de composição.
- Diagrama do contrato do consumidor — o fluxo desta aula em tela cheia.
- Diagrama geral da inversão — comparação entre composição em produção e em teste.
Anterior: Aula 01 — Inverter a seta: DI não é DIP · Próxima: Aula 03 — Compor na borda