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:
// 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
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
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
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
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)
}
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).
fmt.Errorf, sempre use %w em vez de %v. %w preserva o erro original, permitindo que errors.Is e errors.As funcionem corretamente.
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.
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)
}
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.
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()
}
}
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.
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)
}
}
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.
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.
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."
// ✓ 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:
- Retornar error: Operações de arquivo, requisições de rede, validação de entrada do usuário, falhas de lógica de negócio
- Usar panic: Falhas de inicialização
init(), erros irrecuperáveis do programa,t.Fatalem testes - Nunca: Usar
panicpara erros de negócio normais
P2: Qual é a diferença entre errors.Is e errors.As?
// 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:
// ✓ 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:
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:
- Verificar imediatamente — Após receber um
error, verifiqueerr != nilimediatamente - Envolver em cada camada — Adicione contexto em cada camada, use
%wpara preservar o erro original - 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:
- Tentar até
maxTentativasvezes - Imprimir a contagem de tentativas após cada falha
- Definir um erro sentinela
ErrMaxTentativasExcedido, retornado quando as tentativas se esgotam - Usar
fmt.Errorfpara envolver erros subjacentes preservando contexto
// 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:
type Nivel int
const (
NivelDebug Nivel = iota
NivelInfo
NivelWarn
NivelError
NivelFatal
)
type ErroComNivel struct {
Nivel Nivel
Mensagem string
Causa error
}
Requisitos:
- Implementar a interface
error - Implementar o método
Unwrap() error - Escrever uma função
EhNivel(err error, nivel Nivel) bool - Testar verificação de nível para vários erros
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:
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:
- Usar
sync.Mutexousync.RWMutexpara segurança de concorrência - Escrever testes: lançar múltiplas goroutines chamando
Adicionarconcorrentemente, depois verificar os resultados da coleta
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.



