Accept interfaces, return structs

TL;DR

Um idioma central de Go, quase um mantra: funções recebem interfaces como parâmetro, mas retornam tipos concretos (structs, ponteiros para struct). Receber interface dá ao chamador liberdade de passar qualquer implementação — real, fake, mock — sem que a função precise saber qual. Retornar interface faz o oposto: esconde do chamador o que ele recebeu, tira a chance de ele usar campos e métodos extras do tipo concreto, e — pior — obriga quem escreve a função a decidir de antemão toda operação que o consumidor algum dia vai precisar. A assinatura idiomática é func NovoServico(dep Dependencia) *Servico: parâmetro genérico, retorno específico. A regra tem exceções documentadas (erros, iteradores, io.Reader de volta) — mas o padrão default é este, e ele é a base de por que Go favorece interfaces pequenas.

O cenário que expõe o problema

Imagine que você está escrevendo um serviço de notificação. Ele precisa enviar mensagens — por enquanto, por e-mail. Escreve isso, sem pensar muito em abstração:

type EmailSender struct {
    smtpHost string
}
 
func (e *EmailSender) Send(destino, msg string) error {
    // ... conecta no SMTP e envia
    return nil
}
 
type NotificationService struct {
    sender *EmailSender
}
 
func NovoServico(host string) *NotificationService {
    return &NotificationService{sender: &EmailSender{smtpHost: host}}
}
 
func (n *NotificationService) Notificar(destino, msg string) error {
    return n.sender.Send(destino, msg)
}

Funciona. Mas seis meses depois alguém pede notificação por SMS também. NotificationService.sender está amarrado a *EmailSender — trocar ou adicionar um segundo canal significa reescrever o struct, o construtor, e todo teste que hoje monta um NotificationService real batendo num SMTP de verdade (ou mockado com muito esforço, porque *EmailSender é um tipo concreto, sem ponto de substituição).

O problema não é o EmailSender em si — é que NotificationService depende do tipo concreto, não de “algo que sabe enviar mensagem”. A correção é pequena, mas muda a forma como a API inteira se comporta:

type Sender interface {
    Send(destino, msg string) error
}
 
type NotificationService struct {
    sender Sender
}
 
func NovoServico(sender Sender) *NotificationService {
    return &NotificationService{sender: sender}
}

NovoServico agora aceita uma interface — qualquer coisa com Send(string, string) error serve, seja *EmailSender, um futuro *SMSSender, ou um MockSender de teste que só grava chamadas numa slice. E NovoServico continua retornando um tipo concreto, *NotificationService — não interface{ Notificar(...) error }. Essa combinação — parâmetro abstrato, retorno concreto — é o idioma que dá nome a esta nota.

Por que não simetria? Retornar interface parece “mais consistente”

A tentação de quem vem de linguagens com programação por contrato mais formal (Java com suas interfaces explícitas, C# com suas abstractions) é achar que, se aceitar interface é bom, retornar interface deveria ser igualmente bom — simetria arquitetural. Go rejeita essa simetria, e a razão fica clara olhando o que o chamador perde:

func NovoServico(sender Sender) Notificador { // retorno interface — o que evitar
    return &NotificationService{sender: sender}
}

Se NovoServico devolve Notificador (uma interface), o chamador só enxerga os métodos que Notificador declarou — nada mais. Se *NotificationService mais tarde ganhar um método Historico() []string para inspecionar notificações já enviadas, ninguém que recebeu o valor como Notificador consegue chamá-lo sem um type assertion manual (svc.(*NotificationService)) — e essa conversão é exatamente o tipo de código frágil que a nota anterior avisou para tratar com cautela. A interface no retorno virou uma parede entre o chamador e a implementação real, sem necessidade nenhuma: o chamador não pediu essa parede, foi imposta.

flowchart LR
    subgraph certo["accept interfaces, return structs"]
        direction LR
        C1["chamador"] -->|"passa qualquer Sender"| C2["NovoServico(Sender) *NotificationService"]
        C2 -->|"devolve tipo concreto completo"| C3["chamador vê TODOS os métodos"]
    end
    subgraph errado["retorno em interface (evitar)"]
        direction LR
        E1["chamador"] -->|"passa qualquer Sender"| E2["NovoServico(Sender) Notificador"]
        E2 -->|"devolve só o que a interface declara"| E3["chamador vê SÓ os métodos da interface"]
    end

    style C2 fill:#4A90D9,color:#fff
    style E2 fill:#D0021B,color:#fff

Há um segundo custo, mais sutil: retornar interface obriga quem escreve a função a adivinhar de antemão toda operação que qualquer consumidor futuro vai precisar, e cravar isso na interface de retorno. Se a previsão falhar — e ela falha, porque requisitos mudam — a correção exige alterar a assinatura pública da interface, o que é uma mudança breaking para todo mundo que já depende dela. Retornar o struct concreto adia essa decisão: o consumidor pega o tipo completo e decide sozinho, no seu próprio código, que subconjunto de comportamento ele quer tratar como abstrato (declarando a própria interface local — assunto da nota 08).

Anatomia da assinatura idiomática

flowchart LR
    A["func NovoServico"] --> B["(dep Dependencia)"]
    B --> C["*Servico"]

    B -.->|"parâmetro: interface\n— o CHAMADOR decide a implementação"| B
    C -.->|"retorno: struct concreto\n— o CHAMADOR vê tudo"| C

    style B fill:#F5A623,color:#000
    style C fill:#4A90D9,color:#fff

A leitura de uma assinatura como func NovoServico(dep Dependencia) *Servico já entrega, sem olhar o corpo, o contrato inteiro: “esta função aceita qualquer coisa que satisfaça Dependencia, e devolve um *Servico de verdade, com todos os seus métodos disponíveis”. Compare com a assinatura equivalente em Java, onde a convenção do “programe para interfaces” empurra para o outro lado:

// Java — convenção comum: retorno também em interface
public interface Notificador {
    void notificar(String destino, String msg);
}
 
public Notificador criarServico(Sender sender) {
    return new NotificationServiceImpl(sender);
}

Em Java, esconder o tipo concreto de retorno atrás de uma interface é reforçado por padrões como Factory e por frameworks de DI que injetam por tipo de interface. Go não tem esse reforço cultural — a comunidade converge para o oposto, e a razão de fundo é a mesma que motivou interfaces implícitas na nota 01: em Go, quem consome um valor é quem decide quanta abstração precisa, não quem o produz.

Casos práticos

1. Construtor que aceita io.Writer, retorna struct concreto — o padrão mais comum na stdlib e em código idiomático:

package logger
 
import (
    "fmt"
    "io"
)
 
type Logger struct {
    saida  io.Writer
    prefix string
}
 
// Aceita io.Writer (interface) — qualquer destino serve:
// os.Stdout, um *bytes.Buffer em teste, um arquivo, uma conexão de rede.
func NovoLogger(saida io.Writer, prefix string) *Logger {
    return &Logger{saida: saida, prefix: prefix}
}
 
func (l *Logger) Info(msg string) {
    fmt.Fprintf(l.saida, "[%s] INFO: %s\n", l.prefix, msg)
}

Testar isso não exige mock nenhum de biblioteca externa — bytes.Buffer já satisfaz io.Writer:

func TestLoggerInfo(t *testing.T) {
    var buf bytes.Buffer
    log := NovoLogger(&buf, "app")
 
    log.Info("iniciado")
 
    if !strings.Contains(buf.String(), "iniciado") {
        t.Errorf("esperava mensagem no buffer, got %q", buf.String())
    }
}

2. Dependência trocável via interface pequena, retorno concreto com método extra, retomando o NotificationService:

type Sender interface {
    Send(destino, msg string) error
}
 
type NotificationService struct {
    sender    Sender
    enviados  int
}
 
func NovoServico(sender Sender) *NotificationService {
    return &NotificationService{sender: sender}
}
 
func (n *NotificationService) Notificar(destino, msg string) error {
    if err := n.sender.Send(destino, msg); err != nil {
        return fmt.Errorf("notificar %s: %w", destino, err)
    }
    n.enviados++
    return nil
}
 
// Método que só existe no tipo concreto — inacessível se NovoServico
// tivesse retornado uma interface no lugar de *NotificationService.
func (n *NotificationService) TotalEnviados() int {
    return n.enviados
}

Quem chama NovoServico recebe *NotificationService de verdade — TotalEnviados() está ali, sem type assertion, porque o retorno nunca escondeu o tipo:

svc := NovoServico(&EmailSender{smtpHost: "smtp.exemplo.com"})
svc.Notificar("ana@exemplo.com", "bem-vinda")
svc.Notificar("bob@exemplo.com", "bem-vindo")
fmt.Println(svc.TotalEnviados()) // 2

3. Fake de teste satisfazendo a mesma interface, sem framework de mock nenhum:

type SenderFake struct {
    Chamadas []string
}
 
func (s *SenderFake) Send(destino, msg string) error {
    s.Chamadas = append(s.Chamadas, destino+": "+msg)
    return nil
}
 
func TestNotificar(t *testing.T) {
    fake := &SenderFake{}
    svc := NovoServico(fake) // mesmo construtor, dependência trocada
 
    svc.Notificar("carla@exemplo.com", "oi")
 
    if len(fake.Chamadas) != 1 {
        t.Fatalf("esperava 1 chamada, got %d", len(fake.Chamadas))
    }
}

Nada disso precisaria de biblioteca de mocking, geração de código, ou anotação — SenderFake satisfaz Sender estruturalmente (assunto da nota 01), e o construtor idiomático func NovoServico(sender Sender) *NotificationService já foi projetado, desde o parâmetro, para aceitar essa troca.

slices e maps (Go 1.21+) seguem o mesmo idioma

Funções como slices.Sort recebem []E (um slice concreto, não uma interface — slices não são interface) e funções que aceitam algo iterável tendem a pedir func(E) bool ou tipos concretos específicos, não uma interface genérica de “coleção”. A regra “accept interfaces” não significa “toda entrada deveria ser interface” — significa: use interface onde variação de implementação é o ponto real, como em io.Writer. Onde não há variação a abstrair, tipo concreto é mais simples e mais rápido (sem indireção de interface, sem alocação em heap por causa de conversão).

Armadilhas comuns

Interface enorme no parâmetro anula o benefício

func NovoServico(dep DependenciaComVinteMetodos) *Servico não dá liberdade real ao chamador — poucas implementações vão satisfazer vinte métodos por acidente, e cada fake de teste precisa implementar todos, mesmo os que o Servico nunca chama. O idioma “accept interfaces” rende o máximo quando combinado com interfaces pequenas, ideal 1-3 métodos — assunto direto da próxima nota.

Retornar interface "para não vazar detalhe de implementação" costuma vazar mais, não menos

A intuição de esconder o tipo concreto por retorno de interface, achando que isso é “encapsulamento”, tem o efeito oposto do pretendido: força o pacote a manter uma interface pública sincronizada com tudo que o consumidor pode precisar, e qualquer método novo no tipo concreto fica inacessível até alguém lembrar de adicioná-lo à interface de retorno também. Retornar o struct (ou *struct) é, paradoxalmente, menos acoplamento a manter — o pacote consumidor decide sozinho, com sua própria interface local, o que quer tratar como abstrato.

Não confundir "aceitar interface" com "aceitar interface{}/any"

func Processar(v any) não é o idioma desta nota — é o oposto: perde toda informação de tipo em troca de flexibilidade máxima, e devolve o problema para type assertions dentro da função (assunto da nota 02). O idioma “accept interfaces” pede uma interface com método(s) definidosSender, io.Writer, fmt.Stringer — não o empty interface disfarçado de flexibilidade.

Vindo de outras linguagens

LinguagemConvenção comumDiferença do idioma Go
Java”Program to an interface, not an implementation” — geralmente aplicado a parâmetro e retornoGo aplica só ao parâmetro; retorno idiomático é o tipo concreto
C#Interfaces em toda camada de serviço + DI container resolvendo por tipo de interfaceGo não tem DI container padrão; a “injeção” é passar o valor concreto direto no construtor
PythonDuck typing dispensa interface explícita em qualquer direção — nem parâmetro nem retorno costumam declarar um “contrato” formalGo formaliza a interface no parâmetro (compilador verifica), mas mantém o retorno concreto — meio-termo entre disciplina estática e flexibilidade
TypeScriptinterface/type costumam anotar tanto entrada quanto saída de funções em código de bibliotecaGo evita anotar a saída com abstração, salvo os casos documentados (erro, io.Reader variável)

Como explicar em inglês

Go’s idiom is “accept interfaces, return structs”: function parameters should be interfaces — giving the caller freedom to pass any implementation, including test fakes — while return values should be concrete types, usually a pointer to struct. Returning an interface hides the concrete type’s full method set from the caller and forces the author to predict every operation a future consumer might need, baking that guess into a public contract that’s expensive to change later. The convention is a default, not an absolute: functions like errors.New legitimately return an interface (error) because the concrete type genuinely varies and callers never need to see it. Applied consistently, this idiom is what makes small, locally-declared interfaces — rather than large upfront service interfaces — the natural unit of abstraction in Go.

Termo PTTermo EN
aceitar interfaces, retornar structsaccept interfaces, return structs
tipo concretoconcrete type
interface de retornoreturn interface
esconder o tipo concretohide the concrete type
fake de testetest fake / test double
acoplamentocoupling
contrato públicopublic contract

O que vem a seguir

Este idioma só funde bem quando a interface aceita como parâmetro é pequena — o exemplo usou Sender com um único método e io.Writer, também com um único método, de propósito. A próxima nota mergulha nas duas interfaces mais reutilizadas da stdlib, io.Reader e io.Writer, para mostrar por que “menor é melhor” não é só estética — é o que torna qualquer tipo, de qualquer pacote, imediatamente compatível com toda a árvore de composição de I/O da linguagem.

Veja também

Fontes