Golang

O tipo `error` e tratamento idiomático de falhas Já leu

9 min de leitura

O tipo `error` e tratamento idiomático de falhas
A primeira coisa que surpreende programadores vindos de Python, Java ou C# ao estudar Go é a ausência de exceções. Não existe try, não existe catch, não existe throw. Em Go, erros são valores comuns — retornados por funç

A primeira coisa que surpreende programadores vindos de Python, Java ou C# ao estudar Go é a ausência de exceções. Não existe try, não existe catch, não existe throw. Em Go, erros são valores comuns — retornados por funções, armazenados em variáveis, passados como argumentos e inspecionados com if.

Essa escolha não é uma limitação. É uma decisão filosófica com consequências práticas muito positivas: o fluxo de erros se torna explícito, visível no código e auditável. Não existe a possibilidade de um erro "escapar" sem ser tratado pelo menos uma vez — o compilador obriga o programador a receber o valor de erro, mesmo que a decisão seja descartá-lo com _.


A interface error

O tipo error em Go é uma interface simples definida na biblioteca padrão:

type error interface {
    Error() string
}

Qualquer tipo que implemente o método Error() string é um error. Essa simplicidade é o que permite que erros sejam tão flexíveis — podem carregar qualquer informação adicional necessária além da mensagem.


Criando erros simples

O pacote errors da biblioteca padrão oferece a forma mais direta de criar um valor de erro:

package main

import (
    "errors"
    "fmt"
)

func dividir(a, b float64) (float64, error) {
    if b == 0 {
        return 0, errors.New("divisão por zero")
    }
    return a / b, nil
}

func main() {
    resultado, err := dividir(10, 2)
    if err != nil {
        fmt.Println("Erro:", err)
        return
    }
    fmt.Printf("Resultado: %.2f\n", resultado) // 5.00

    _, err = dividir(5, 0)
    if err != nil {
        fmt.Println("Erro:", err) // Erro: divisão por zero
    }
}

O valor nil como erro significa ausência de falha — a operação foi bem-sucedida. A convenção é sempre retornar nil quando não há erro.


Formatando erros com fmt.Errorf

O pacote fmt oferece fmt.Errorf para criar erros com mensagens formatadas, semelhante ao fmt.Sprintf:

func buscarUsuario(id int) (*Usuario, error) {
    if id <= 0 {
        return nil, fmt.Errorf("id inválido: %d", id)
    }
    // ...
    return nil, fmt.Errorf("usuário %d não encontrado", id)
}

Erros personalizados com contexto

Quando um erro precisa carregar informações estruturadas além de uma mensagem textual, cria-se um tipo personalizado que implementa a interface error:

package main

import "fmt"

type ErroValidacao struct {
    Campo   string
    Valor   any
    Motivo  string
}

func (e *ErroValidacao) Error() string {
    return fmt.Sprintf("validação falhou no campo '%s' (valor: %v): %s",
        e.Campo, e.Valor, e.Motivo)
}

type ErroHTTP struct {
    Codigo  int
    Metodo  string
    URL     string
}

func (e *ErroHTTP) Error() string {
    return fmt.Sprintf("HTTP %d ao chamar %s %s", e.Codigo, e.Metodo, e.URL)
}

func validarIdade(idade int) error {
    if idade < 0 {
        return &ErroValidacao{
            Campo:  "idade",
            Valor:  idade,
            Motivo: "não pode ser negativa",
        }
    }
    if idade > 150 {
        return &ErroValidacao{
            Campo:  "idade",
            Valor:  idade,
            Motivo: "valor improvável",
        }
    }
    return nil
}

func main() {
    if err := validarIdade(-5); err != nil {
        fmt.Println(err)

        // Inspecionando o tipo concreto
        if e, ok := err.(*ErroValidacao); ok {
            fmt.Printf("Campo problemático: %s\n", e.Campo)
        }
    }
}

Wrapping: embrulhando erros com contexto

Uma prática essencial em Go é adicionar contexto ao propagar erros pela pilha de chamadas. Em vez de retornar o erro original diretamente, embrulha-se ele com informações sobre o que estava acontecendo:

func lerArquivo(caminho string) ([]byte, error) {
    dados, err := os.ReadFile(caminho)
    if err != nil {
        return nil, fmt.Errorf("lerArquivo: falha ao ler %s: %w", caminho, err)
    }
    return dados, nil
}

func carregarConfig(caminho string) (*Config, error) {
    dados, err := lerArquivo(caminho)
    if err != nil {
        return nil, fmt.Errorf("carregarConfig: %w", err)
    }
    // ...
}

O verbo %w — disponível desde Go 1.13 — embrulha o erro original, preservando-o para inspeção posterior. O resultado é uma cadeia de erros com contexto acumulado:

carregarConfig: lerArquivo: falha ao ler config.json: open config.json: no such file or directory

Cada camada adiciona seu contexto, formando uma trilha que facilita o diagnóstico sem ocultar a causa raiz.


errors.Is: verificando erros específicos

Com erros embrulhados, verificar err == ErrEspecifico não funciona — o erro está embrulhado em outros. A função errors.Is percorre toda a cadeia de wrapping:

package main

import (
    "errors"
    "fmt"
    "os"
)

func lerConfig(caminho string) error {
    _, err := os.ReadFile(caminho)
    if err != nil {
        return fmt.Errorf("lerConfig: %w", err)
    }
    return nil
}

func main() {
    err := lerConfig("inexistente.json")

    if errors.Is(err, os.ErrNotExist) {
        fmt.Println("Arquivo não encontrado — usando configuração padrão")
    } else if err != nil {
        fmt.Println("Erro inesperado:", err)
    }
}

errors.Is verifica se algum erro na cadeia é igual ao alvo — tanto por comparação direta quanto pelo método Is(error) bool se o tipo o implementar.


errors.As: extraindo o tipo concreto da cadeia

Enquanto errors.Is verifica identidade, errors.As extrai o primeiro erro na cadeia que corresponde a um tipo específico:

package main

import (
    "errors"
    "fmt"
)

type ErroConexao struct {
    Host string
    Porta int
}

func (e *ErroConexao) Error() string {
    return fmt.Sprintf("falha ao conectar em %s:%d", e.Host, e.Porta)
}

func conectar(host string, porta int) error {
    return fmt.Errorf("iniciarServico: %w",
        &ErroConexao{Host: host, Porta: porta})
}

func main() {
    err := conectar("localhost", 5432)

    var erroConn *ErroConexao
    if errors.As(err, &erroConn) {
        fmt.Printf("Problema de conexão detectado\n")
        fmt.Printf("Host: %s\n", erroConn.Host)
        fmt.Printf("Porta: %d\n", erroConn.Porta)
    }
}

errors.As percorre a cadeia de wrapping e preenche a variável alvo com o erro encontrado, já com o tipo correto — sem necessidade de type assertion manual.


Erros sentinela: valores de erro pré-definidos

Erros sentinela são variáveis de erro exportadas que representam condições conhecidas. Permitem que os chamadores verifiquem condições específicas com errors.Is:

package main

import (
    "errors"
    "fmt"
)

// Erros sentinela exportados
var (
    ErrNaoEncontrado   = errors.New("registro não encontrado")
    ErrAcessoNegado    = errors.New("acesso negado")
    ErrDadosInvalidos  = errors.New("dados inválidos")
)

type BancoDados struct {
    dados map[int]string
}

func (db *BancoDados) Buscar(id int, usuarioAdmin bool) (string, error) {
    if !usuarioAdmin {
        return "", fmt.Errorf("Buscar id=%d: %w", id, ErrAcessoNegado)
    }
    valor, existe := db.dados[id]
    if !existe {
        return "", fmt.Errorf("Buscar id=%d: %w", id, ErrNaoEncontrado)
    }
    return valor, nil
}

func main() {
    db := &BancoDados{dados: map[int]string{1: "Ricardo", 2: "Ana"}}

    _, err := db.Buscar(1, false)
    fmt.Println(errors.Is(err, ErrAcessoNegado))   // true
    fmt.Println(errors.Is(err, ErrNaoEncontrado))  // false

    _, err = db.Buscar(99, true)
    fmt.Println(errors.Is(err, ErrNaoEncontrado))  // true
    fmt.Println(err) // Buscar id=99: registro não encontrado
}

Múltiplos erros: errors.Join

Desde Go 1.20, errors.Join permite combinar múltiplos erros em um único valor, útil para validações que precisam reportar todos os problemas de uma vez:

package main

import (
    "errors"
    "fmt"
)

type Formulario struct {
    Nome  string
    Email string
    Idade int
}

func (f Formulario) Validar() error {
    var erros []error

    if f.Nome == "" {
        erros = append(erros, errors.New("nome é obrigatório"))
    }
    if f.Email == "" {
        erros = append(erros, errors.New("email é obrigatório"))
    }
    if f.Idade < 18 {
        erros = append(erros, fmt.Errorf("idade mínima é 18, recebido: %d", f.Idade))
    }

    return errors.Join(erros...)
}

func main() {
    f := Formulario{Nome: "", Email: "", Idade: 15}

    if err := f.Validar(); err != nil {
        fmt.Println("Erros encontrados:")
        fmt.Println(err)
    }
}

panic e recover: para situações excepcionais

Go possui panic e recover, mas seu uso é reservado para situações verdadeiramente excepcionais — erros de programação irrecuperáveis, como índice fora dos limites ou desreferência de ponteiro nil. Não devem ser usados como substitutos para o fluxo normal de tratamento de erros.

panic interrompe a execução da goroutine atual e começa a desenrolar a pilha. recover pode capturar o pânico dentro de uma função defer:

package main

import "fmt"

func operacaoArriscada() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Pânico recuperado:", r)
        }
    }()

    var s []int
    fmt.Println(s[0]) // índice fora dos limites — causa pânico
}

func main() {
    operacaoArriscada()
    fmt.Println("Execução continua após recover")
}

A regra prática é clara: use error para falhas esperadas e recuperáveis. Reserve panic para condições que indicam bugs no código — estado corrompido, invariantes violadas, uso incorreto de APIs internas.


Boas práticas consolidadas

Adicione contexto ao propagar erros. Cada camada da aplicação deve adicionar informação relevante com %w:

return fmt.Errorf("processarPedido id=%d: %w", id, err)

Trate erros no nível correto. Um erro deve ser tratado — logado, convertido ou respondido ao usuário — apenas uma vez, na camada que tem contexto suficiente para fazer isso.

Não ignore erros com _ em código de produção. O compilador permite descartar erros com _, mas isso deve ser exceção extremamente rara com comentário explicando o motivo.

Erros sentinela para condições conhecidas. Defina variáveis de erro exportadas quando chamadores precisam distinguir condições específicas.

Tipos de erro para dados estruturados. Crie tipos personalizados quando o erro precisa carregar informações além da mensagem.


Resumo do que foi coberto

Este artigo percorreu o sistema de tratamento de erros do Go em profundidade: a interface error, criação com errors.New e fmt.Errorf, tipos de erro personalizados, wrapping com %w, inspeção com errors.Is e errors.As, erros sentinela, errors.Join para múltiplos erros e o papel restrito de panic e recover. Com esse módulo completo, o curso avança para organização de código com pacotes e módulos.


Referências e leituras complementares

Comentários

Mais em Golang

Artigo 41 — Linting, formatação e análise estática: gofmt, golangci-lint
Artigo 41 — Linting, formatação e análise estática: gofmt, golangci-lint

Artigo 41 — Linting, formatação e análise estática: gofmt, golangci-lint Qual...

Estudando e se aprofundando em Go
Estudando e se aprofundando em Go

Existe uma pergunta leg&iacute;tima que qualquer desenvolvedor deve fazer ant...

Artigo 29 — Roteamento avançado com Gorilla Mux ou Chi
Artigo 29 — Roteamento avançado com Gorilla Mux ou Chi

Artigo 29 — Roteamento avançado com Gorilla Mux ou Chi Quando a biblioteca pa...