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
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:
- Com Exceções (
try/catch): Qualquer linha dentro do blocotrypode 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:
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:
- 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). - 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 depoisreturn err, e a função de cima fizer o mesmo, o mesmo erro será logado 5 vezes nos servidores. Retorne o erro com%we 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
panicexclusivamente para erros irrecuperáveis durante o startup do binário.