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:

FerramentaQuando usar
errors.NewErros simples e estáticos
fmt.Errorf com %wAdicionar contexto e preservar a cadeia
errors.IsVerificar se um erro específico está na cadeia
errors.AsExtrair um tipo de erro específico
errors.JoinCombinar múltiplos erros (Go 1.20+)
Sentinel errorsCondições conhecidas e comparáveis
Tipos personalizadosErros ricos com dados estruturados
panicApenas 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