Tecnologias/01 - Linguagens/01 - Go6 min
Erros, Interfaces e o Estilo Go
Conceito
Duas decisões dão ao código Go a cara que ele tem. As duas são sobre tornar visível o que outras linguagens escondem.
1. Erro é valor, não fluxo de controle
error é só uma interface de um método:
type error interface {
Error() string
}
Erros esperados costumam aparecer como error no retorno. Go permite descartar
resultados, inclusive ignorar o retorno de uma chamada; o compilador não obriga a
tratar todo erro. Revisão, testes e análise estática complementam o contrato.
Referência: chamadas usadas como instruções.
O ferramental moderno é o wrapping, que constrói uma cadeia de causa:
if err != nil {
return fmt.Errorf("buscar usuário %d: %w", id, err) // %w preserva a causa
}
E do outro lado você inspeciona por identidade ou por tipo, nunca por texto:
var ErrNaoEncontrado = errors.New("não encontrado") // erro sentinela
errors.Is(err, ErrNaoEncontrado) // é este erro (ou embrulha ele)?
errors.Is(err, sql.ErrNoRows) // idem, atravessando o wrapping
var pgErr *pgconn.PgError
if errors.As(err, &pgErr) && pgErr.Code == "23505" { // violação de unique
...
}
Comparar
err.Error() == "não encontrado"é a versão Go decatch (Exception e): compila, passa nos testes e quebra quando alguém melhorar a mensagem.
panic não é exceção. panic é para bug — índice fora do slice, invariante
violada, configuração impossível na inicialização. Erro esperado (arquivo não existe,
rede caiu, validação falhou) é error. A única exceção pragmática é recover na
fronteira de um servidor, para que um bug num handler não derrube o processo inteiro —
que é exatamente o que o net/http já faz por você.
2. Interface é implícita e pequena
Você não declara que implementa uma interface. Se o método existe com a assinatura certa, o tipo satisfaz — verificado em tempo de compilação:
type Repositório interface { // definida por QUEM CONSOME
BuscarUsuário(ctx context.Context, id int) (*Usuário, error)
}
Isso inverte a dependência sem framework, sem anotação e sem injeção mágica: o pacote de serviço declara o que precisa; o pacote de Postgres apenas acontece de atender. Ver 3. Interfaces.
Daí saem duas máximas do estilo Go:
- "Accept interfaces, return structs." Receba o mínimo que você usa; devolva o tipo concreto, para o chamador não perder informação.
- Quanto menor, mais útil.
io.Readertem um método e é a interface mais reutilizada da história da linguagem. Uma interface com 15 métodos é uma classe abstrata com outro nome.
E a composição substitui a herança por embedding:
type ServiçoBase struct{ log *slog.Logger }
func (s ServiçoBase) Log() *slog.Logger { return s.log }
type ServiçoPedidos struct {
ServiçoBase // embedding: promove os métodos, NÃO cria subtipo
repo Repositório
}
ServiçoPedidos ganha Log(), mas não é um ServiçoBase — não há polimorfismo por
hierarquia, só por interface. Ver 4. Composição vs Herança.
Trade-offs
| Ganho | Preço |
|---|---|
| Caminho de erro visível na leitura linear | if err != nil ocupa ~30% das linhas |
| Sem stack trace implícito, sem custo escondido | você precisa construir o contexto no wrapping, ou perde a trilha |
| Interface implícita = zero acoplamento ao contrato | achar "quem implementa isto" exige ferramenta, não grep |
| Sem herança = sem hierarquia frágil | reuso de comportamento fica mais verboso |
error é só uma interface |
qualquer coisa pode ser um erro; sem tipos-soma, o compilador não exige que você trate os casos |
O ponto mais sentido na prática é o stack trace. Em Java você recebe o rastro de graça.
Em Go, um err que sobe 8 camadas sem wrapping chega como "EOF" — verdadeiro e
inútil. A disciplina compensa: embrulhe com o que você sabe e o chamador não sabe
(qual id, qual operação), nunca com o que ele já sabe.
O trade-off central: Go trocou "tratar erro é opcional e invisível" por "tratar erro fica explícito no retorno". O ruído é o preço combinado — o que não é aceitável é pagar o ruído e escrever
if err != nil { return err }sem contexto, porque aí você perdeu os dois lados.
Exemplo prático
Erros de domínio atravessando camadas com contexto, e o handler HTTP traduzindo para status — o padrão que substitui a árvore de exceções:
// domínio: erros sentinela, sem saber nada de HTTP ou de banco
var (
ErrNaoEncontrado = errors.New("recurso não encontrado")
ErrConflito = errors.New("conflito de estado")
)
// repositório: traduz o erro do driver para o erro do domínio
func (r *PgRepo) BuscarUsuário(ctx context.Context, id int) (*Usuário, error) {
var u Usuário
err := r.db.QueryRowContext(ctx, `SELECT id, email FROM usuarios WHERE id = $1`, id).
Scan(&u.ID, &u.Email)
switch {
case errors.Is(err, sql.ErrNoRows):
return nil, fmt.Errorf("usuário %d: %w", id, ErrNaoEncontrado)
case err != nil:
return nil, fmt.Errorf("consultar usuário %d: %w", id, err)
}
return &u, nil
}
// transporte: o ÚNICO lugar que conhece códigos HTTP
func status(err error) int {
switch {
case errors.Is(err, ErrNaoEncontrado):
return http.StatusNotFound
case errors.Is(err, ErrConflito):
return http.StatusConflict
case errors.Is(err, context.DeadlineExceeded):
return http.StatusGatewayTimeout
default:
return http.StatusInternalServerError
}
}
Repare no desenho: o sql.ErrNoRows não vaza para o handler, e o handler não
conhece Postgres. Se amanhã o repositório for MongoDB, só a função de
tradução muda. É a mesma lição de 2. Abstração e 1. Encapsulamento, obtida com
interface implícita em vez de herança.
Como isso falha em produção
- Erro engolido.
_ = json.NewEncoder(w).Encode(v)— a resposta sai truncada e nada é registrado. niltipado. Uma interface que guarda um ponteiro nulo não énil:
Nunca declare o retorno como tipo concreto de erro; declarefunc f() error { var e *MeuErro; return e } // e == nil, mas f() != nil if err := f(); err != nil { /* entra aqui! */ }error.deferno laço.defer rows.Close()dentro de umforsó executa no fim da função — em 10 mil iterações, 10 mil cursores abertos.- Wrapping sem contexto. 8 camadas de
return erre o log final diz"EOF". - Interface prematura. Uma interface por struct "para poder mockar" — o resultado é o dobro de arquivos e nenhum ganho. Ver 1. Code Smells.
Aprofundamento aplicado
Um ponteiro nil dentro de uma interface
type Falha struct{}
func (*Falha) Error() string { return "falha" }
func incorreto() error {
var f *Falha
return f
}
incorreto() == nil é falso: a interface contém um tipo dinâmico *Falha, mesmo com
valor ponteiro nil. Para representar ausência de erro, retorne nil explicitamente.
Esse problema aparece quando uma função muda de retorno concreto para error.
Referência: interfaces e nil.
Quem decide o que fazer com o erro
A camada de banco acrescenta contexto sobre a operação; a regra decide se a ausência é esperada; a borda HTTP traduz para uma resposta. Registrar a mesma falha em todas as camadas produz vários logs para um incidente e dificulta contar falhas reais.
Exercício: represente “pedido inexistente” com um erro identificável e envolva-o
com fmt.Errorf("buscar pedido %s: %w", id, err). Resultado esperado: errors.Is
continua reconhecendo a causa, mesmo após a mensagem ganhar contexto. A resposta pública
não precisa expor detalhes do driver.
Relacionado
Parte de MOC Go · go.dev/blog/error-handling-and-go