Interfaces em Go: Entenda de Verdade

“The bigger the interface, the weaker the abstraction.” – Rob Pike

Interfaces em Go são diferentes de qualquer outra linguagem. Não existem palavras-chave como implements ou extends. Em Go, interfaces são satisfeitas implicitamente: se um tipo tem os métodos que a interface exige, ele já a implementa. Ponto.

Essa decisão de design torna Go incrivelmente flexível para desacoplamento, testes e composição. Neste guia, vamos do conceito básico até padrões avançados usados em projetos reais.


O que é uma Interface?

Uma interface define um contrato de comportamento. Ela especifica quais métodos um tipo deve ter, mas não diz como implementá-los:

// Definição de interface: "qualquer coisa que saiba falar"
type Falante interface {
    Falar() string
}

Qualquer tipo com um método Falar() string satisfaz automaticamente essa interface:

// Cachorro implementa Falante (sem declarar isso explicitamente!)
type Cachorro struct {
    Nome string
}

func (c Cachorro) Falar() string {
    return c.Nome + " diz: Au au!"
}

// Gato também implementa Falante
type Gato struct {
    Nome string
}

func (g Gato) Falar() string {
    return g.Nome + " diz: Miau!"
}

// Papagaio também implementa Falante
type Papagaio struct {
    Nome  string
    Frase string
}

func (p Papagaio) Falar() string {
    return p.Nome + " repete: " + p.Frase
}

Agora podemos usar qualquer um deles onde se espera um Falante:

// cumprimentar aceita QUALQUER Falante
func cumprimentar(f Falante) {
    fmt.Println(f.Falar())
}

func main() {
    cumprimentar(Cachorro{Nome: "Rex"})
    cumprimentar(Gato{Nome: "Mimi"})
    cumprimentar(Papagaio{Nome: "Loro", Frase: "Oi, bonito!"})
}
// Saída:
// Rex diz: Au au!
// Mimi diz: Miau!
// Loro repete: Oi, bonito!

Note: em nenhum lugar escrevemos Cachorro implements Falante. O compilador verifica isso automaticamente. Essa é a implementação implícita.


Por que Implementação Implícita?

Na maioria das linguagens, você precisa declarar explicitamente quais interfaces um tipo implementa:

// Java — implementação explícita
class Cachorro implements Falante {
    public String falar() { return "Au au!"; }
}

Em Go, não. E isso traz vantagens enormes:

  1. Desacoplamento real: o tipo não precisa “saber” sobre a interface
  2. Interfaces podem ser definidas pelo consumidor: quem usa o código define a interface, não quem implementa
  3. Retroatividade: tipos existentes satisfazem interfaces criadas depois
  4. Sem hierarquias rígidas: não há herança de interfaces forçada

Interfaces da Biblioteca Padrão

A stdlib do Go é rica em interfaces pequenas e poderosas. Conhecê-las é essencial:

io.Reader e io.Writer

// io.Reader — qualquer coisa de onde se pode ler bytes
type Reader interface {
    Read(p []byte) (n int, err error)
}

// io.Writer — qualquer coisa para onde se pode escrever bytes
type Writer interface {
    Write(p []byte) (n int, err error)
}

Quem implementa io.Reader? Arquivos, conexões de rede, buffers, compressores, corpo de requisições HTTP e muito mais. Essa é a beleza:

package main

import (
    "fmt"
    "io"
    "os"
    "strings"
)

// contarBytes funciona com QUALQUER io.Reader
func contarBytes(r io.Reader) (int64, error) {
    return io.Copy(io.Discard, r)
}

func main() {
    // Funciona com string
    n, _ := contarBytes(strings.NewReader("Olá, Go!"))
    fmt.Printf("String: %d bytes\n", n)

    // Funciona com arquivo
    arquivo, _ := os.Open("main.go")
    defer arquivo.Close()
    n, _ = contarBytes(arquivo)
    fmt.Printf("Arquivo: %d bytes\n", n)

    // Funciona com stdin
    // n, _ = contarBytes(os.Stdin)
}

fmt.Stringer

// fmt.Stringer — controla como um tipo é impresso
type Stringer interface {
    String() string
}
type Moeda struct {
    Valor  float64
    Codigo string
}

// Implementa fmt.Stringer
func (m Moeda) String() string {
    return fmt.Sprintf("%s %.2f", m.Codigo, m.Valor)
}

func main() {
    preco := Moeda{Valor: 49.90, Codigo: "BRL"}
    fmt.Println(preco) // BRL 49.90 (usa String() automaticamente)
}

error

A interface mais famosa do Go:

type error interface {
    Error() string
}

Qualquer tipo com Error() string é um erro. Já exploramos isso em detalhes no guia de tratamento de erros.

sort.Interface

type Interface interface {
    Len() int
    Less(i, j int) bool
    Swap(i, j int)
}
type PorIdade []Pessoa

func (p PorIdade) Len() int           { return len(p) }
func (p PorIdade) Less(i, j int) bool { return p[i].Idade < p[j].Idade }
func (p PorIdade) Swap(i, j int)      { p[i], p[j] = p[j], p[i] }

func main() {
    pessoas := []Pessoa{
        {"Alice", 30},
        {"Bob", 25},
        {"Carol", 35},
    }
    sort.Sort(PorIdade(pessoas))
    // Bob (25), Alice (30), Carol (35)
}

Interface Vazia: any (interface{})

A interface vazia não tem nenhum método, portanto qualquer tipo a satisfaz:

// Antes do Go 1.18
var qualquerCoisa interface{}

// A partir do Go 1.18 (alias)
var qualquerCoisa any

Use com moderação. A interface vazia descarta a segurança de tipos:

func imprimir(valor any) {
    fmt.Printf("Tipo: %T, Valor: %v\n", valor, valor)
}

func main() {
    imprimir(42)           // Tipo: int, Valor: 42
    imprimir("texto")      // Tipo: string, Valor: texto
    imprimir(true)         // Tipo: bool, Valor: true
    imprimir([]int{1, 2})  // Tipo: []int, Valor: [1 2]
}

Quando usar any:

  • Funções genéricas como fmt.Println
  • Containers genéricos (antes dos generics do Go 1.18)
  • Decodificação de JSON dinâmico

Quando NÃO usar:

  • Quando você sabe o tipo: use o tipo concreto
  • Quando precisa de comportamento: use uma interface com métodos

Type Assertions: Recuperando o Tipo Concreto

Quando você tem um valor de interface, pode descobrir ou afirmar o tipo concreto:

// Type assertion básica
var f Falante = Cachorro{Nome: "Rex"}

// Afirma que f é um Cachorro
cachorro, ok := f.(Cachorro)
if ok {
    fmt.Println("É um cachorro:", cachorro.Nome)
} else {
    fmt.Println("Não é um cachorro")
}

// Type assertion sem verificação (PERIGOSO — causa panic se falhar!)
cachorro2 := f.(Cachorro) // panic se f não for Cachorro

Sempre use a forma com dois retornos (valor, ok) para evitar panics.

Type Switch: Múltiplos Tipos

O type switch é elegante para lidar com múltiplos tipos possíveis:

func descrever(i any) string {
    switch v := i.(type) {
    case int:
        return fmt.Sprintf("Inteiro: %d", v)
    case string:
        return fmt.Sprintf("String: %q (comprimento: %d)", v, len(v))
    case bool:
        if v {
            return "Booleano: verdadeiro"
        }
        return "Booleano: falso"
    case []int:
        return fmt.Sprintf("Slice de inteiros com %d elementos", len(v))
    case nil:
        return "Nulo"
    default:
        return fmt.Sprintf("Tipo desconhecido: %T", v)
    }
}

func main() {
    fmt.Println(descrever(42))           // Inteiro: 42
    fmt.Println(descrever("Go"))         // String: "Go" (comprimento: 2)
    fmt.Println(descrever(true))         // Booleano: verdadeiro
    fmt.Println(descrever([]int{1, 2}))  // Slice de inteiros com 2 elementos
}

Composição de Interfaces

Em Go, interfaces podem compor (embutir) outras interfaces:

// Interfaces pequenas e focadas
type Leitor interface {
    Ler(p []byte) (int, error)
}

type Escritor interface {
    Escrever(p []byte) (int, error)
}

type Fechador interface {
    Fechar() error
}

// Composição: combina múltiplas interfaces
type LeitorEscritor interface {
    Leitor
    Escritor
}

type LeitorFechador interface {
    Leitor
    Fechador
}

type LeitorEscritorFechador interface {
    Leitor
    Escritor
    Fechador
}

A biblioteca padrão usa esse padrão extensivamente:

// io.ReadWriter = io.Reader + io.Writer
type ReadWriter interface {
    Reader
    Writer
}

// io.ReadCloser = io.Reader + io.Closer
type ReadCloser interface {
    Reader
    Closer
}

// io.ReadWriteCloser = io.Reader + io.Writer + io.Closer
type ReadWriteCloser interface {
    Reader
    Writer
    Closer
}

Padrão: Aceite Interfaces, Retorne Structs

Este é um dos princípios mais importantes do design em Go:

// ✅ BOM: aceita interface (flexível)
func salvarDados(w io.Writer, dados []byte) error {
    _, err := w.Write(dados)
    return err
}

// ❌ RUIM: aceita tipo concreto (rígido)
func salvarDados(f *os.File, dados []byte) error {
    _, err := f.Write(dados)
    return err
}

A versão que aceita io.Writer funciona com arquivos, buffers, conexões de rede, HTTP responses e qualquer outra coisa que implemente Write. A que aceita *os.File só funciona com arquivos.

// ✅ BOM: retorna struct (concreto, informativo)
func NovoServidor(config Config) *Servidor {
    return &Servidor{config: config}
}

// ❌ RUIM: retorna interface (esconde o tipo real sem necessidade)
func NovoServidor(config Config) ServidorInterface {
    return &Servidor{config: config}
}

Padrão do Mundo Real: Repository Pattern

O Repository Pattern é perfeito para interfaces em Go:

// Defina a interface pelo consumidor (não pelo implementador)
type RepositorioUsuario interface {
    BuscarPorID(ctx context.Context, id int64) (*Usuario, error)
    BuscarPorEmail(ctx context.Context, email string) (*Usuario, error)
    Criar(ctx context.Context, u *Usuario) error
    Atualizar(ctx context.Context, u *Usuario) error
    Deletar(ctx context.Context, id int64) error
}

// Implementação com PostgreSQL
type postgresUsuarioRepo struct {
    db *sql.DB
}

func NovoPostgresUsuarioRepo(db *sql.DB) RepositorioUsuario {
    return &postgresUsuarioRepo{db: db}
}

func (r *postgresUsuarioRepo) BuscarPorID(ctx context.Context, id int64) (*Usuario, error) {
    var u Usuario
    err := r.db.QueryRowContext(ctx,
        "SELECT id, nome, email FROM usuarios WHERE id = $1", id,
    ).Scan(&u.ID, &u.Nome, &u.Email)
    if err == sql.ErrNoRows {
        return nil, ErrNaoEncontrado
    }
    return &u, err
}

func (r *postgresUsuarioRepo) BuscarPorEmail(ctx context.Context, email string) (*Usuario, error) {
    var u Usuario
    err := r.db.QueryRowContext(ctx,
        "SELECT id, nome, email FROM usuarios WHERE email = $1", email,
    ).Scan(&u.ID, &u.Nome, &u.Email)
    if err == sql.ErrNoRows {
        return nil, ErrNaoEncontrado
    }
    return &u, err
}

func (r *postgresUsuarioRepo) Criar(ctx context.Context, u *Usuario) error {
    _, err := r.db.ExecContext(ctx,
        "INSERT INTO usuarios (nome, email) VALUES ($1, $2)", u.Nome, u.Email,
    )
    return err
}

func (r *postgresUsuarioRepo) Atualizar(ctx context.Context, u *Usuario) error {
    _, err := r.db.ExecContext(ctx,
        "UPDATE usuarios SET nome=$1, email=$2 WHERE id=$3", u.Nome, u.Email, u.ID,
    )
    return err
}

func (r *postgresUsuarioRepo) Deletar(ctx context.Context, id int64) error {
    _, err := r.db.ExecContext(ctx,
        "DELETE FROM usuarios WHERE id = $1", id,
    )
    return err
}

// O serviço depende da INTERFACE, não da implementação
type ServicoUsuario struct {
    repo RepositorioUsuario // Interface, não *postgresUsuarioRepo
}

func NovoServicoUsuario(repo RepositorioUsuario) *ServicoUsuario {
    return &ServicoUsuario{repo: repo}
}

func (s *ServicoUsuario) ObterUsuario(ctx context.Context, id int64) (*Usuario, error) {
    return s.repo.BuscarPorID(ctx, id)
}

Testando com Interfaces: Mocks

A grande vantagem de depender de interfaces é a facilidade para testar:

// Mock do repositório para testes
type mockUsuarioRepo struct {
    usuarios map[int64]*Usuario
    erroBusca error
}

func novoMockRepo() *mockUsuarioRepo {
    return &mockUsuarioRepo{
        usuarios: make(map[int64]*Usuario),
    }
}

func (m *mockUsuarioRepo) BuscarPorID(ctx context.Context, id int64) (*Usuario, error) {
    if m.erroBusca != nil {
        return nil, m.erroBusca
    }
    u, existe := m.usuarios[id]
    if !existe {
        return nil, ErrNaoEncontrado
    }
    return u, nil
}

func (m *mockUsuarioRepo) BuscarPorEmail(ctx context.Context, email string) (*Usuario, error) {
    for _, u := range m.usuarios {
        if u.Email == email {
            return u, nil
        }
    }
    return nil, ErrNaoEncontrado
}

func (m *mockUsuarioRepo) Criar(ctx context.Context, u *Usuario) error {
    m.usuarios[u.ID] = u
    return nil
}

func (m *mockUsuarioRepo) Atualizar(ctx context.Context, u *Usuario) error {
    m.usuarios[u.ID] = u
    return nil
}

func (m *mockUsuarioRepo) Deletar(ctx context.Context, id int64) error {
    delete(m.usuarios, id)
    return nil
}

// Agora o teste usa o mock em vez do banco real
func TestObterUsuario(t *testing.T) {
    // Arrange
    mock := novoMockRepo()
    mock.usuarios[1] = &Usuario{ID: 1, Nome: "João", Email: "joao@email.com"}

    servico := NovoServicoUsuario(mock)

    // Act
    usuario, err := servico.ObterUsuario(context.Background(), 1)

    // Assert
    if err != nil {
        t.Fatalf("erro inesperado: %v", err)
    }
    if usuario.Nome != "João" {
        t.Errorf("nome esperado 'João', obteve '%s'", usuario.Nome)
    }
}

func TestObterUsuarioNaoEncontrado(t *testing.T) {
    mock := novoMockRepo()
    servico := NovoServicoUsuario(mock)

    _, err := servico.ObterUsuario(context.Background(), 999)
    if !errors.Is(err, ErrNaoEncontrado) {
        t.Errorf("esperava ErrNaoEncontrado, obteve: %v", err)
    }
}

Padrão Strategy com Interfaces

Interfaces permitem trocar algoritmos em tempo de execução:

// Interface de notificação
type Notificador interface {
    Enviar(destinatario string, mensagem string) error
}

// Implementação: Email
type NotificadorEmail struct {
    SMTPHost string
    SMTPPort int
}

func (n *NotificadorEmail) Enviar(destinatario, mensagem string) error {
    fmt.Printf("Enviando email para %s: %s\n", destinatario, mensagem)
    // Lógica real de envio de email
    return nil
}

// Implementação: SMS
type NotificadorSMS struct {
    APIKey string
}

func (n *NotificadorSMS) Enviar(destinatario, mensagem string) error {
    fmt.Printf("Enviando SMS para %s: %s\n", destinatario, mensagem)
    // Lógica real de envio de SMS
    return nil
}

// Implementação: Push notification
type NotificadorPush struct {
    AppID string
}

func (n *NotificadorPush) Enviar(destinatario, mensagem string) error {
    fmt.Printf("Enviando push para %s: %s\n", destinatario, mensagem)
    return nil
}

// Serviço que usa a interface (não sabe qual implementação vai receber)
type ServicoPedido struct {
    notificador Notificador
}

func (s *ServicoPedido) ConfirmarPedido(pedidoID int, clienteContato string) error {
    // Lógica de negócio...
    msg := fmt.Sprintf("Pedido #%d confirmado com sucesso!", pedidoID)
    return s.notificador.Enviar(clienteContato, msg)
}

func main() {
    // Fácil trocar a estratégia de notificação
    servicoEmail := &ServicoPedido{
        notificador: &NotificadorEmail{SMTPHost: "smtp.gmail.com", SMTPPort: 587},
    }
    servicoEmail.ConfirmarPedido(123, "joao@email.com")

    servicoSMS := &ServicoPedido{
        notificador: &NotificadorSMS{APIKey: "abc123"},
    }
    servicoSMS.ConfirmarPedido(456, "+5511999999999")
}

Armadilha: Pointer Receivers vs Value Receivers

Esta é uma das confusões mais comuns em Go:

type Contador struct {
    valor int
}

// Método com VALUE receiver
func (c Contador) Valor() int {
    return c.valor
}

// Método com POINTER receiver
func (c *Contador) Incrementar() {
    c.valor++
}

type Incrementavel interface {
    Incrementar()
    Valor() int
}

func main() {
    // ✅ FUNCIONA: ponteiro satisfaz interface com pointer receiver
    var i Incrementavel = &Contador{}
    i.Incrementar()
    fmt.Println(i.Valor()) // 1

    // ❌ NÃO COMPILA: valor NÃO satisfaz interface com pointer receiver
    // var i2 Incrementavel = Contador{} // ERRO de compilação!
    // cannot use Contador{} as Incrementavel: Contador does not implement Incrementavel
    // (method Incrementar has pointer receiver)
}

Regra: se qualquer método da interface usa pointer receiver (*T), apenas *T (ponteiro) satisfaz a interface. Valor T não satisfaz.

Métodos do tipoT satisfaz?*T satisfaz?
Todos com value receiverSimSim
Algum com pointer receiverNaoSim

Interfaces Pequenas: O Segredo do Design em Go

A filosofia do Go favorece interfaces pequenas e focadas. Compare:

// ❌ Interface grande e genérica (fraca abstração)
type Banco interface {
    Conectar() error
    Desconectar() error
    Consultar(query string) (*Resultado, error)
    Inserir(tabela string, dados map[string]any) error
    Atualizar(tabela string, id int, dados map[string]any) error
    Deletar(tabela string, id int) error
    IniciarTransacao() (*Transacao, error)
    Migrar() error
    Backup() error
}

// ✅ Interfaces pequenas e compostas (forte abstração)
type Consultor interface {
    Consultar(ctx context.Context, query string, args ...any) (*Resultado, error)
}

type Executor interface {
    Executar(ctx context.Context, query string, args ...any) (sql.Result, error)
}

// Compõe quando necessário
type ConsultorExecutor interface {
    Consultor
    Executor
}

As interfaces da stdlib seguem essa filosofia rigorosamente:

  • io.Reader: 1 método
  • io.Writer: 1 método
  • io.Closer: 1 método
  • fmt.Stringer: 1 método
  • error: 1 método
  • sort.Interface: 3 métodos (e já é considerada “grande”)

Conclusão

Interfaces em Go são fundamentalmente diferentes de outras linguagens, e essa diferença é proposital. A implementação implícita, combinada com a filosofia de interfaces pequenas, cria um sistema de tipos extraordinariamente flexível.

Pontos essenciais:

  • Implementação implícita: sem implements, o compilador verifica automaticamente
  • Interfaces pequenas: 1-3 métodos é o ideal
  • Aceite interfaces, retorne structs: maximiza flexibilidade e clareza
  • Defina interfaces pelo consumidor: quem usa define o contrato
  • Composição sobre herança: combine interfaces pequenas para criar maiores
  • Teste com mocks: interfaces tornam o código testável
  • Cuidado com pointer receivers: afetam qual tipo satisfaz a interface
  • Use any com moderação: prefira interfaces com métodos

Lembre-se do provérbio de Rob Pike: “The bigger the interface, the weaker the abstraction.” Mantenha suas interfaces pequenas, focadas e definidas onde são consumidas. Esse é o caminho para um design Go idiomático e sustentável.


Veja também