Tratamento de Erros

Lição 10: Tratamento de Erros

Analogia do Mundo Real

Imagine que você vai a um restaurante para pedir comida. O garçom diz: "Desculpe, este prato esgotou hoje." — Esta é uma situação esperada; você pode pedir outra coisa, e a vida continua. Isso é error em Go.

Mas se a cozinha de repente pega fogo e todos devem evacuar imediatamente — Este é um desastre inesperado. Isso é panic em Go.

A filosofia de tratamento de erros do Go é direta: lidar graciosamente com problemas antecipados, e apenas travar para os não antecipados. Ao contrário de outras linguagens que misturam todas as exceções com try/catch, Go exige que você lide com cada erro de forma explícita e individual.


Conceitos Principais

Conceito Descrição
Interface error Tipo de erro embutido no Go com apenas um método: Error() string
Múltiplos valores de retorno Funções Go tipicamente retornam erros como último valor de retorno
errors.New Cria um erro de texto simples
fmt.Errorf Cria um erro formatado (pode envolver com %w)
errors.Is Verifica se uma cadeia de erros contém um erro específico
errors.As Extrai um tipo de erro específico de uma cadeia de erros
panic Dispara um panic em tempo de execução, travando o programa
recover Captura panic em defer para evitar travamento do programa
Encapsulamento de erro Envolver erros subjacentes com %w ou tipos personalizados para preservar contexto

Fluxograma de Tratamento de Erros

Função retorna erro
       │
       ▼
  err != nil ?
  ┌────┴────┐
  │ Sim     │ Não
  ▼         ▼
Tratar erro  Continuar
  │
  ├─ Recuperável → Log / Retornar padrão / Tentar novamente
  ├─ Precisa reportar → Envolvar e passar adiante
  └─ Irrecuperável → panic

Sintaxe e Uso Básico

1. Interface error

error é o tipo de interface embutido do Go, definido de forma muito concisa:

GO
// Definição da interface error (embutida, não precisa importar)
type error interface {
    Error() string
}

Qualquer tipo que implemente o método Error() string é um error.

2. Criando Erros

GO
package main

import (
    "errors"
    "fmt"
)

func main() {
    // Método 1: errors.New — criar um erro de texto simples
    err1 := errors.New("arquivo não encontrado")

    // Método 2: fmt.Errorf — criar um erro formatado
    nomeArquivo := "config.yaml"
    err2 := fmt.Errorf("falha ao ler arquivo %s", nomeArquivo)

    // Método 3: fmt.Errorf + %w — envolver erro subjacente (recomendado)
    errBase := errors.New("permissão negada")
    err3 := fmt.Errorf("não foi possível escrever log: %w", errBase)

    fmt.Println(err1) // arquivo não encontrado
    fmt.Println(err2) // falha ao ler arquivo config.yaml
    fmt.Println(err3) // não foi possível escrever log: permissão negada
}

3. Verificando Erros

GO
package main

import (
    "errors"
    "fmt"
)

var ErrNaoEncontrado = errors.New("registro não encontrado")

func encontrarUsuario(id int) (string, error) {
    if id <= 0 {
        return "", ErrNaoEncontrado
    }
    return "Alice", nil
}

func main() {
    nome, err := encontrarUsuario(0)
    if err != nil {
        // errors.Is verifica se a cadeia de erros contém o erro alvo
        if errors.Is(err, ErrNaoEncontrado) {
            fmt.Println("Usuário não encontrado, verifique o ID")
        } else {
            fmt.Println("Erro desconhecido:", err)
        }
        return
    }
    fmt.Println("Usuário encontrado:", nome)
}

4. Extraindo Tipos de Erro Específicos

GO
package main

import (
    "errors"
    "fmt"
)

// Tipo de erro personalizado
type ErroValidacao struct {
    Campo   string
    Mensagem string
}

func (e *ErroValidacao) Error() string {
    return fmt.Sprintf("falha na validação [%s]: %s", e.Campo, e.Mensagem)
}

func validarIdade(idade int) error {
    if idade < 0 || idade > 150 {
        return &ErroValidacao{
            Campo:   "idade",
            Mensagem: "idade deve estar entre 0 e 150",
        }
    }
    return nil
}

func main() {
    err := validarIdade(200)
    if err != nil {
        // errors.As extrai um tipo específico da cadeia de erros
        var ev *ErroValidacao
        if errors.As(err, &ev) {
            fmt.Printf("Campo: %s, Razão: %s\n", ev.Campo, ev.Mensagem)
        } else {
            fmt.Println("Outro erro:", err)
        }
    }
}

5. panic e recover

GO
package main

import "fmt"

// divisaoSegura usa recover para capturar panic
func divisaoSegura(a, b int) (resultado int, err error) {
    defer func() {
        if r := recover(); r != nil {
            err = fmt.Errorf("panic capturado: %v", r)
        }
    }()

    // Dividir por zero dispara um panic
    return a / b, nil
}

func main() {
    resultado, err := divisaoSegura(10, 0)
    if err != nil {
        fmt.Println("Erro:", err) // Erro: panic capturado: runtime error: integer divide by zero
        return
    }
    fmt.Println("Resultado:", resultado)
}
💡 Dica: Em Go, a grande maioria dos erros deve ser tratada retornando error, não panic. Use panic apenas quando o programa realmente não pode continuar (ex: falha de inicialização, erros de lógica irrecuperáveis).

💡 Dica: Ao envolver erros com fmt.Errorf, sempre use %w em vez de %v. %w preserva o erro original, permitindo que errors.Is e errors.As funcionem corretamente.

💡 Dica: Mensagens de erro devem ser frases em minúsculas sem pontuação final, pois frequentemente são concatenadas em mensagens de erro maiores.


Exemplos

Exemplo: Tratamento Básico de Erros (Dificuldade ⭐)

Simular um leitor simples de arquivo de configuração para demonstrar o padrão mais básico de retorno e verificação de erros.

GO
package main

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

// Definir erros sentinelas no nível do pacote
var (
    ErrArquivoNaoEncontrado = errors.New("arquivo de configuração não encontrado")
    ErrArquivoVazio         = errors.New("arquivo de configuração está vazio")
    ErrFormatoInvalido      = errors.New("formato de arquivo de configuração inválido")
)

// Config representa a configuração da aplicação
type Config struct {
    Host string
    Porta int
}

// CarregarConfig carrega configuração de um arquivo (simulado)
func CarregarConfig(caminho string) (*Config, error) {
    // Simular arquivo não encontrado
    if caminho == "" {
        return nil, ErrArquivoNaoEncontrado
    }

    // Simular leitura de arquivo
    dados, err := os.ReadFile(caminho)
    if err != nil {
        // Envolver erro subjacente, preservando informação original
        return nil, fmt.Errorf("lendo arquivo de configuração %s: %w", caminho, err)
    }

    // Verificar se arquivo está vazio
    if len(dados) == 0 {
        return nil, ErrArquivoVazio
    }

    // Simular parsing de configuração
    return &Config{
        Host: "localhost",
        Porta: 8080,
    }, nil
}

func main() {
    // Tentar carregar configuração
    config, err := CarregarConfig("app.conf")
    if err != nil {
        // Usar errors.Is para determinar tipo de erro específico
        switch {
        case errors.Is(err, ErrArquivoNaoEncontrado):
            fmt.Println("Erro: Por favor especifique o caminho do arquivo de configuração")
        case errors.Is(err, ErrArquivoVazio):
            fmt.Println("Erro: Arquivo de configuração está vazio, verifique o conteúdo")
        case errors.Is(err, ErrFormatoInvalido):
            fmt.Println("Erro: Formato do arquivo de configuração é inválido")
        default:
            fmt.Println("Erro:", err)
        }
        return
    }

    fmt.Printf("Configuração carregada com sucesso: %s:%d\n", config.Host, config.Porta)
}
▶ Experimente

Saída (quando arquivo não existe):

Erro: Por favor especifique o caminho do arquivo de configuração

Exemplo: Encapsulamento de Erro e Verificação em Cadeia (Dificuldade ⭐⭐)

Demonstrar encapsulamento de erro, propagação e verificação em cadeia em chamadas de função multi-camada.

GO
package main

import (
    "errors"
    "fmt"
)

// Definir erros de negócio
var (
    ErrUsuarioNaoEncontrado = errors.New("usuário não encontrado")
    ErrPermissao            = errors.New("permissões insuficientes")
)

// Representa erros da camada de banco de dados
type ErroBancoDeDados struct {
    Operacao string
    Tabela   string
    Err      error
}

func (e *ErroBancoDeDados) Error() string {
    return fmt.Sprintf("erro de banco de dados [%s.%s]: %e", e.Tabela, e.Operacao, e.Err)
}

func (e *ErroBancoDeDados) Unwrap() error {
    return e.Err
}

// RepositorioUsuario repositório de usuários
type RepositorioUsuario struct {
    usuarios map[int]string
}

// BuscarPorID encontra um usuário por ID
func (r *RepositorioUsuario) BuscarPorID(id int) (string, error) {
    nome, existe := r.usuarios[id]
    if !existe {
        // Envolver como erro da camada de banco de dados
        return "", &ErroBancoDeDados{
            Operacao: "SELECT",
            Tabela:   "usuarios",
            Err:      ErrUsuarioNaoEncontrado,
        }
    }
    return nome, nil
}

// Servico camada de serviço de negócio
type Servico struct {
    repo *RepositorioUsuario
}

// ObterUsuario obtém informações do usuário
func (s *Servico) ObterUsuario(id int, requerAdmin bool) (string, error) {
    nome, err := s.repo.BuscarPorID(id)
    if err != nil {
        // Envolver para cima, adicionando contexto de negócio
        return "", fmt.Errorf("obtendo informações do usuário (id=%d): %w", id, err)
    }

    if requerAdmin && nome != "admin" {
        return "", fmt.Errorf("usuário %s: %w", nome, ErrPermissao)
    }

    return nome, nil
}

func main() {
    repo := &RepositorioUsuario{
        usuarios: map[int]string{
            1: "admin",
            2: "Alice",
        },
    }
    svc := &Servico{repo: repo}

    // Cenários de teste
    casosTeste := []struct {
        nome         string
        id           int
        requerAdmin bool
    }{
        {"Encontrar admin", 1, false},
        {"Encontrar usuário comum", 2, false},
        {"Encontrar usuário inexistente", 99, false},
        {"Usuário comum acessando função admin", 2, true},
    }

    for _, ct := range casosTeste {
        fmt.Printf("--- %s ---\n", ct.nome)
        usuario, err := svc.ObterUsuario(ct.id, ct.requerAdmin)
        if err != nil {
            fmt.Printf("  Falhou: %v\n", err)

            // errors.Is pode percorrer a cadeia de erros para encontrar causa raiz
            if errors.Is(err, ErrUsuarioNaoEncontrado) {
                fmt.Println("  Ação: Solicitar ao usuário que verifique o ID")
            } else if errors.Is(err, ErrPermissao) {
                fmt.Println("  Ação: Solicitar ao usuário que contate o administrador")
            }

            // errors.As pode extrair tipos de erro específicos de camadas intermediárias
            var dbErr *ErroBancoDeDados
            if errors.As(err, &dbErr) {
                fmt.Printf("  Detalhes BD: tabela=%s, operação=%s\n", dbErr.Tabela, dbErr.Operacao)
            }
        } else {
            fmt.Printf("  Sucesso: Usuário %s\n", usuario)
        }
        fmt.Println()
    }
}
▶ Experimente

Saída:

--- Encontrar admin ---
  Sucesso: Usuário admin

--- Encontrar usuário comum ---
  Sucesso: Usuário Alice

--- Encontrar usuário inexistente ---
  Falhou: obtendo informações do usuário (id=99): erro de banco de dados [usuarios.SELECT]: usuário não encontrado
  Ação: Solicitar ao usuário que verifique o ID
  Detalhes BD: tabela=usuarios, operação=SELECT

--- Usuário comum acessando função admin ---
  Falhou: Usuário Alice: permissões insuficientes
  Ação: Solicitar ao usuário que contate o administrador

Exemplo: Tipos de Erro Personalizados e recover na Prática (Dificuldade ⭐⭐⭐)

Implementar um handler completo de requisição HTTP com tipos de erro personalizados, códigos de erro e um middleware de recuperação de panic.

GO
package main

import (
    "errors"
    "fmt"
    "runtime/debug"
)

// ==================== Sistema de Erro Personalizado ====================

// ErroApp erro de nível de aplicação com código de erro e contexto
type ErroApp struct {
    Codigo   int               // Código de erro de negócio
    Mensagem string            // Mensagem amigável ao usuário
    Detalhe  string            // Informação de debug para desenvolvedor
    Causa    error             // Causa subjacente
    Contexto map[string]string // Contexto adicional
}

func (e *ErroApp) Error() string {
    if e.Causa != nil {
        return fmt.Sprintf("[%d] %s: %v", e.Codigo, e.Mensagem, e.Causa)
    }
    return fmt.Sprintf("[%d] %s", e.Codigo, e.Mensagem)
}

func (e *ErroApp) Unwrap() error {
    return e.Causa
}

// Is implementa lógica de comparação de erro personalizada
func (e *ErroApp) Is(alvo error) bool {
    t, ok := alvo.(*ErroApp)
    if !ok {
        return false
    }
    return e.Codigo == t.Codigo
}

// NovoErroApp cria um novo erro de aplicação
func NovoErroApp(codigo int, mensagem string) *ErroApp {
    return &ErroApp{
        Codigo:   codigo,
        Mensagem: mensagem,
        Contexto: make(map[string]string),
    }
}

// ComCausa define a causa subjacente
func (e *ErroApp) ComCausa(err error) *ErroApp {
    e.Causa = err
    return e
}

// ComDetalhe define detalhes de debug
func (e *ErroApp) ComDetalhe(detalhe string) *ErroApp {
    e.Detalhe = detalhe
    return e
}

// ComContexto adiciona informação de contexto
func (e *ErroApp) ComContexto(chave, valor string) *ErroApp {
    e.Contexto[chave] = valor
    return e
}

// Códigos de erro predefinidos
var (
    ErrRequisicaoInvalida  = NovoErroApp(400, "requisição inválida")
    ErrNaoAutorizado       = NovoErroApp(401, "acesso não autorizado")
    ErrProibido            = NovoErroApp(403, "proibido")
    ErrNaoEncontrado       = NovoErroApp(404, "recurso não encontrado")
    ErrInterno             = NovoErroApp(500, "erro interno do servidor")
)

// ==================== Camada de Negócio Simulada ====================

// Requisicao simula uma requisição HTTP
type Requisicao struct {
    UsuarioID int
    Caminho   string
    EhAdmin   bool
}

// Resposta simula uma resposta HTTP
type Resposta struct {
    StatusCode int
    Corpo      string
}

// validarRequisicao valida parâmetros da requisição
func validarRequisicao(req *Requisicao) error {
    if req.UsuarioID <= 0 {
        return ErrRequisicaoInvalida.
            ComDetalhe("usuario_id deve ser um inteiro positivo").
            ComContexto("usuario_id", fmt.Sprintf("%d", req.UsuarioID))
    }
    if req.Caminho == "" {
        return ErrRequisicaoInvalida.
            ComDetalhe("caminho não pode ser vazio")
    }
    return nil
}

// autenticar autenticação
func autenticar(req *Requisicao) error {
    if req.UsuarioID == 0 {
        return ErrNaoAutorizado.ComDetalhe("credenciais de autenticação ausentes")
    }
    return nil
}

// autorizar autorização
func autorizar(req *Requisicao) error {
    if !req.EhAdmin {
        return ErrProibido.
            ComDetalhe("privilégios de administrador necessários").
            ComContexto("usuario_id", fmt.Sprintf("%d", req.UsuarioID))
    }
    return nil
}

// encontrarRecurso encontra um recurso (pode disparar panic)
func encontrarRecurso(caminho string) (string, error) {
    // Simular um bug que causa panic
    if caminho == "/crash" {
        var p *int
        *p = 1 // Desreferência de ponteiro nulo, dispara panic
    }

    recursos := map[string]string{
        "/usuarios": "Lista de usuários",
        "/perfil":   "Perfil do usuário",
    }

    dados, existe := recursos[caminho]
    if !existe {
        return "", ErrNaoEncontrado.
            ComDetalhe(fmt.Sprintf("recurso para caminho %s não encontrado", caminho))
    }
    return dados, nil
}

// handleRequisicao processa uma requisição (lógica de negócio)
func handleRequisicao(req *Requisicao) (*Resposta, error) {
    // 1. Validar parâmetros
    if err := validarRequisicao(req); err != nil {
        return nil, err
    }

    // 2. Autenticar
    if err := autenticar(req); err != nil {
        return nil, err
    }

    // 3. Autorizar (apenas para caminhos admin)
    if req.Caminho == "/admin" {
        if err := autorizar(req); err != nil {
            return nil, err
        }
    }

    // 4. Encontrar recurso
    dados, err := encontrarRecurso(req.Caminho)
    if err != nil {
        return nil, err
    }

    return &Resposta{
        StatusCode: 200,
        Corpo:      dados,
    }, nil
}

// ==================== Middleware de Recuperação ====================

// MiddlewareRecuperacao middleware de recuperação de panic
func MiddlewareRecuperacao(handler func(*Requisicao) (*Resposta, error)) func(*Requisicao) (*Resposta, error) {
    return func(req *Requisicao) (resp *Resposta, err error) {
        defer func() {
            if r := recover(); r != nil {
                // Logar stack trace do panic
                stack := string(debug.Stack())
                fmt.Printf("[PANIC] %v\nStack:\n%s\n", r, stack)

                // Retornar erro interno em vez de travar
                err = ErrInterno.
                    ComDetalhe(fmt.Sprintf("panic: %v", r)).
                    ComCausa(fmt.Errorf("panic: %v", r))
            }
        }()

        return handler(req)
    }
}

// ==================== Formatação de Erro ====================

// formatarErro formata um erro em uma resposta amigável
func formatarErro(err error) *Resposta {
    var appErr *ErroApp
    if errors.As(err, &appErr) {
        msg := fmt.Sprintf("Erro [%d]: %s", appErr.Codigo, appErr.Mensagem)
        if appErr.Detalhe != "" {
            msg += fmt.Sprintf("\n  Detalhe: %s", appErr.Detalhe)
        }
        if len(appErr.Contexto) > 0 {
            msg += "\n  Contexto:"
            for k, v := range appErr.Contexto {
                msg += fmt.Sprintf("\n    %s: %s", k, v)
            }
        }
        return &Resposta{
            StatusCode: appErr.Codigo,
            Corpo:      msg,
        }
    }

    return &Resposta{
        StatusCode: 500,
        Corpo:      fmt.Sprintf("Erro desconhecido: %v", err),
    }
}

// ==================== Função Principal ====================

func main() {
    // Envolver handler com middleware de recuperação
    handlerSeguro := MiddlewareRecuperacao(handleRequisicao)

    // Testar vários cenários
    casosTeste := []struct {
        nome string
        req  *Requisicao
    }{
        {
            nome: "Requisição normal",
            req:  &Requisicao{UsuarioID: 1, Caminho: "/usuarios", EhAdmin: true},
        },
        {
            nome: "Parâmetros inválidos",
            req:  &Requisicao{UsuarioID: -1, Caminho: "/usuarios"},
        },
        {
            nome: "Não autenticado",
            req:  &Requisicao{UsuarioID: 0, Caminho: "/usuarios"},
        },
        {
            nome: "Permissões insuficientes",
            req:  &Requisicao{UsuarioID: 2, Caminho: "/admin", EhAdmin: false},
        },
        {
            nome: "Recurso não encontrado",
            req:  &Requisicao{UsuarioID: 1, Caminho: "/desconhecido"},
        },
        {
            nome: "Disparar panic (recuperação automática)",
            req:  &Requisicao{UsuarioID: 1, Caminho: "/crash"},
        },
    }

    for _, ct := range casosTeste {
        fmt.Printf("=== %s ===\n", ct.nome)

        resp, err := handlerSeguro(ct.req)
        if err != nil {
            resp = formatarErro(err)
        }

        fmt.Printf("  Status: %d\n", resp.StatusCode)
        fmt.Printf("  Resposta: %s\n\n", resp.Corpo)
    }
}
▶ Experimente

Saída:

=== Requisição normal ===
  Status: 200
  Resposta: Lista de usuários

=== Parâmetros inválidos ===
  Status: 400
  Resposta: Erro [400]: requisição inválida
  Detalhe: usuario_id deve ser um inteiro positivo
  Contexto:
    usuario_id: -1

=== Não autenticado ===
  Status: 401
  Resposta: Erro [401]: acesso não autorizado
  Detalhe: credenciais de autenticação ausentes

=== Permissões insuficientes ===
  Status: 403
  Resposta: Erro [403]: proibido
  Detalhe: privilégios de administrador necessários
  Contexto:
    usuario_id: 2

=== Recurso não encontrado ===
  Status: 404
  Resposta: Erro [404]: recurso não encontrado
  Detalhe: recurso para caminho /desconhecido não encontrado

=== Disparar panic (recuperação automática) ===
[PANIC] runtime error: invalid memory address or nil pointer dereference
Stack:
...
  Status: 500
  Resposta: Erro [500]: erro interno do servidor
  Detalhe: panic: runtime error: invalid memory address or nil pointer dereference

Cenários do Mundo Real

Cenário 1: Rollback de Transação de Banco de Dados

Em operações de banco de dados envolvendo múltiplos passos, qualquer falha requer rollback de operações já executadas.

GO
package main

import (
    "errors"
    "fmt"
)

var (
    ErrSaldoInsuficiente = errors.New("saldo insuficiente")
    ErrContaCongelada    = errors.New("conta está congelada")
)

// ErroTransacao erro de transação, registra o passo que falhou
type ErroTransacao struct {
    Passo   string
    Causa   error
    Acoes   []string // Ações executadas que precisam de rollback
}

func (e *ErroTransacao) Error() string {
    return fmt.Sprintf("falha na transação [passo: %s]: %v", e.Passo, e.Causa)
}

func (e *ErroTransacao) Unwrap() error {
    return e.Causa
}

// ServicoTransferencia serviço de transferência
type ServicoTransferencia struct {
    saldos   map[string]float64
    congelados map[string]bool
}

// Transferir executa uma transação de transferência
func (s *ServicoTransferencia) Transferir(de, para string, valor float64) error {
    var passosExecutados []string

    // Passo 1: Validar conta de origem
    if s.congelados[de] {
        return &ErroTransacao{
            Passo: "validar conta de origem",
            Causa: ErrContaCongelada,
        }
    }
    passosExecutados = append(passosExecutados, "bloquear conta de origem")

    // Passo 2: Verificar saldo
    if s.saldos[de] < valor {
        return &ErroTransacao{
            Passo:  "verificar saldo",
            Causa:  ErrSaldoInsuficiente,
            Acoes:  passosExecutados,
        }
    }
    passosExecutados = append(passosExecutados, "verificação de saldo aprovada")

    // Passo 3: Deduzir da conta de origem
    s.saldos[de] -= valor
    passosExecutados = append(passosExecutados, fmt.Sprintf("deduzido %.2f", valor))

    // Passo 4: Validar conta de destino
    if s.congelados[para] {
        return &ErroTransacao{
            Passo:  "validar conta de destino",
            Causa:  ErrContaCongelada,
            Acoes:  passosExecutados, // Precisa de rollback do valor deduzido
        }
    }

    // Passo 5: Adicionar à conta de destino
    s.saldos[para] += valor

    fmt.Printf("  Transferência bem-sucedida: %s -> %s, Valor: %.2f\n", de, para, valor)
    return nil
}

func main() {
    svc := &ServicoTransferencia{
        saldos: map[string]float64{
            "Alice": 1000,
            "Bob":   500,
            "Carol": 0,
        },
        congelados: map[string]bool{
            "Carol": true,
        },
    }

    testes := []struct {
        nome string
        de   string
        para string
        valor float64
    }{
        {"Transferência normal", "Alice", "Bob", 200},
        {"Saldo insuficiente", "Alice", "Bob", 9999},
        {"Conta destino congelada", "Alice", "Carol", 100},
    }

    for _, t := range testes {
        fmt.Printf("--- %s ---\n", t.nome)
        fmt.Printf("  Antes: %s=%.2f, %s=%.2f\n",
            t.de, svc.saldos[t.de], t.para, svc.saldos[t.para])

        err := svc.Transferir(t.de, t.para, t.valor)
        if err != nil {
            fmt.Printf("  Falhou: %v\n", err)

            // Verificar se rollback é necessário
            var txErr *ErroTransacao
            if errors.As(err, &txErr) && len(txErr.Acoes) > 0 {
                fmt.Printf("  Rollback %d ações executadas:\n", len(txErr.Acoes))
                for _, acao := range txErr.Acoes {
                    fmt.Printf("    - Desfazer: %s\n", acao)
                }
                // Em projetos reais, executar lógica de rollback aqui
                // ex: svc.saldos[de] += valor
            }
        }

        fmt.Printf("  Depois: %s=%.2f, %s=%.2f\n\n",
            t.de, svc.saldos[t.de], t.para, svc.saldos[t.para])
    }
}

Cenário 2: Processamento em Lote de Arquivos com Coleta de Erros

Ao processar múltiplos arquivos, não pare na primeira falha. Em vez disso, colete todos os erros e reporte-os juntos.

GO
package main

import (
    "errors"
    "fmt"
    "strings"
)

var (
    ErrArquivoCorrompido = errors.New("arquivo está corrompido")
    ErrPermissao         = errors.New("permissões insuficientes")
    ErrDiscoCheio        = errors.New("espaço em disco insuficiente")
)

// ErroArquivo erro para um único arquivo
type ErroArquivo struct {
    Caminho string
    Op      string
    Causa   error
}

func (e *ErroArquivo) Error() string {
    return fmt.Sprintf("[%s] %s: %v", e.Op, e.Caminho, e.Causa)
}

func (e *ErroArquivo) Unwrap() error {
    return e.Causa
}

// ResultadoLote resultado do processamento em lote
type ResultadoLote struct {
    Sucesso []string
    Falha   []ErroArquivo
}

func (r *ResultadoLote) TemErros() bool {
    return len(r.Falha) > 0
}

func (r *ResultadoLote) Resumo() string {
    var sb strings.Builder
    sb.WriteString(fmt.Sprintf("Processamento concluído: %d com sucesso, %d com falha\n",
        len(r.Sucesso), len(r.Falha)))

    if len(r.Sucesso) > 0 {
        sb.WriteString("Arquivos com sucesso:\n")
        for _, f := range r.Sucesso {
            sb.WriteString(fmt.Sprintf("  ✓ %s\n", f))
        }
    }

    if len(r.Falha) > 0 {
        sb.WriteString("Arquivos com falha:\n")
        for _, e := range r.Falha {
            sb.WriteString(fmt.Sprintf("  ✗ %s (%s: %v)\n", e.Caminho, e.Op, e.Causa))
        }
    }

    return sb.String()
}

// processarArquivo simula processamento de um único arquivo
func processarArquivo(caminho string) error {
    // Simular vários erros possíveis
    errosArquivo := map[string]error{
        "corrompido.txt": ErrArquivoCorrompido,
        "secreto.txt":    ErrPermissao,
        "grande.txt":     ErrDiscoCheio,
    }

    if err, existe := errosArquivo[caminho]; existe {
        return &ErroArquivo{
            Caminho: caminho,
            Op:      "processar",
            Causa:   err,
        }
    }
    return nil
}

// processarArquivos processa arquivos em lote
func processarArquivos(caminhos []string) *ResultadoLote {
    resultado := &ResultadoLote{}

    for _, caminho := range caminhos {
        err := processarArquivo(caminho)
        if err != nil {
            resultado.Falha = append(resultado.Falha, ErroArquivo{
                Caminho: caminho,
                Op:      "processar",
                Causa:   errors.Unwrap(err),
            })
        } else {
            resultado.Sucesso = append(resultado.Sucesso, caminho)
        }
    }

    return resultado
}

func main() {
    arquivos := []string{
        "relatorio.pdf",
        "dados.csv",
        "corrompido.txt",
        "imagem.png",
        "secreto.txt",
        "grande.txt",
        "leia-me.md",
    }

    fmt.Printf("Iniciando processamento de %d arquivos...\n\n", len(arquivos))

    resultado := processarArquivos(arquivos)
    fmt.Println(resultado.Resumo())

    // Fornecer diferentes sugestões baseadas nos tipos de erro
    for _, fe := range resultado.Falha {
        switch {
        case errors.Is(fe.Causa, ErrArquivoCorrompido):
            fmt.Printf("Sugestão: %s precisa ser restaurado do backup\n", fe.Caminho)
        case errors.Is(fe.Causa, ErrPermissao):
            fmt.Printf("Sugestão: Verifique as permissões do arquivo %s\n", fe.Caminho)
        case errors.Is(fe.Causa, ErrDiscoCheio):
            fmt.Printf("Sugestão: Libere espaço em disco e tente novamente %s\n", fe.Caminho)
        }
    }
}

❓ Perguntas Frequentes

P1: Quando devo usar panic vs retornar error?

Princípio: Use error para falhas esperadas; use panic para erros de programação que "nunca deveriam acontecer."

GO
// ✓ Correto: retornar error — entrada do usuário é imprevisível
func dividir(a, b float64) (float64, error) {
    if b == 0 {
        return 0, errors.New("divisor não pode ser zero")
    }
    return a / b, nil
}

// ✓ Correto: usar panic — isso é um bug do programador, nil nunca deveria ser passado
func NaoDeveSerNulo(v interface{}) {
    if v == nil {
        panic("valor não deve ser nulo, este é um erro de programação do chamador")
    }
}

Regras práticas:

P2: Qual é a diferença entre errors.Is e errors.As?

GO
// errors.Is: verifica se a cadeia de erros contém um erro sentinela específico
// Equivalente a: err == alvo || err.Unwrap() == alvo || ...
errors.Is(err, ErrNaoEncontrado)  // true/false

// errors.As: extrai um tipo de erro específico da cadeia de erros
// Como uma asserção de tipo, mas percorre toda a cadeia de erros
var appErr *ErroApp
errors.As(err, &appErr)  // true + atribuição, ou false

// Analogia:
// errors.Is → "Você é João Silva?" (verificando identidade)
// errors.As → "Você é programador? Se sim, me diga suas habilidades" (extraindo info)

P3: Por que mensagens de erro são recomendadas para começar com minúsculas e sem pontuação?

Convenção oficial e da comunidade Go:

GO
// ✓ Recomendado
fmt.Errorf("lendo arquivo %s falhou: %w", nome, err)
// Saída: lendo arquivo config.yaml falhou: permissão negada

// ✗ Não recomendado
fmt.Errorf("lendo arquivo %s falhou.: %w", nome, err)  // Pontuação desnecessária
fmt.Errorf("lendo arquivo %s falhou!: %w", nome, err)  // Exclamação desnecessária
fmt.Errorf("Ler arquivo %s falhou: %w", nome, err)     // Linguagem inconsistente

A razão é que erros frequentemente são concatenados em cadeias de erros, e minúsculas sem pontuação são mais fáceis de ler:

inicializando banco de dados: conectando a postgres: dial tcp 127.0.0.1:5432: conexão recusada

P4: Como comparar erros envolvidos?

Comparação direta com == não funciona para erros envolvidos; você deve usar errors.Is:

GO
var ErrSentinela = errors.New("sentinela")

envolvido := fmt.Errorf("contexto: %w", ErrSentinela)

// ✗ Errado: comparação direta falha
fmt.Println(envolvido == ErrSentinela)  // false

// ✓ Correto: usar errors.Is
fmt.Println(errors.Is(envolvido, ErrSentinela))  // true

// errors.Is percorre toda a cadeia de erros:
// envolvido → Unwrap() → ErrSentinela → correspondência!

📖 Resumo

Tópico Pontos Chave
Interface error Precisa apenas implementar o método Error() string
Criando erros errors.New para erros simples, fmt.Errorf + %w para erros envolvidos
Verificando erros errors.Is para verificação de tipo, errors.As para extração de tipo
Erros personalizados Implementar métodos Error() + Unwrap(), incluir contexto de negócio
panic/recover Apenas para erros irrecuperáveis; capturar com recover em defer
Filosofia de erro Tratamento explícito, verificar cada erro, erros são valores não exceções
Encapsulamento de erro Usar %w para preservar erros subjacentes, usar %v para descartá-los
Convenção da comunidade Mensagens de erro em minúsculas sem pontuação, como último valor de retorno

Três regras de ouro do tratamento de erros Go:

  1. Verificar imediatamente — Após receber um error, verifique err != nil imediatamente
  2. Envolver em cada camada — Adicione contexto em cada camada, use %w para preservar o erro original
  3. Degradação graciosa — Recupere se possível, reporte para cima se não, use panic apenas como último recurso

📝 Exercícios

Exercício 1: Implementar um Leitor de Arquivo com Tentativa

Escrever uma função LerComTentativa(caminho string, maxTentativas int) ([]byte, error) com estes requisitos:

GO
// Framework de referência
var ErrMaxTentativasExcedido = errors.New("máximo de tentativas excedido")

func LerComTentativa(caminho string, maxTentativas int) ([]byte, error) {
    var ultimoErr error
    for i := 0; i < maxTentativas; i++ {
        dados, err := os.ReadFile(caminho)
        if err == nil {
            return dados, nil
        }
        ultimoErr = err
        fmt.Printf("  Tentativa %d falhou: %v\n", i+1, err)
    }
    return nil, fmt.Errorf("lendo %s: %w (último erro: %v)", caminho, ErrMaxTentativasExcedido, ultimoErr)
}

Exercício 2: Construir um Sistema de Níveis de Erro

Implementar um sistema de níveis de erro suportando os seguintes níveis:

GO
type Nivel int

const (
    NivelDebug Nivel = iota
    NivelInfo
    NivelWarn
    NivelError
    NivelFatal
)

type ErroComNivel struct {
    Nivel    Nivel
    Mensagem string
    Causa    error
}

Requisitos:

Exercício 3: Implementar um Coletor de Erros (Seguro para Concorrência)

Escrever um tipo ColetorErros para coletar erros de múltiplas goroutines em cenários concorrentes:

GO
type ColetorErros struct {
    // Campos que você precisa
}

// Adicionar adiciona um erro (seguro para concorrência)
func (c *ColetorErros) Adicionar(err error) { ... }

// Erros retorna todos os erros coletados
func (c *ColetorErros) Erros() []error { ... }

// TemErros verifica se há algum erro
func (c *ColetorErros) TemErros() bool { ... }

// Erro combina todos os erros em um único erro
func (c *ColetorErros) Erro() error { ... }

Requisitos:


Próxima Lição

Após completar esta lição, continue com Lição 11: Pacotes e Módulos para aprender como organizar código Go, gerenciar dependências e publicar seus próprios pacotes.

Web-Tutorial.com

Equipe Técnica Web-Tutorial

Uma plataforma de tutoriais mantida por diversos desenvolvedores. Cada tutorial é escrito e revisado por profissionais da área correspondente. Trabalhamos para manter nosso conteúdo preciso e confiável — se encontrar algum problema, avise-nos.

100%