Tratamento de Erros em Go: Guia Definitivo
“Errors are values.” – Rob Pike
Em Go, erros não são exceções. Não existe try/catch. Erros são valores comuns que você retorna, inspeciona e trata de forma explícita. Essa abordagem, que pode parecer verbosa no começo, é uma das maiores forças da linguagem: torna o fluxo de erro visível, previsível e impossível de ignorar acidentalmente.
Neste guia, vamos explorar tudo sobre tratamento de erros em Go, desde o básico até padrões avançados usados em produção.
A Interface error
Em Go, um erro é qualquer tipo que implemente a interface error:
// Definição da interface error (pacote builtin)
type error interface {
Error() string
}
Sim, é só isso. Qualquer tipo com um método Error() string é um erro válido em Go. Essa simplicidade é intencional e poderosa.
Criando Erros Simples
Com errors.New
A forma mais simples de criar um erro:
package main
import (
"errors"
"fmt"
)
func dividir(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("divisão por zero não é permitida")
}
return a / b, nil
}
func main() {
resultado, err := dividir(10, 0)
if err != nil {
fmt.Println("Erro:", err)
return
}
fmt.Println("Resultado:", resultado)
}
Com fmt.Errorf
Para erros com informações dinâmicas:
func buscarUsuario(id int) (*Usuario, error) {
// Simula busca no banco
if id <= 0 {
return nil, fmt.Errorf("ID inválido: %d (deve ser positivo)", id)
}
if id > 1000 {
return nil, fmt.Errorf("usuário com ID %d não encontrado", id)
}
return &Usuario{ID: id, Nome: "João"}, nil
}
O Padrão if err != nil
Este é o padrão mais fundamental em Go. Toda chamada que pode falhar retorna um erro que deve ser verificado:
func processarArquivo(caminho string) error {
// Abrir arquivo
arquivo, err := os.Open(caminho)
if err != nil {
return fmt.Errorf("erro ao abrir arquivo: %w", err)
}
defer arquivo.Close()
// Ler conteúdo
dados, err := io.ReadAll(arquivo)
if err != nil {
return fmt.Errorf("erro ao ler arquivo: %w", err)
}
// Processar dados
if err := processarDados(dados); err != nil {
return fmt.Errorf("erro ao processar dados: %w", err)
}
return nil // Sucesso!
}
Sim, é verboso. Mas cada erro tem contexto claro, e o fluxo é completamente explícito. Você nunca se pergunta “de onde veio esse erro?”
Wrapping: Encadeando Erros com %w
A partir do Go 1.13, o verbo %w permite “embrulhar” (wrap) erros, criando uma cadeia:
package main
import (
"errors"
"fmt"
"os"
)
// Erro original
var ErrArquivoNaoEncontrado = errors.New("arquivo não encontrado")
func lerConfig(caminho string) ([]byte, error) {
dados, err := os.ReadFile(caminho)
if err != nil {
// Wrapping: preserva o erro original com %w
return nil, fmt.Errorf("ao ler config '%s': %w", caminho, err)
}
return dados, nil
}
func inicializarApp() error {
dados, err := lerConfig("/etc/app/config.yaml")
if err != nil {
// Segundo nível de wrapping
return fmt.Errorf("falha na inicialização: %w", err)
}
_ = dados
return nil
}
func main() {
err := inicializarApp()
if err != nil {
fmt.Println(err)
// Saída: falha na inicialização: ao ler config '/etc/app/config.yaml':
// open /etc/app/config.yaml: no such file or directory
// A cadeia completa de erros aparece na mensagem!
}
}
Importante: %w vs %v
// %w — embrulha o erro (mantém a cadeia, permite Unwrap)
return fmt.Errorf("contexto: %w", err)
// %v — apenas formata a mensagem (PERDE a cadeia)
return fmt.Errorf("contexto: %v", err) // NÃO faça isso se quiser inspecionar depois
Regra: use %w quando quiser que o chamador possa inspecionar o erro original. Use %v apenas quando quiser esconder deliberadamente a causa interna (raro).
errors.Is: Comparando Erros na Cadeia
errors.Is verifica se um erro (ou qualquer erro na cadeia de wrapping) corresponde a um valor específico:
package main
import (
"database/sql"
"errors"
"fmt"
"io"
"os"
)
func buscarNoBanco(id int) (string, error) {
// Simula um erro de banco
err := sql.ErrNoRows
return "", fmt.Errorf("buscarNoBanco(id=%d): %w", id, err)
}
func obterUsuario(id int) (string, error) {
nome, err := buscarNoBanco(id)
if err != nil {
return "", fmt.Errorf("obterUsuario: %w", err)
}
return nome, nil
}
func main() {
_, err := obterUsuario(42)
if err != nil {
// errors.Is percorre TODA a cadeia de erros
if errors.Is(err, sql.ErrNoRows) {
fmt.Println("Usuário não encontrado")
// Trata como 404
} else {
fmt.Println("Erro inesperado:", err)
// Trata como 500
}
}
// Outro exemplo: verificar erro de arquivo
_, err = os.Open("/nao/existe")
if errors.Is(err, os.ErrNotExist) {
fmt.Println("Arquivo não existe")
}
// Verificar EOF
err = fmt.Errorf("leitura falhou: %w", io.EOF)
if errors.Is(err, io.EOF) {
fmt.Println("Fim do arquivo detectado (mesmo com wrapping)")
}
}
Nunca use == para comparar erros wrapped:
// ERRADO — não funciona com wrapping
if err == sql.ErrNoRows { ... }
// CORRETO — funciona com qualquer nível de wrapping
if errors.Is(err, sql.ErrNoRows) { ... }
errors.As: Extraindo Tipos de Erro
errors.As verifica se um erro na cadeia corresponde a um tipo específico e extrai seus dados:
package main
import (
"errors"
"fmt"
"net"
)
// Tipo de erro personalizado
type ErroValidacao struct {
Campo string
Mensagem string
Valor interface{}
}
func (e *ErroValidacao) Error() string {
return fmt.Sprintf("campo '%s': %s (valor: %v)", e.Campo, e.Mensagem, e.Valor)
}
func validarIdade(idade int) error {
if idade < 0 {
return &ErroValidacao{
Campo: "idade",
Mensagem: "não pode ser negativa",
Valor: idade,
}
}
if idade > 150 {
return &ErroValidacao{
Campo: "idade",
Mensagem: "valor improvável",
Valor: idade,
}
}
return nil
}
func processarFormulario(idade int) error {
if err := validarIdade(idade); err != nil {
return fmt.Errorf("processamento do formulário: %w", err)
}
return nil
}
func main() {
err := processarFormulario(-5)
if err != nil {
// errors.As extrai o tipo específico do erro
var erroVal *ErroValidacao
if errors.As(err, &erroVal) {
fmt.Printf("Erro de validação!\n")
fmt.Printf(" Campo: %s\n", erroVal.Campo)
fmt.Printf(" Mensagem: %s\n", erroVal.Mensagem)
fmt.Printf(" Valor: %v\n", erroVal.Valor)
// Pode retornar um 400 Bad Request com detalhes
}
}
// Exemplo com erro de rede
_, err = net.Dial("tcp", "servidor-inexistente:80")
if err != nil {
var dnsErr *net.DNSError
if errors.As(err, &dnsErr) {
fmt.Printf("Erro DNS: host '%s' não encontrado\n", dnsErr.Name)
fmt.Printf(" Temporário: %v\n", dnsErr.Temporary())
}
}
}
Sentinel Errors: Erros Conhecidos
Sentinel errors são erros globais pré-definidos que servem como “sentinelas” para condições específicas:
package main
import (
"errors"
)
// Defina sentinel errors no nível do pacote
var (
ErrNaoEncontrado = errors.New("recurso não encontrado")
ErrNaoAutorizado = errors.New("acesso não autorizado")
ErrJaExiste = errors.New("recurso já existe")
ErrEntradaInvalida = errors.New("entrada inválida")
ErrLimiteExcedido = errors.New("limite de requisições excedido")
)
// Sentinels da biblioteca padrão que você deve conhecer:
// io.EOF — fim do fluxo de dados
// sql.ErrNoRows — nenhuma linha retornada
// os.ErrNotExist — arquivo/diretório não existe
// os.ErrPermission — permissão negada
// context.Canceled — contexto cancelado
// context.DeadlineExceeded — timeout
Usando Sentinel Errors no Código
type RepositorioUsuario struct {
usuarios map[int]*Usuario
}
func (r *RepositorioUsuario) BuscarPorID(id int) (*Usuario, error) {
usuario, existe := r.usuarios[id]
if !existe {
return nil, fmt.Errorf("buscar usuário id=%d: %w", id, ErrNaoEncontrado)
}
return usuario, nil
}
func (r *RepositorioUsuario) Criar(u *Usuario) error {
if _, existe := r.usuarios[u.ID]; existe {
return fmt.Errorf("criar usuário id=%d: %w", u.ID, ErrJaExiste)
}
r.usuarios[u.ID] = u
return nil
}
// No handler HTTP, traduza erros de domínio para status HTTP
func handleBuscarUsuario(w http.ResponseWriter, r *http.Request) {
usuario, err := repo.BuscarPorID(id)
if err != nil {
switch {
case errors.Is(err, ErrNaoEncontrado):
http.Error(w, "Usuário não encontrado", http.StatusNotFound)
case errors.Is(err, ErrNaoAutorizado):
http.Error(w, "Não autorizado", http.StatusUnauthorized)
default:
http.Error(w, "Erro interno", http.StatusInternalServerError)
}
return
}
json.NewEncoder(w).Encode(usuario)
}
Erros Personalizados com Contexto Rico
Para erros complexos, crie tipos que carregam informações estruturadas:
package apierro
import "fmt"
// Codigo representa códigos de erro da aplicação
type Codigo string
const (
CodigoNaoEncontrado Codigo = "NAO_ENCONTRADO"
CodigoNaoAutorizado Codigo = "NAO_AUTORIZADO"
CodigoValidacao Codigo = "VALIDACAO"
CodigoInterno Codigo = "INTERNO"
CodigoConflito Codigo = "CONFLITO"
)
// ErroAPI é um erro rico com informações para a resposta HTTP
type ErroAPI struct {
Codigo Codigo `json:"codigo"`
Mensagem string `json:"mensagem"`
StatusHTTP int `json:"-"` // Não inclui no JSON
Detalhes map[string]string `json:"detalhes,omitempty"`
Interno error `json:"-"` // Erro original (não expõe ao cliente)
}
func (e *ErroAPI) Error() string {
if e.Interno != nil {
return fmt.Sprintf("[%s] %s: %v", e.Codigo, e.Mensagem, e.Interno)
}
return fmt.Sprintf("[%s] %s", e.Codigo, e.Mensagem)
}
// Unwrap permite que errors.Is/As inspecionem o erro interno
func (e *ErroAPI) Unwrap() error {
return e.Interno
}
// Construtores para cada tipo de erro
func NaoEncontrado(mensagem string) *ErroAPI {
return &ErroAPI{
Codigo: CodigoNaoEncontrado,
Mensagem: mensagem,
StatusHTTP: 404,
}
}
func NaoAutorizado(mensagem string) *ErroAPI {
return &ErroAPI{
Codigo: CodigoNaoAutorizado,
Mensagem: mensagem,
StatusHTTP: 401,
}
}
func Validacao(mensagem string, detalhes map[string]string) *ErroAPI {
return &ErroAPI{
Codigo: CodigoValidacao,
Mensagem: mensagem,
StatusHTTP: 400,
Detalhes: detalhes,
}
}
func Interno(mensagem string, causa error) *ErroAPI {
return &ErroAPI{
Codigo: CodigoInterno,
Mensagem: mensagem,
StatusHTTP: 500,
Interno: causa, // Armazena mas NÃO expõe ao cliente
}
}
Usando no Handler HTTP
func criarUsuarioHandler(w http.ResponseWriter, r *http.Request) {
var req CriarUsuarioRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
responderErro(w, apierro.Validacao("JSON inválido", nil))
return
}
// Validação
if req.Email == "" {
responderErro(w, apierro.Validacao("Campos obrigatórios faltando", map[string]string{
"email": "é obrigatório",
}))
return
}
usuario, err := servico.CriarUsuario(req)
if err != nil {
var apiErr *apierro.ErroAPI
if errors.As(err, &apiErr) {
responderErro(w, apiErr)
} else {
// Erro não esperado — loga e retorna 500 genérico
log.Printf("Erro inesperado: %v", err)
responderErro(w, apierro.Interno("Erro interno do servidor", err))
}
return
}
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(usuario)
}
// responderErro envia a resposta de erro formatada
func responderErro(w http.ResponseWriter, err *apierro.ErroAPI) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(err.StatusHTTP)
json.NewEncoder(w).Encode(err)
}
Panic vs Error: Quando Usar Cada Um
// ❌ NÃO use panic para erros esperados
func buscarUsuario(id int) *Usuario {
u, err := db.Query(...)
if err != nil {
panic(err) // ERRADO! Isso derruba o programa
}
return u
}
// ✅ Use error para situações esperadas
func buscarUsuario(id int) (*Usuario, error) {
u, err := db.Query(...)
if err != nil {
return nil, fmt.Errorf("buscar usuário: %w", err)
}
return u, nil
}
Use panic somente quando:
- Há um bug no programa (invariante violada)
- A inicialização falha de forma irrecuperável
- Uma condição “impossível” acontece
// Uso legítimo de panic: configuração obrigatória ausente
func deveConectar(url string) *sql.DB {
db, err := sql.Open("postgres", url)
if err != nil {
// Se não consegue conectar ao banco na inicialização,
// não faz sentido continuar
panic("banco de dados indisponível: " + err.Error())
}
return db
}
// Uso legítimo: switch que deveria ser exhaustivo
func statusHTTP(codigo apierro.Codigo) int {
switch codigo {
case apierro.CodigoNaoEncontrado:
return 404
case apierro.CodigoNaoAutorizado:
return 401
default:
// Se chegou aqui, esquecemos de tratar um caso
panic(fmt.Sprintf("código de erro não mapeado: %s", codigo))
}
}
Boas Práticas: Resumo
1. Sempre trate erros
// ERRADO — ignora o erro silenciosamente
resultado, _ := funcaoQueRetornaErro()
// CORRETO
resultado, err := funcaoQueRetornaErro()
if err != nil {
return fmt.Errorf("contexto: %w", err)
}
2. Adicione contexto ao propagar
// RUIM — não acrescenta nada
if err != nil {
return err
}
// BOM — adiciona contexto sobre o que estava acontecendo
if err != nil {
return fmt.Errorf("ao salvar pedido #%d: %w", pedido.ID, err)
}
3. Trate o erro uma vez
// ERRADO — loga E retorna (quem receber vai logar de novo)
if err != nil {
log.Printf("erro: %v", err)
return err
}
// CORRETO — ou loga, ou retorna (não os dois)
if err != nil {
return fmt.Errorf("processar pedido: %w", err)
}
// OU (no topo da cadeia)
if err != nil {
log.Printf("Erro ao processar pedido: %v", err)
// Trata o erro (ex: retorna 500 ao cliente)
}
4. Erros devem ser informativos
// RUIM
return errors.New("falhou")
// BOM
return fmt.Errorf("falha ao conectar no Redis (%s:%d): %w", host, porta, err)
5. Use sentinel errors para condições conhecidas
// Defina no nível do pacote
var ErrEmailDuplicado = errors.New("email já cadastrado")
// Use com errors.Is no chamador
if errors.Is(err, ErrEmailDuplicado) {
// Trate especificamente
}
6. Não exponha erros internos ao cliente
// ERRADO — vaza detalhes do banco para o cliente
http.Error(w, err.Error(), 500)
// "pq: duplicate key value violates unique constraint..."
// CORRETO — mensagem amigável para o cliente, log interno para o dev
log.Printf("Erro ao criar usuário: %v", err)
http.Error(w, "Erro ao criar usuário. Tente novamente.", 500)
Erros Múltiplos com errors.Join (Go 1.20+)
A partir do Go 1.20, errors.Join permite combinar múltiplos erros:
func validarProduto(p Produto) error {
var erros []error
if p.Nome == "" {
erros = append(erros, fmt.Errorf("nome é obrigatório"))
}
if p.Preco < 0 {
erros = append(erros, fmt.Errorf("preço não pode ser negativo"))
}
if p.Estoque < 0 {
erros = append(erros, fmt.Errorf("estoque não pode ser negativo"))
}
// Combina todos os erros em um só
return errors.Join(erros...)
}
func main() {
p := Produto{Nome: "", Preco: -10, Estoque: -5}
err := validarProduto(p)
if err != nil {
fmt.Println(err)
// nome é obrigatório
// preço não pode ser negativo
// estoque não pode ser negativo
}
}
Anti-Padrões: O Que NÃO Fazer
// 1. NÃO use strings para comparar erros
if err.Error() == "not found" { } // FRÁGIL! A mensagem pode mudar
// 2. NÃO crie erros com informações sensíveis
return fmt.Errorf("senha incorreta para o user %s: %s", email, senha) // VAZAMENTO!
// 3. NÃO use panic para fluxo de controle
func buscar(id int) Resultado {
panic("não encontrado") // Use error, não panic!
}
// 4. NÃO ignore erros com _
dados, _ := json.Marshal(obj) // E se falhar?
_, _ = fmt.Fprintf(w, "ok") // E se a conexão cair?
// 5. NÃO retorne nil quando deveria retornar error
func conectar() (*DB, error) {
// Algo deu errado mas retorna nil
return nil, nil // O chamador vai receber nil, nil e prosseguir com ponteiro nulo!
}
Conclusão
O tratamento de erros em Go é deliberadamente explícito. Não é verbosidade desnecessária, e sim clareza proposital. Cada if err != nil é uma declaração de que “sim, eu sei que isso pode falhar, e eu decidi como lidar com isso”.
Resumo das ferramentas disponíveis:
| Ferramenta | Quando usar |
|---|---|
errors.New | Erros simples e estáticos |
fmt.Errorf com %w | Adicionar contexto e preservar a cadeia |
errors.Is | Verificar se um erro específico está na cadeia |
errors.As | Extrair um tipo de erro específico |
errors.Join | Combinar múltiplos erros (Go 1.20+) |
| Sentinel errors | Condições conhecidas e comparáveis |
| Tipos personalizados | Erros ricos com dados estruturados |
panic | Apenas para bugs e falhas irrecuperáveis |
Domine essas ferramentas e seu código Go será robusto, debugável e pronto para produção.
Veja também
- Testes em Go — Teste seus custom errors
- API REST com Go — Error handling em APIs
- Concorrência em Go — Erros em goroutines
- Go Cheatsheet — Referência rápida de error handling