Erros avançado
Nesta aula avançada sobre erros em Go, exploramos o wrapping de erros com %w, as funções errors.Is e errors.As para comparação e desempacotamento, a criação de erros customizados com structs, e o padrão de erros sentinela. Com exemplos práticos, você aprenderá a construir e gerenciar hierarquias de erros de forma robusta.
Em Go, o tratamento de erros é uma parte fundamental da linguagem, e à medida que os programas crescem, a simples verificação de erros com if err != nil pode se tornar insuficiente. Nesta aula, vamos aprofundar em técnicas avançadas de manipulação de erros: wrapping, funções de comparação e desempacotamento (errors.Is e errors.As), criação de tipos de erro customizados, e o padrão de erros sentinela. Esses conceitos permitem que você propague informações contextuais, identifique erros específicos em cadeias e construa APIs mais expressivas e seguras.
Wrapping com %w
O wrapping de erros é uma técnica que permite adicionar contexto a um erro existente, criando uma nova camada que envolve o erro original. Em Go, isso é feito usando o verbo %w na função fmt.Errorf. O erro resultante implementa a interface error e também a interface Unwrap() error, que permite extrair o erro interno.
Por exemplo, suponha que você tenha uma função que lê um arquivo e quer informar qual arquivo causou o erro, sem perder o erro original:
func readFile(filename string) error {
data, err := os.ReadFile(filename)
if err != nil {
return fmt.Errorf("falha ao ler %s: %w", filename, err)
}
// processa dados
return nil
}
Ao usar %w, o erro retornado contém o erro original encapsulado. Isso é útil para preservar a causa raiz enquanto se adiciona contexto. O empacotamento pode ser encadeado, formando uma cadeia de erros que pode ser percorrida com funções como errors.Is e errors.As.
errors.Is e errors.As
As funções errors.Is e errors.As são fornecidas pelo pacote errors (a partir de Go 1.13) para trabalhar com cadeias de erros. errors.Is verifica se um erro (ou qualquer erro em sua cadeia) corresponde a um erro alvo. É útil para testar erros sentinela ou erros específicos sem precisar desempacotar manualmente.
var ErrNotExist = errors.New("arquivo não existe")
func openFile(name string) error {
_, err := os.Open(name)
if err != nil {
return fmt.Errorf("abertura falhou: %w", err)
}
return nil
}
func main() {
err := openFile("inexistente.txt")
if errors.Is(err, os.ErrNotExist) {
fmt.Println("Arquivo não existe")
}
}
Já errors.As procura na cadeia de erros o primeiro erro que pode ser atribuído a um tipo específico, e o atribui a uma variável. É útil para obter informações adicionais de erros customizados.
type MyError struct {
Msg string
Code int
}
func (e *MyError) Error() string {
return fmt.Sprintf("erro %d: %s", e.Code, e.Msg)
}
func doSomething() error {
return &MyError{Msg: "algo deu errado", Code: 42}
}
func main() {
err := doSomething()
var myErr *MyError
if errors.As(err, &myErr) {
fmt.Printf("Código do erro: %d\n", myErr.Code)
}
}
Ambas as funções percorrem a cadeia de erros automaticamente, chamando Unwrap() quando disponível, até encontrar uma correspondência ou chegar ao fim.
Erros customizados
Criar seus próprios tipos de erro permite incluir informações adicionais, como códigos de erro, mensagens detalhadas ou dados contextuais. Para que um tipo seja tratado como erro, ele deve implementar a interface error, ou seja, ter um método Error() string. Além disso, se você quiser que ele participe da cadeia de erros (para ser usado com %w e errors.Unwrap), seu tipo deve implementar o método Unwrap() error ou ser um wrapper que contenha um erro interno.
type ValidationError struct {
Field string
Value interface{}
Err error
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("validação falhou no campo %s: %v", e.Field, e.Err)
}
func (e *ValidationError) Unwrap() error {
return e.Err
}
func validateAge(age int) error {
if age < 0 {
return &ValidationError{
Field: "age",
Value: age,
Err: errors.New("idade não pode ser negativa"),
}
}
return nil
}
Com erros customizados, você pode usar errors.As para extrair informações específicas, como o campo que falhou, e tomar decisões baseadas nisso.
Sentinel errors
Erros sentinela são erros pré-definidos que representam condições específicas, usados como valores singletons. O exemplo mais comum é io.EOF. Eles permitem que os chamadores comparem erros com == ou usem errors.Is para detectar situações particulares.
var ErrNotFound = errors.New("recurso não encontrado")
var ErrPermissionDenied = errors.New("permissão negada")
func getResource(id int) error {
if id == 0 {
return ErrNotFound
}
// ...
return nil
}
func main() {
err := getResource(0)
if errors.Is(err, ErrNotFound) {
fmt.Println("Recurso não encontrado, trate adequadamente")
}
}
Boas práticas: defina erros sentinela como variáveis de pacote (exportadas ou não, conforme necessário). Evite usar errors.New repetidamente para o mesmo erro; em vez disso, reutilize a variável. Além disso, sentinel errors são imutáveis e não devem ser modificados.
Boas práticas e observações finais
Use wrapping com %w para adicionar contexto, mas evite wrappers excessivos que possam poluir a cadeia. Prefira erros customizados quando precisar de dados estruturados. Sentinel errors são adequados para condições previsíveis e bem definidas. Sempre que possível, use errors.Is e errors.As em vez de comparação direta com == para garantir compatibilidade com wrapping.
Referências
- Go 1.13 errors (blog oficial)
- Documentação do pacote errors
- Documentação de fmt.Errorf
- Effective Go: Errors
- Error Handling in Go 1.13 (Joe Shaw)
- Go Wiki: Errors
Exercícios
-
Crie uma função que leia um arquivo e retorne um erro empacotado com
%wadicionando o nome do arquivo. Em seguida, useerrors.Ispara verificar se o erro original éos.ErrNotExist.✓ Resposta:package main import ( "errors" "fmt" "os" ) func readFile(filename string) error { _, err := os.ReadFile(filename) if err != nil { return fmt.Errorf("erro ao ler %s: %w", filename, err) } return nil } func main() { err := readFile("inexistente.txt") if errors.Is(err, os.ErrNotExist) { fmt.Println("Arquivo não existe") } } -
Defina um tipo de erro customizado chamado
HTTPErrorcom camposStatusCode inteMessage string. Implemente a interfaceerrore crie uma função que retorne umHTTPError. Useerrors.Aspara extrair o código de status.✓ Resposta:package main import ( "errors" "fmt" ) type HTTPError struct { StatusCode int Message string } func (e *HTTPError) Error() string { return fmt.Sprintf("HTTP %d: %s", e.StatusCode, e.Message) } func fetchResource() error { return &HTTPError{StatusCode: 404, Message: "Not Found"} } func main() { err := fetchResource() var httpErr *HTTPError if errors.As(err, &httpErr) { fmt.Printf("Status code: %d\n", httpErr.StatusCode) } } -
Crie um erro sentinela
ErrInvalidInpute uma função que o retorne quando um valor for negativo. Useerrors.Ispara detectar esse erro.✓ Resposta:package main import ( "errors" "fmt" ) var ErrInvalidInput = errors.New("entrada inválida") func process(n int) error { if n < 0 { return ErrInvalidInput } return nil } func main() { err := process(-1) if errors.Is(err, ErrInvalidInput) { fmt.Println("Entrada inválida detectada") } } -
Escreva um programa que encadeie dois wrappers: uma função A chama B, e B retorna um erro básico. A deve adicionar contexto com
%w. Em seguida, useerrors.Unwrappara extrair o erro original.✓ Resposta:package main import ( "errors" "fmt" ) var ErrBase = errors.New("erro base") func B() error { return ErrBase } func A() error { err := B() if err != nil { return fmt.Errorf("A falhou: %w", err) } return nil } func main() { err := A() if err != nil { fmt.Println("Erro empacotado:", err) // Desempacotar unwrapped := errors.Unwrap(err) fmt.Println("Erro original:", unwrapped) } } -
Crie um tipo de erro customizado que implemente
Unwrap() errore contenha um erro interno. Useerrors.Ispara verificar se o erro interno é um sentinela específico.✓ Resposta:package main import ( "errors" "fmt" ) var ErrPermission = errors.New("permissão negada") type WrappedError struct { Msg string Err error } func (e *WrappedError) Error() string { return fmt.Sprintf("%s: %v", e.Msg, e.Err) } func (e *WrappedError) Unwrap() error { return e.Err } func doSomething() error { return &WrappedError{Msg: "operação falhou", Err: ErrPermission} } func main() { err := doSomething() if errors.Is(err, ErrPermission) { fmt.Println("Erro de permissão detectado") } }