Aula 02 · 50–70 minutos

Interfaces em Go

Aula 2 — satisfação implícita, a convenção de onde declarar a interface, por que isso facilita testes, e a pegadinha do nil.

Resultado e mapa da aula

Na Lição 1 você viu a Regra de Dependência: o código de alto nível define uma interface, o código de baixo nível a implementa. Esta aula mostra como isso é escrito em Go — e a resposta tem uma peculiaridade que costuma surpreender quem vem de outras linguagens orientadas a objetos.

  1. Distinguimos struct (dados) de interface (comportamento).
  2. Vemos a satisfação implícita — a peculiaridade central de Go.
  3. Aplicamos a convenção de onde declarar a interface, ligando de volta à Regra de Dependência.
  4. Fechamos com uma pegadinha clássica: nil que não é nil.

Struct e interface, rapidamente

Antes de entrar na peculiaridade, dois tipos que Go usa o tempo todo — e que valem uma pausa, porque o resto da aula depende de você distinguir os dois de cabeça.

Uma struct agrupa dados: uma lista de campos, cada um com um tipo. É um tipo concreto — algo que existe de fato na memória, com valores reais dentro. Se você já usou uma classe só com atributos (sem métodos ainda), ou um record/"data class", é a mesma ideia:

type Pedido struct {
    ID    string
    Total float64
}

p := Pedido{ID: "1", Total: 500}

Uma interface agrupa comportamento: uma lista de métodos que um tipo precisa ter, sem nenhum dado próprio. Isso é igual ao conceito de interface que você já conhece de outras linguagens orientadas a objetos — um contrato, sem estado:

type Buscador interface {
    Buscar(id string) (Pedido, error)
}

Repare que Buscador não tem campos, só a assinatura de um método. Nada aqui ainda diz quem implementa esse método — só que qualquer tipo que tiver um método Buscar com essa assinatura conta como um Buscador. É exatamente aí que mora a peculiaridade de Go, na próxima seção.

Satisfação implícita

Em Java, C# ou PHP, uma classe declara explicitamente quais interfaces implementa:

// Java
class PedidoRepositorySQL implements PedidoRepository {
    public Pedido buscar(String id) { ... }
}

Go não tem a palavra-chave implements. Um tipo satisfaz uma interface simplesmente por ter os métodos que ela exige — nada no código do tipo precisa mencionar a interface:

type PedidoRepository interface {
    Buscar(id string) (Pedido, error)
}

type PedidoRepositorySQL struct {
    db *sql.DB
}

// Nenhuma menção a "PedidoRepository" aqui — nada até este ponto
// conecta PedidoRepositorySQL à interface.
func (r *PedidoRepositorySQL) Buscar(id string) (Pedido, error) {
    // ...
    return Pedido{}, nil
}

Repare: até aqui, não existe nenhum vínculo escrito entre PedidoRepositorySQL e PedidoRepository. Nenhuma linha do código acima diz "estes dois têm relação". A checagem só acontece no momento em que alguém tenta usar um como se fosse o outro — por exemplo, ao guardar um PedidoRepositorySQL numa variável do tipo PedidoRepository:

// É AQUI que a checagem acontece — neste ponto de uso, não na declaração acima.
var repo PedidoRepository = &PedidoRepositorySQL{db: conexao}

É só nesta linha que o compilador olha para PedidoRepositorySQL, olha para o que PedidoRepository exige (um método Buscar com essa assinatura), confirma que bate, e permite a atribuição. Se PedidoRepositorySQL não tivesse esse método, ou tivesse com uma assinatura diferente, esta linha — e só esta linha — daria erro de compilação. Sem essa tentativa de uso, o compilador nunca teria motivo para comparar os dois tipos.

Isso é chamado de satisfação implícita (às vezes descrita como "duck typing" verificado em tempo de compilação — se anda como pato e grasna como pato, é um pato). "Implícito" descreve a declaração: o struct nunca precisa dizer, antecipadamente, quais interfaces satisfaz. A checagem em si é bem concreta e acontece em cada ponto de uso, como o da linha acima.

Quem declara a interface

Isso muda uma decisão que em outras linguagens é quase invisível: onde a interface deveria morar. Em Go, a convenção oficial é clara — e ela é exatamente a Regra de Dependência da Lição 1, só que agora com nome de arquivo e pacote.

"Go interfaces generally belong in the package that uses values of the interface type, not the package that implements those values." — Go Code Review Comments (wiki oficial mantido pela equipe do Go)

Ou seja: quem usa a interface a declara. Quem implementa não sabe, e não precisa saber, que a interface existe além de precisar ter os métodos certos. Volte ao exemplo: PedidoRepository deveria ser declarada no pacote do Use Case (quem chama Buscar), não no pacote que fala com o Postgres (quem implementa Buscar).

// pacote usecase — dono da interface, porque é quem a USA
package usecase

type PedidoRepository interface {
    Buscar(id string) (Pedido, error)
}

type AprovarPedido struct {
    repo PedidoRepository
}

func NovoAprovarPedido(r PedidoRepository) *AprovarPedido {
    return &AprovarPedido{repo: r}
}

// pacote postgres — implementa, sem importar "usecase" para isso
package postgres

type PedidoRepositorySQL struct{ db *sql.DB }

func (r *PedidoRepositorySQL) Buscar(id string) (Pedido, error) {
    // ...
}

Repare: o pacote postgres nem precisa importar o pacote usecase para satisfazer a interface — ele só precisa ter o método certo. É a satisfação implícita que torna esse desacoplamento possível fisicamente, não só conceitualmente. Em Java, o pacote de infra seria obrigado a importar a interface para escrever implements PedidoRepository; em Go, essa obrigação simplesmente não existe.

Satisfação implícita entre os pacotes usecase e postgresO pacote usecase declara a interface PedidoRepository e a usa em AprovarPedido. O pacote postgres implementa PedidoRepositorySQL e a satisfaz estruturalmente, sem precisar importar o pacote usecase.pacote usecasePedidoRepositoryinterface, declarada aquiAprovarPedidousa PedidoRepositoryusapacote postgresPedidoRepositorySQLimplementa Buscar(id)satisfaz —sem import

O pacote usecase declara e usa PedidoRepository. O pacote postgres implementa PedidoRepositorySQL e satisfaz a interface estruturalmente — a seta de satisfação vai de postgres para a interface, mas nenhum import parte de postgres para usecase.

Por que isso facilita testes

A consequência prática: para testar AprovarPedido sem banco de dados, basta escrever qualquer struct com um método Buscar — não existe hierarquia de classes, nem precisa "implementar" nada formalmente:

type PedidoRepositoryFake struct {
    pedidos map[string]Pedido
}

func (r *PedidoRepositoryFake) Buscar(id string) (Pedido, error) {
    p, ok := r.pedidos[id]
    if !ok {
        return Pedido{}, errors.New("não encontrado")
    }
    return p, nil
}

// no teste:
fake := &PedidoRepositoryFake{pedidos: map[string]Pedido{"1": {ID: "1", Total: 500}}}
useCase := usecase.NovoAprovarPedido(fake)

PedidoRepositoryFake satisfaz PedidoRepository pela mesma regra estrutural — mesmo estando definida só dentro do arquivo de teste, longe de qualquer banco de dados real.

Uma pegadinha: nil não é sempre nil

Um detalhe que engana quem vem de outras linguagens: uma interface que guarda um ponteiro nulo não é igual a nil. Isso acontece porque, internamente, uma interface em Go guarda dois campos — o tipo concreto e o valor. Se o tipo está preenchido (mesmo que o valor seja um ponteiro nulo), a interface inteira não é nil.

func Buscar(id string) error {
    var erro *MeuErro = nil
    // ... erro nunca é preenchido neste caminho
    return erro // retorna um error NÃO nulo!
}

Isso surpreende porque erro "parece" nulo — mas o error retornado carrega o tipo *MeuErro, então o resultado comparado com nil dá falso. A correção é simples: retornar o literal nil diretamente, não uma variável tipada que é nula.

Prática guiada: desacople um pacote de notificação

Raciocínio antes do código

O pacote usecase representa a camada de Use Cases da Lição 1 — deveria ser uma camada interna, que não conhece detalhes técnicos externos. Ao importar infra/smtp diretamente, o Use Case passa a depender de um pacote de infraestrutura concreto: se amanhã a equipe trocar o provedor de e-mail, ou passar a notificar por SMS além de e-mail, será necessário alterar o arquivo do Use Case — exatamente o tipo de mudança que a Regra de Dependência deveria isolar na camada externa.

Uma abordagem plausível, mas incorreta: criar a interface no lugar errado

package smtp

type Notificador interface {
    Notificar(destino, mensagem string) error
}

package usecase

import "meuapp/infra/smtp"

type AprovarPedido struct {
    repo        PedidoRepository
    notificador smtp.Notificador
}

Por que falha: tecnicamente há uma interface agora, mas ela está declarada no lugar errado — no pacote de infraestrutura, não no pacote que a usa. usecase continua importando infra/smtp, só que agora para referenciar o tipo smtp.Notificador em vez do tipo concreto. O acoplamento estrutural não mudou: se infra/smtp for removido ou renomeado, usecase quebra do mesmo jeito. Essa é a violação mais comum e mais sutil da convenção "quem usa, declara" — parece resolvido porque existe uma interface, mas a direção da dependência continua errada.

A solução correta: interface declarada em usecase

package usecase

// Notificador pertence a quem usa, não a quem implementa.
type Notificador interface {
    Notificar(destino, mensagem string) error
}

type AprovarPedido struct {
    repo        PedidoRepository
    notificador Notificador
}

func NovoAprovarPedido(r PedidoRepository, n Notificador) *AprovarPedido {
    return &AprovarPedido{repo: r, notificador: n}
}

func (a *AprovarPedido) Executar(id string) error {
    pedido, err := a.repo.Buscar(id)
    if err != nil {
        return err
    }
    pedido.Aprovado = true
    if err := a.repo.Salvar(pedido); err != nil {
        return err
    }
    return a.notificador.Notificar(pedido.ClienteEmail, "Seu pedido foi aprovado!")
}

// pacote infra/smtp — implementa sem importar usecase
package smtp

type Cliente struct{ /* config SMTP */ }

func (c *Cliente) Notificar(destino, mensagem string) error {
    // envia e-mail de verdade
    return nil
}

Por que está correto: usecase não importa mais infra/smtp em lugar nenhum — a última linha do arquivo confirma isso. smtp.Cliente satisfaz usecase.Notificador estruturalmente, só por ter o método Notificar com a assinatura certa. Testar AprovarPedido.Executar agora dispensa qualquer servidor SMTP: basta um fake com um método Notificar que grava numa lista em memória. Trocar o provedor de notificação (e-mail → SMS, ou um serviço terceirizado) exige só uma nova implementação de Notificador em outro pacote de infraestrutura — o Use Case não muda.

Verificação: escreva um teste com um fake notificador que apenas registra as chamadas recebidas, rode go test ./..., e confirme que o pacote usecase compila e testa sem que infra/smtp seja sequer mencionado no arquivo de teste. Se o teste exigir configurar um servidor SMTP de qualquer forma, a extração não está completa.

Perguntas de entrevista e de mercado

"O que é 'satisfação implícita' de interfaces em Go, e por que a linguagem foi desenhada assim?" Defina primeiro o mecanismo — um tipo satisfaz uma interface só por ter os métodos exigidos, sem palavra-chave como implements — e depois explique o "porquê": satisfação implícita permite que um pacote satisfaça uma interface declarada em outro pacote sem precisar importá-lo, o que possibilita a inversão de dependência ser expressa fisicamente no código, não só conceitualmente. Isso também permite adaptar tipos já existentes (inclusive de bibliotecas de terceiros) a interfaces novas, sem tocar no código-fonte desses tipos.

"Onde você declararia uma interface num projeto Go novo: junto à implementação, ou junto a quem consome?" A resposta esperada é "junto a quem consome", citando a convenção oficial da Go Code Review Comments. Uma resposta forte vai além e explica a consequência prática: se a interface mora junto à implementação (hábito comum de quem vem de Java), o pacote de infraestrutura acumula interfaces que ele nunca usa sozinho, e qualquer consumidor novo precisa importar o pacote de infraestrutura só para referenciar o tipo da interface — recriando o acoplamento que a interface deveria evitar.

"Explique a pegadinha do 'nil que não é nil' em Go, e como você evitaria esse bug em produção." Explique a representação interna de uma interface (par tipo+valor) antes de mostrar o código do bug — isso demonstra que você entende a causa, não só decorou o sintoma. Para evitar: nunca declare uma variável de erro tipada como ponteiro concreto e a retorne diretamente, prefira o literal nil quando não há erro; use go vet e linters como staticcheck; evite variáveis intermediárias de tipo concreto de erro em funções que retornam error.

"Quando você NÃO criaria uma interface em Go, mesmo tendo múltiplas implementações possíveis no futuro?" Esta pergunta testa se você entende o princípio idiomático "aceite interfaces, retorne structs" e evita over-engineering. Uma resposta madura cita a orientação da Go Code Review Comments: não crie uma interface só especulativamente, "porque no futuro pode ter outra implementação" — crie quando já existe uma necessidade real de desacoplamento. Interfaces prematuras em Go têm custo: adicionam indireção, dificultam navegar o código, e frequentemente ficam com uma única implementação para sempre.

Recupere da memória: 10 questões

Recupere da memória: 10 questões

Em Go, onde uma interface como PedidoRepository deveria ser declarada?

Em Go, quando exatamente o compilador verifica se um tipo satisfaz uma interface?

Qual é a principal diferença entre como Java e Go lidam com a implementação de interfaces?

No exemplo com os pacotes usecase e postgres desta aula, o pacote postgres precisa importar o pacote usecase para satisfazer a interface PedidoRepository?

Por que uma função que retorna error com um ponteiro tipado nulo resulta num erro 'não nulo' para quem chama?

Qual afirmação descreve corretamente a diferença entre struct e interface em Go?

Uma mesma struct em Go pode satisfazer mais de uma interface ao mesmo tempo?

Segundo o critério de organização de pacotes citado nesta aula (Dave Cheney), qual pergunta deveria guiar o design de um pacote em Go?

Segundo a Go Code Review Comments, citada nesta aula, quando NÃO vale a pena criar uma interface?

Qual princípio idiomático de Go orienta quando criar uma interface para um tipo de retorno de função?

Feche o modelo mental

Interfaces em Go são satisfeitas estruturalmente, não declaradas explicitamente — e é essa peculiaridade que torna a Regra de Dependência da Lição 1 fisicamente possível: o pacote de infraestrutura nunca precisa importar o pacote que declara a interface que ele satisfaz. A convenção "quem usa, declara" é a mesma regra, só que expressa em nome de pacote. Guarde também a pegadinha do nil: um detalhe pequeno de implementação de interfaces que causa bugs reais em produção quando ignorado.

Próxima aula planejada: Módulos e pacotes em Go — go.mod, a relação entre diretório e pacote, e o diretório internal.

Referências primárias para aprofundar

Consulte também o glossário desta trilha.