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:
- Desacoplamento real: o tipo não precisa “saber” sobre a interface
- Interfaces podem ser definidas pelo consumidor: quem usa o código define a interface, não quem implementa
- Retroatividade: tipos existentes satisfazem interfaces criadas depois
- 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 tipo | T satisfaz? | *T satisfaz? |
|---|---|---|
| Todos com value receiver | Sim | Sim |
| Algum com pointer receiver | Nao | Sim |
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étodoio.Writer: 1 métodoio.Closer: 1 métodofmt.Stringer: 1 métodoerror: 1 métodosort.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
anycom 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
- Testes em Go — Use interfaces para criar mocks
- Tratamento de Erros — A interface error
- Microservices com Go — Interfaces em arquitetura limpa
- Go para Iniciantes — Fundamentos de Go