Três tipos de erro
Código Go usa três formatos de erro, do mais simples ao mais rico:
- Um erro avulso:
errors.New("...")oufmt.Errorf("...")criado na hora. Quem chama só consegue ler a mensagem. - Um erro sentinela: uma variável de nível de pacote como
io.EOF. Quem chama pode testá-lo comerrors.Is. - Um tipo de erro: uma struct com um método
Error(), carregando campos. Quem chama pode extraí-lo comerrors.Ase ler os campos.
Escolha o mais simples que permita a quem chama fazer o que precisa.
Erros sentinela
Um sentinela é um valor de erro declarado uma vez e comparado por identidade. Por convenção, os nomes começam com Err.
Saída:
mug bought
cap is out of stock, notify me later
buy "hat": not found
Duas chamadas de errors.New com o mesmo texto geram erros diferentes: errors.New("x") == errors.New("x") é false. Por isso um sentinela precisa ser uma única variável compartilhada.
Sentinelas passam a fazer parte da API do seu pacote. Depois que quem chama verifica ErrNotFound, você não pode parar de devolvê-lo sem quebrar esse código. Exporte só os que quem chama realmente precisa para tomar decisões.
Tipos de erro personalizados
Quando quem chama precisa de detalhes (qual campo, qual código de status, quanto esperar antes de tentar de novo), defina um tipo.
Saída:
register: age: must be between 0 and 150
bad field: age
O método tem receiver ponteiro e a função devolve &ValidationError{...}, então o alvo do errors.As é um *ValidationError, e você passa o endereço dele (&ve, um **ValidationError). Errar esse nível é o erro clássico com errors.As: com receiver de valor, você devolveria ValidationError{...} e declararia var ve ValidationError. O go vet pega o deslize mais comum, passar ve em vez de &ve: second argument to errors.As must be a non-nil pointer to either a type that implements error, or to any interface type.
Empacotando com %w
fmt.Errorf com %w devolve um erro que se lembra daquele que empacota. Cada camada acrescenta contexto e mantém a cadeia intacta.
Saída:
start: connect db: timeout
*fmt.wrapError: start: connect db: timeout
*fmt.wrapError: connect db: timeout
*errors.errorString: timeout
true
false
Com %v a mensagem é a mesma, mas a cadeia é cortada, então errors.Is devolve false. Escolha %w quando quem chama deve conseguir ver a causa, e %v quando a causa é um detalhe de implementação do qual você não quer que dependam.
Desde o Go 1.20, uma chamada pode empacotar vários erros: fmt.Errorf("%w; %w", err1, err2). errors.Is então casa com qualquer um dos dois.
errors.Join: vários erros de uma vez
Validação e limpeza muitas vezes geram mais de um erro. O errors.Join (Go 1.20) os agrupa.
Saída:
name: required
email: required
age: negative
---
true
true
errors.Join devolve nil quando todos os argumentos são nil, então a função acima não precisa de um caso especial para "nenhum erro".
Métodos Unwrap e Is
errors.Is e errors.As encontram erros empacotados chamando um método Unwrap. Um tipo próprio que guarda uma causa deve expô-la:
Um tipo que empacota vários erros implementa Unwrap() []error no lugar.
Um tipo também pode definir Is(target error) bool para decidir a igualdade por conta própria, por exemplo para casar qualquer *HTTPError com o mesmo código de status. Você raramente precisa disso; empacotar um sentinela e devolvê-lo em Unwrap normalmente resolve.
A armadilha do nil tipado
Nunca declare uma variável de erro com o seu tipo concreto e a devolva como error:
func check() error {
var err *ValidationError // nil pointer
// ... no problem found
return err // non-nil error! Its type is *ValidationError
}
O if err != nil de quem chama dá verdadeiro, porque uma interface que guarda um ponteiro nil tipado não é nil. Devolva um nil literal em caso de sucesso e mantenha as variáveis locais de erro com o tipo error. A página de interfaces explica o porquê.
Qual tipo escolher
| Quem chama precisa | Ofereça |
|---|---|
| só registrar ou exibir a falha | fmt.Errorf("...: %w", err) |
| tomar uma decisão com base em uma condição específica | um sentinela var ErrX = errors.New(...) |
| ler detalhes sobre a falha | um tipo de erro com campos |
| ver várias falhas independentes | errors.Join |
Erros comuns
- Comparar erros empacotados com
==. Useerrors.Is. - Passar algo que não é ponteiro para o
errors.As. Ele precisa de um ponteiro para uma variável do tipo alvo. - Criar um "sentinela" dentro de uma função.
return errors.New("not found")cria um valor novo a cada chamada; quem chama não consegue comparar com ele. - Exportar todos os erros. Cada sentinela ou tipo exportado é uma promessa de API.
- Comparar pelo
err.Error(). Strings são para humanos.
Perguntas frequentes
Como criar um erro personalizado em Go?
Para uma condição fixa, declare um sentinela no nível de pacote: var ErrNotFound = errors.New("not found"). Para um erro que carrega dados, defina um tipo com um método Error() string: type ValidationError struct { Field string } e func (e *ValidationError) Error() string { return e.Field + " is invalid" }.
Como empacotar um erro em Go?
Use fmt.Errorf com o verbo %w: return fmt.Errorf("load user %d: %w", id, err). A mensagem do erro novo inclui a do antigo, e errors.Unwrap, errors.Is e errors.As conseguem chegar ao original. Desde o Go 1.20, uma chamada de Errorf pode empacotar vários erros com vários verbos %w.
Qual a diferença entre errors.Is e errors.As?
errors.Is(err, target) responde "este valor de erro específico está em algum lugar da cadeia?", para sentinelas como io.EOF. errors.As(err, &target) responde "existe um erro deste tipo na cadeia?" e, se existir, o guarda em target para você ler os campos.
O que o errors.Join faz?
errors.Join(errs...) (Go 1.20) combina vários erros em um. A mensagem é a das mensagens individuais separadas por quebras de linha, erros nil são descartados, e ele devolve nil se todos forem nil. errors.Is e errors.As casam se qualquer um dos erros combinados casar.