Interfaces: contratos implícitos e polimorfismo

Interfaces: contratos implícitos e polimorfismo

Interfaces em Go: implementação implícita, any, type assertion e type switch, Stringer, error, io.Reader e io.Writer, errors.As para erros embrulhados, composição, o nil tipado que não é nil, a verificação em tempo de compilação e por que declarar a interface no pacote que a consome.
Golang

• • 15 min de leitura

 

Interfaces existem em Java, C#, TypeScript e diversas outras linguagens. Mas a implementação de interfaces em Go é radicalmente diferente: ela é implícita. Um tipo não precisa declarar que implementa uma interface. Se ele possui todos os métodos que a interface exige, ele a implementa automaticamente — sem herança, sem implements, sem nenhuma declaração formal.

Essa decisão de design tem consequências profundas. Ela desacopla completamente a definição de uma interface de sua implementação, permite que tipos de pacotes externos implementem interfaces definidas localmente, e torna o sistema de tipos extraordinariamente flexível sem sacrificar a segurança em tempo de compilação.

Definindo uma interface

Uma interface é um conjunto de assinaturas de métodos. Qualquer tipo que possua todos esses métodos satisfaz a interface:

type Forma interface {
    Area() float64
    Perimetro() float64
}

Nenhum tipo precisa declarar implements Forma. Se um tipo tem os métodos Area() float64 e Perimetro() float64, ele é uma Forma — automaticamente e sem qualquer declaração adicional.

Implementação implícita na prática

Criando dois tipos que satisfazem a interface Forma sem nunca mencionar a interface em suas definições:

package main

import (
    "fmt"
    "math"
)

type Forma interface {
    Area() float64
    Perimetro() float64
}

type Retangulo struct {
    Largura, Altura float64
}

func (r Retangulo) Area() float64 {
    return r.Largura * r.Altura
}

func (r Retangulo) Perimetro() float64 {
    return 2 * (r.Largura + r.Altura)
}

type Circulo struct {
    Raio float64
}

func (c Circulo) Area() float64 {
    return math.Pi * c.Raio * c.Raio
}

func (c Circulo) Perimetro() float64 {
    return 2 * math.Pi * c.Raio
}

func descreverForma(f Forma) {
    fmt.Printf("Área: %.2f | Perímetro: %.2f\n", f.Area(), f.Perimetro())
}

func main() {
    r := Retangulo{Largura: 4, Altura: 3}
    c := Circulo{Raio: 5}

    descreverForma(r) // Área: 12.00 | Perímetro: 14.00
    descreverForma(c) // Área: 78.54 | Perímetro: 31.42
}

A função descreverForma aceita qualquer valor que satisfaça Forma. Ela não sabe — nem precisa saber — se está recebendo um Retangulo, um Circulo ou qualquer outro tipo futuro.

A interface vazia: interface{} e any

A interface vazia não exige nenhum método. Portanto, qualquer tipo a satisfaz. Em Go moderno, any é um alias para interface{} e é a forma preferida:

func imprimir(v any) {
    fmt.Println(v)
}

func main() {
    imprimir(42)
    imprimir("texto")
    imprimir(true)
    imprimir([]int{1, 2, 3})
}

any é útil quando um valor de tipo genuinamente desconhecido precisa ser armazenado ou passado. Porém, seu uso excessivo é um sinal de design impreciso — quando o tipo é conhecido, interfaces específicas são sempre preferíveis.

Type assertion: recuperando o tipo concreto

Quando se tem um valor de tipo interface e precisa acessar o tipo concreto subjacente, usa-se a type assertion:

var f Forma = Circulo{Raio: 3}

// Forma segura — com comma ok
if c, ok := f.(Circulo); ok {
    fmt.Println("É um círculo de raio:", c.Raio)
}

// Forma direta — causa pânico se o tipo não corresponder
c := f.(Circulo)
fmt.Println(c.Raio)

Sempre prefira a forma com ok a menos que a certeza sobre o tipo seja absoluta. A forma direta deve ser usada com cautela, pois um tipo incorreto causa pânico em tempo de execução.

Type switch: múltiplos tipos em um só bloco

Quando diferentes ações são necessárias para diferentes tipos concretos, o type switch é mais legível do que uma cadeia de type assertions:

package main

import "fmt"

func descrever(v any) {
    switch val := v.(type) {
    case int:
        fmt.Printf("Inteiro: %d (dobro: %d)\n", val, val*2)
    case string:
        fmt.Printf("String: %q (tamanho: %d)\n", val, len(val))
    case bool:
        fmt.Printf("Booleano: %t\n", val)
    case []int:
        fmt.Printf("Slice de int com %d elementos\n", len(val))
    case nil:
        fmt.Println("Valor nulo")
    default:
        fmt.Printf("Tipo desconhecido: %T\n", val)
    }
}

func main() {
    descrever(42)
    descrever("Go é elegante")
    descrever(true)
    descrever([]int{1, 2, 3})
    descrever(nil)
    descrever(3.14)
}

A variável val dentro de cada case já tem o tipo concreto correspondente — não é necessário fazer uma nova type assertion.

Interfaces da biblioteca padrão: o poder do design implícito

Algumas das interfaces mais importantes do Go são definidas na biblioteca padrão com apenas um ou dois métodos. Sua simplicidade é o que as torna universais.

fmt.Stringer — para representação textual:

type Stringer interface {
    String() string
}

error — a interface de erros do Go:

type error interface {
    Error() string
}

io.Reader e io.Writer — para leitura e escrita de dados:

type Reader interface {
    Read(p []byte) (n int, err error)
}

type Writer interface {
    Write(p []byte) (n int, err error)
}

Qualquer tipo que implemente Read pode ser usado onde um io.Reader é esperado — seja um arquivo em disco, uma conexão de rede, um buffer em memória ou um leitor de strings. Todo o sistema de I/O do Go é construído sobre essas duas interfaces simples.

Implementando a interface error

Criar tipos de erro personalizados é uma prática comum e poderosa em Go:

package main

import "fmt"

type ErroDivisao struct {
    Dividendo float64
    Divisor   float64
}

func (e *ErroDivisao) Error() string {
    return fmt.Sprintf("não é possível dividir %.2f por %.2f", e.Dividendo, e.Divisor)
}

func dividir(a, b float64) (float64, error) {
    if b == 0 {
        return 0, &ErroDivisao{Dividendo: a, Divisor: b}
    }
    return a / b, nil
}

func main() {
    resultado, err := dividir(10, 0)
    if err != nil {
        fmt.Println("Erro:", err)

        // Recuperando informações específicas do tipo de erro
        if e, ok := err.(*ErroDivisao); ok {
            fmt.Printf("Tentou dividir %.0f por %.0f\n", e.Dividendo, e.Divisor)
        }
        return
    }
    fmt.Println(resultado)
}

A asserção err.(*ErroDivisao) funciona aqui porque o erro chega intacto. Na prática, erros costumam ser embrulhados no caminho com contexto — fmt.Errorf("calculando média: %w", err) —, e aí a asserção falha, porque o tipo concreto passou a ser o do embrulho. A forma que atravessa os embrulhos, disponível desde o Go 1.13, é errors.As: com var alvo *ErroDivisao, a chamada errors.As(err, &alvo) devolve true e preenche alvo mesmo com o erro embrulhado, enquanto a asserção direta devolve false. O tratamento de erros ganha um artigo próprio mais adiante na série.

Composição de interfaces

Assim como structs podem embutir outras structs, interfaces podem embutir outras interfaces. Isso permite construir contratos maiores a partir de contratos menores:

type Leitor interface {
    Ler() ([]byte, error)
}

type Escritor interface {
    Escrever(dados []byte) error
}

type LeitorEscritor interface {
    Leitor
    Escritor
}

Um tipo que implementa tanto Ler quanto Escrever automaticamente satisfaz LeitorEscritor. A biblioteca padrão usa esse padrão extensivamente — io.ReadWriter, io.ReadWriteCloser e outros são composições de interfaces menores.

Interfaces e nil: uma armadilha importante

Um valor de interface em Go é internamente composto por dois campos: o tipo concreto e o ponteiro para o valor. Uma interface é nil apenas quando ambos são nil. Essa distinção cria uma armadilha clássica:

package main

import "fmt"

type MinhaInterface interface {
    Fazer()
}

type MeuTipo struct{}

func (m *MeuTipo) Fazer() {}

func retornarInterface(retornarNil bool) MinhaInterface {
    var p *MeuTipo  // ponteiro nil do tipo *MeuTipo
    if retornarNil {
        return nil  // interface genuinamente nil
    }
    return p  // interface NÃO nil — tem tipo, mas valor nil
}

func main() {
    i1 := retornarInterface(true)
    i2 := retornarInterface(false)

    fmt.Println(i1 == nil) // true
    fmt.Println(i2 == nil) // false — armadilha!
}

No segundo caso, a interface contém informação de tipo (*MeuTipo) mesmo que o valor seja nil. A verificação == nil retorna false. Para evitar esse problema, nunca retorne uma variável tipada como nil quando o retorno esperado é uma interface — retorne nil diretamente.

Interfaces pequenas são melhores

Um princípio central do design Go é que interfaces devem ser pequenas. A comunidade consolidou o ditado: "The bigger the interface, the weaker the abstraction." — Rob Pike.

Uma interface com dez métodos é difícil de satisfazer e difícil de substituir. Uma interface com um método é trivial de implementar, de testar com mocks e de compor com outras interfaces.

// Difícil de satisfazer — acoplamento alto
type ServicoCompleto interface {
    Criar(dados any) error
    Ler(id int) (any, error)
    Atualizar(id int, dados any) error
    Deletar(id int) error
    Listar() ([]any, error)
    Buscar(query string) ([]any, error)
    Exportar(formato string) ([]byte, error)
}

// Fácil de satisfazer — acoplamento baixo
type Criador interface {
    Criar(dados any) error
}

type Leitor interface {
    Ler(id int) (any, error)
}

Quando uma função precisa apenas criar registros, ela deve receber um Criador — não um ServicoCompleto. Isso facilita testes, mocks e substituição de implementações.

Exemplo completo: sistema de notificações

Consolidando os conceitos em um exemplo prático que demonstra polimorfismo real:

package main

import "fmt"

type Notificador interface {
    Enviar(destinatario, mensagem string) error
    Nome() string
}

type Email struct {
    Servidor string
}

func (e Email) Enviar(dest, msg string) error {
    fmt.Printf("[EMAIL via %s] Para: %s | %s\n", e.Servidor, dest, msg)
    return nil
}

func (e Email) Nome() string { return "Email" }

type SMS struct {
    Operadora string
}

func (s SMS) Enviar(dest, msg string) error {
    fmt.Printf("[SMS via %s] Para: %s | %s\n", s.Operadora, dest, msg)
    return nil
}

func (s SMS) Nome() string { return "SMS" }

type Push struct{}

func (p Push) Enviar(dest, msg string) error {
    fmt.Printf("[PUSH] Para: %s | %s\n", dest, msg)
    return nil
}

func (p Push) Nome() string { return "Push" }

type ServicoNotificacao struct {
    canais []Notificador
}

func (sn *ServicoNotificacao) AdicionarCanal(n Notificador) {
    sn.canais = append(sn.canais, n)
}

func (sn *ServicoNotificacao) NotificarTodos(dest, msg string) {
    for _, canal := range sn.canais {
        if err := canal.Enviar(dest, msg); err != nil {
            fmt.Printf("Erro no canal %s: %v\n", canal.Nome(), err)
        }
    }
}

func main() {
    servico := &ServicoNotificacao{}

    servico.AdicionarCanal(Email{Servidor: "smtp.exemplo.com"})
    servico.AdicionarCanal(SMS{Operadora: "Vivo"})
    servico.AdicionarCanal(Push{})

    servico.NotificarTodos("usuario@exemplo.com", "Seu pedido foi aprovado!")
}

Interfaces em Go descrevem o que um valor sabe fazer, e o tipo concreto as satisfaz sem pedir licença — é isso que permite a um *os.File, a um bytes.Buffer e a uma conexão de rede passarem pelo mesmo io.Reader. A liberdade tem dois cuidados de contrapartida: a verificação que o compilador só faz quando alguém atribui, e que a linha var _ Forma = (*T)(nil) antecipa, e o valor de interface que tem tipo mas guarda nil, que não é igual a nil. Quanto menor a interface, mais fácil satisfazê-la e substituí-la.

Fontes e leituras recomendadas

Exercícios

Exercício 1

Uma função validar(dados) error declara var e *ErroValidacao no início, preenche e só quando encontra problema e termina com return e. Os dados estão corretos, mas o chamador sempre entra no if err != nil e loga a mensagem de erro padrão. O que está acontecendo?

Ver resposta

✓ Resposta: É a armadilha do nil em interfaces, que o artigo mostra, no caso mais comum na vida real: o tipo error. Ao devolver e, um *ErroValidacao nulo, a função converte esse ponteiro num valor de interface que tem tipo (*ErroValidacao) e valor nil. Uma interface só é igual a nil quando os dois campos são nulos, então err != nil dá verdadeiro. Pior: se o método Error() não acessa campos, ele roda normalmente com o receptor nulo, e o log mostra uma mensagem plausível para um erro que não existe. Num teste, a função "sem erro" devolveu um valor para o qual e == nil é false e cuja impressão mostrava "divisão por zero". O go vet não aponta nada. A correção é nunca devolver a variável tipada: if e == nil { return nil } antes do retorno, ou, melhor ainda, retornar nil literal no caminho de sucesso e &ErroValidacao{...} direto no de falha, sem variável intermediária.

Exercício 2

Um pacote define a interface Forma e o tipo Quadrado, com Area() de receiver ponteiro. Tudo compila, até que um consumidor tenta var f Forma = Quadrado{L: 2}. Como o autor do pacote poderia ter descoberto o problema no próprio build, sem esperar um consumidor?

Ver resposta

✓ Resposta: Como a implementação é implícita, nada obriga o compilador a conferir se Quadrado satisfaz Forma até alguém fazer uma atribuição. A convenção para antecipar a verificação é uma declaração que existe só para isso: var _ Forma = (*Quadrado)(nil), que confere o ponteiro sem alocar nada, ou var _ Forma = Quadrado{}, que confere o valor. No caso do exercício, a segunda linha falha no build do próprio pacote com Quadrado does not implement Forma (method Area has pointer receiver), o que obriga o autor a decidir: ou documenta que o tipo deve ser usado como ponteiro e troca a verificação pela primeira forma, ou muda o receiver para valor. O identificador _ faz a variável não ocupar espaço nem gerar aviso de "não usada". É um custo de uma linha que transforma a implementação implícita, que o artigo apresenta como vantagem, em um contrato conferido pelo compilador.

Exercício 3

Um retângulo r := Ret{2, 3} é guardado em duas variáveis de interface: var f Forma = r e var g Forma = &r. Depois, r.L = 10. O que f.Area() e g.Area() devolvem, e o que isso ensina sobre guardar valores em interfaces?

Ver resposta

✓ Resposta: f.Area() devolve 6 e g.Area() devolve 30. Atribuir um valor a uma interface copia o valor: f guarda um Ret{2, 3} próprio, que nada mais altera, e a mudança em r não chega até ele. Já g guarda o ponteiro, e por isso enxerga r como ele é no momento da chamada. Na prática, isso decide o comportamento de coleções como o []Notificador do exemplo do artigo: se os canais forem adicionados por valor, cada um é uma cópia congelada, e reconfigurar a struct original depois não tem efeito nenhum sobre o serviço. Se o tipo tem estado que muda, como contadores, conexões ou configuração recarregável, ele deve entrar na interface como ponteiro. Se é um valor imutável, como uma forma geométrica ou uma cor, a cópia é exatamente o que se quer, porque ninguém consegue alterá-la por fora.

Exercício 4

Um cache genérico usa map[any]string e compara chaves do tipo any com ==. Com números e strings funciona; um dia alguém passa um []int como chave e o serviço cai. O compilador não reclamou. Por quê, e qual é o erro?

Ver resposta

✓ Resposta: Porque, para o compilador, any é comparável: dois valores any podem ser comparados com == e usados como chave de map, e var a, b any = 1, 1 seguido de a == b dá true. A comparação de verdade só acontece em tempo de execução, olhando o tipo dinâmico guardado dentro de cada interface, e é aí que um tipo não comparável aparece. Com var x, y any = []int{1}, []int{1}, a expressão x == y para o programa com runtime error: comparing uncomparable type []int, e inserir uma fatia como chave num map[any] produz o mesmo pânico. Nem o go vet percebe. É o preço de trocar um tipo concreto por any: verificações que seriam de compilação passam a ser de execução. A correção é restringir o tipo da chave. Com generics, func Cache[K comparable, V any] exige, no compilador, que a chave seja comparável; sem generics, a saída é converter a chave para uma string canônica antes de usá-la.

Exercício 5

Um serviço de pedidos depende de um repositório com doze métodos, e os testes precisam de um repositório falso. O time discute onde declarar a interface: no pacote do banco, junto da implementação, ou no pacote de pedidos, que a consome. O que o princípio de "interfaces pequenas" do artigo sugere, e por que a implementação implícita torna isso possível?

Ver resposta

✓ Resposta: Sugere declarar a interface no pacote que consome, com apenas os métodos que ele usa. Se o serviço de pedidos chama só BuscarPedido e SalvarPedido, a interface dele tem esses dois métodos e mais nada, e o falso dos testes precisa implementar dois métodos, não doze. Em linguagens com implements, isso seria inviável, porque o tipo do banco teria de declarar que implementa a interface de cada consumidor e passaria a importar todos eles. Em Go, o repositório real satisfaz a interface de dois métodos automaticamente, sem saber que ela existe e sem nenhuma importação no sentido inverso. É o que se costuma resumir como "aceite interfaces, devolva structs": o construtor do banco devolve o tipo concreto, com tudo o que ele sabe fazer, e cada consumidor recorta a fatia que interessa. A interface grande no pacote do banco, com doze métodos, costuma ser sinal de que ela foi escrita antes de existir um segundo implementador que justificasse sua existência.

Comentários

Mais em Golang

Leitura e escrita de arquivos com os pacotes os e bufio
Leitura e escrita de arquivos com os pacotes os e bufio

Arquivos em Go: ReadFile e WriteFile, Open, Create e OpenFile, bufio.Scanner e…

Ponteiros: conceito, uso e quando evitar
Ponteiros: conceito, uso e quando evitar

Ponteiros em Go: os operadores & e *, o nil que entra em pânico e o que não…

Estudando e se aprofundando em Go
Estudando e se aprofundando em Go

Por que aprender Go hoje: a linguagem de Docker e Kubernetes, o que ela…