Pular para o conteúdo

Trilha de Go · parte 3 de 37

Tratamento de Erros Idiomático em Go: Wrapping, Sentinelas e Exceções

Como tratar erros como valores em Go: encadear causas com %w, inspecionar com errors.Is e errors.As e quando evitar panic.

Publicada em · 5 min de leitura

Tamanho da fonte:
Largura da coluna:

Tratamento de Erros Idiomático em Go: Wrapping, Sentinelas e Exceções

Uma das decisões de design mais celebradas (e inicialmente controversas) da linguagem Go é a ausência de mecanismos de exceção tradicionais como try/catch/finally. Em vez de tratar erros como interrupções mágicas de fluxo de controle, a filosofia fundadora do Go estabelece que:

“Errors are values.” — Rob Pike

Erros em Go são simplesmente valores normais de retorno, inspecionáveis, manipuláveis e transmitidos explicitamente na tupla de saída de funções ((T, error)). Esse modelo força o engenheiro a encarar as falhas como parte do fluxo feliz da aplicação, eliminando saltos ocultos de pilha de execução e gerando sistemas altamente estáveis e previsíveis.


1. Erros Explícitos vs Exceções Ocultas

A comparação entre o modelo explícito do Go e os blocos try/catch de outras linguagens ilustra por que o Go é tão resistente a bugs silenciosos em produção:

Erros Explícitos como Valores vs Exceções Ocultas
figura 1 · Erros Explícitos como Valores vs Exceções Ocultas
  • Com Exceções (try/catch): Qualquer linha dentro do bloco try pode lançar uma exceção não declarada que salta camadas arbitrárias da pilha, dificultando o raciocínio sobre quais recursos foram limpos ou em qual estado o sistema ficou.
  • Com Erros Explícitos em Go: O fluxo é estritamente linear e visível. Você sempre sabe exatamente onde uma falha pode ocorrer e tem a obrigação de tratá-la ou propagá-la.

2. A Interface error e Erros Sentinela

Na Standard Library do Go, a interface error é a mais simples e poderosa de toda a linguagem:

type error interface {
    Error() string
}

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

Erros Sentinela (Sentinel Errors)

Erros sentinela são variáveis globais públicas exportadas por pacotes para representar falhas conhecidas e comparáveis:

package repository

import "errors"

// Sentinelas padronizados pelo pacote
var (
	ErrNotFound       = errors.New("registro não encontrado")
	ErrDuplicateKey   = errors.New("chave já cadastrada no sistema")
	ErrConnectionLost = errors.New("conexão com o banco interrompida")
)

3. O Padrão Moderno: Wrapping com %w, errors.Is e errors.As

A partir do Go 1.13, o tratamento de erros foi revolucionado com a introdução do operador %w e dos métodos de árvore de erros:

Cadeia de Encapsulamento de Erros com %w
figura 2 · Cadeia de Encapsulamento de Erros com %w

3.1 Encapsulamento com fmt.Errorf("%w")

Ao propagar um erro de uma camada inferior para uma camada superior, nunca retorne o erro cru sem contexto e nunca formate com %v ou %s (que destroem a cadeia de tipos). Use sempre %w (wrap):

func (s *UserService) GetUserProfile(ctx context.Context, id int) (*Profile, error) {
	user, err := s.repo.FindByID(ctx, id)
	if err != nil {
		// %w encapsula o erro original permitindo inspeção futura
		return nil, fmt.Errorf("user_service: falha ao carregar perfil para o id %d: %w", id, err)
	}
	return user.Profile, nil
}

3.2 Inspecionando Erros com errors.Is

Nunca use err == ErrNotFound em código moderno! Se o erro tiver sido encapsulado por camadas superiores com %w, a igualdade simples falhará.

O método errors.Is(err, target) percorre recursivamente toda a árvore de erros desempacotando cada nível:

profile, err := userService.GetUserProfile(ctx, 42)
if err != nil {
	// errors.Is encontra ErrNotFound mesmo que ele esteja encapsulado 3 camadas abaixo!
	if errors.Is(err, repository.ErrNotFound) {
		http.Error(w, `{"error":"usuario_inexistente"}`, http.StatusNotFound)
		return
	}
	
	http.Error(w, `{"error":"erro_interno"}`, http.StatusInternalServerError)
	return
}

3.3 Extraindo Tipos Customizados com errors.As

Quando um erro carrega campos ricos estruturados (como erros de validação com lista de campos inválidos), utilizamos errors.As(err, &target) para desempacotar o erro com segurança de tipos:

type ValidationError struct {
	Field   string
	Message string
}

func (e *ValidationError) Error() string {
	return fmt.Sprintf("campo '%s' inválido: %s", e.Field, e.Message)
}

// Consumo:
err := validateInput(data)
if err != nil {
	var valErr *ValidationError
	// errors.As desempacota o erro e popula o ponteiro se encontrar o tipo na árvore
	if errors.As(err, &valErr) {
		log.Printf("Erro de validação no campo: %s (detalhes: %s)\n", valErr.Field, valErr.Message)
		return
	}
	log.Printf("Outro tipo de erro: %v\n", err)
}

3.4 Agrupando Múltiplos Erros com errors.Join (Go 1.20+)

Quando executamos validações em lote ou tarefas paralelas onde múltiplas operações podem falhar simultaneamente, o errors.Join combina múltiplos erros em um único valor composto:

func ValidateUser(u User) error {
	var errs []error

	if u.Name == "" {
		errs = append(errs, errors.New("o nome não pode ser vazio"))
	}
	if u.Age < 18 {
		errs = append(errs, errors.New("a idade mínima é 18 anos"))
	}

	// Retorna nil se errs estiver vazio, ou o erro unificado
	return errors.Join(errs...)
}

4. Quando Usar panic e recover?

Em Go, panic não é uma exceção comum e nunca deve ser usado para fluxo de controle de negócio.

Quando o panic é Justificado:

  1. Falhas Críticas de Inicialização no main(): O arquivo de configuração principal não existe, a string de conexão está vazia ou uma expressão regular estática falhou (regexp.MustCompile).
  2. Invariantes do Compilador: Bugs de código interno que nunca deveriam acontecer se a lógica estivesse correta.

Protegendo Servidores de Produção com recover():

Para impedir que um pânico em uma goroutine derrube todo o servidor de produção, interceptamos pânicos em Middlewares em Go:

func RecoveryMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		defer func() {
			if rec := recover(); rec != nil {
				log.Printf("PANIC RECUPERADO: %v\n", rec)
				http.Error(w, `{"error":"internal_server_error"}`, http.StatusInternalServerError)
			}
		}()
		next.ServeHTTP(w, r)
	})
}

5. Boas Práticas e Checklist de Engenharia

  • Nunca Descarte Erros Silenciosamente: Nunca faça _ = db.Close() ou ignore o erro de retorno sem uma justificativa explícita documentada.
  • Não Logue E Retorne o Erro: Se uma função intermediária fizer log.Println(err) e depois return err, e a função de cima fizer o mesmo, o mesmo erro será logado 5 vezes nos servidores. Retorne o erro com %w e logue apenas UMA vez na camada de borda.
  • Mensagens em Minúsculas: Mensagens de erro em Go por convenção iniciam com letra minúscula e não terminam com ponto final: errors.New("conexão perdida") (e não "Conexão perdida."), pois elas serão concatenadas pelo wrapping.

6. Resumo Conceitual

  • Erros em Go são valores comuns retornados explicitamente na assinatura de funções.
  • Utilize erros sentinela para falhas conhecidas e compare sempre com errors.Is(err, target).
  • Use fmt.Errorf("...: %w", err) para enriquecer o erro com contexto de rastreabilidade sem quebrar a cadeia de inspeção.
  • Desempacote structs de erro ricas e tipadas com errors.As(err, &target).
  • Agrupe múltiplos erros simultâneos com errors.Join(errs...).
  • Reserve o panic exclusivamente para erros irrecuperáveis durante o startup do binário.

↑ Voltar ao topo

Atalhos de teclado

Abrir busca
/
Alternar tema claro/escuro
t
Aula anterior na série
←
Próxima aula na série
→
Lista de atalhos
?