Golang

Interfaces: contratos implícitos e polimorfismo Já leu

9 min de leitura

Interfaces: contratos implícitos e polimorfismo
  Interfaces existem em Java, C#, TypeScript e diversas outras linguagens. Mas a implementação de interfaces em Go é radicalmente dif

 

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)
}

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!")
}

Resumo do que foi coberto

Este artigo apresentou interfaces em Go com profundidade: implementação implícita, a interface vazia any, type assertions, type switch, interfaces essenciais da biblioteca padrão, criação de erros personalizados, composição de interfaces, a armadilha do nil em interfaces e o princípio de interfaces pequenas. Com structs, métodos e interfaces dominados, o próximo artigo explora como Go usa composição para substituir herança.


Referências e leituras complementares

Comentários

Mais em Golang

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

Existe uma pergunta legítima que qualquer desenvolvedor deve fazer ant...

Maps: criação, iteração e boas práticas
Maps: criação, iteração e boas práticas

Se slices são a espinha dorsal das sequências em Go, maps s&atil...

A biblioteca padrão: um tour pelas principais ferramentas
A biblioteca padrão: um tour pelas principais ferramentas

Uma das razões pelas quais Go se tornou a linguagem preferida para inf...