Erros avançado
Esta aula aborda técnicas avançadas de tratamento de erros em Go, incluindo wrapping com %w, funções errors.Is e errors.As, criação de erros customizados e uso de sentinel errors. O conteúdo é prático e progressivo, com exemplos de código e exercícios para fixação.
Em Go, o tratamento de erros é explícito e faz parte da cultura da linguagem. Após dominar o básico de error como interface, é essencial conhecer técnicas mais avançadas para lidar com erros em aplicações reais. Nesta aula, exploraremos wrapping de erros, funções de comparação e desempacotamento, criação de tipos de erro customizados e o padrão de sentinel errors.
Wrapping com %w
O wrapping de erros permite adicionar contexto a um erro sem perder a informação original. Em Go, usamos o verbo %w na função fmt.Errorf para criar um erro que "embrulha" outro. Isso é útil para propagar erros com mensagens mais descritivas, mantendo a cadeia de erros intacta.
Exemplo básico:
package main
import (
"fmt"
"os"
)
func readFile(name string) error {
_, err := os.ReadFile(name)
if err != nil {
return fmt.Errorf("falha ao ler arquivo %s: %w", name, err)
}
return nil
}
func main() {
err := readFile("inexistente.txt")
if err != nil {
fmt.Println(err) // saída: falha ao ler arquivo inexistente.txt: open inexistente.txt: no such file or directory
}
}Note que %w só pode ser usado uma vez por chamada de fmt.Errorf. Para múltiplos wraps, é necessário aninhar chamadas. O wrapping cria uma árvore de erros que pode ser percorrida com errors.Is e errors.As.
errors.Is e errors.As
As funções errors.Is e errors.As (pacote errors) permitem inspecionar a cadeia de erros. errors.Is verifica se um erro específico (geralmente um sentinel error) está presente na cadeia, enquanto errors.As encontra o primeiro erro que corresponde a um tipo específico e o atribui a uma variável.
Exemplo com errors.Is:
package main
import (
"errors"
"fmt"
"os"
)
var ErrNotFound = errors.New("arquivo não encontrado")
func openFile(name string) error {
_, err := os.Open(name)
if err != nil {
return fmt.Errorf("open %s: %w", name, ErrNotFound)
}
return nil
}
func main() {
err := openFile("test.txt")
if errors.Is(err, ErrNotFound) {
fmt.Println("Erro: arquivo não encontrado")
}
}Exemplo com errors.As:
package main
import (
"errors"
"fmt"
"os"
)
type PathError struct {
Op string
Path string
Err error
}
func (e *PathError) Error() string {
return e.Op + " " + e.Path + ": " + e.Err.Error()
}
func openFile(name string) error {
_, err := os.Open(name)
if err != nil {
return &PathError{Op: "open", Path: name, Err: err}
}
return nil
}
func main() {
err := openFile("test.txt")
var pathErr *PathError
if errors.As(err, &pathErr) {
fmt.Printf("Operação: %s, Caminho: %s\n", pathErr.Op, pathErr.Path)
}
}Ambas as funções percorrem a cadeia de erros criada por %w. Se o erro não foi envolvido com %w, a cadeia não é formada e a verificação falha.
Erros customizados
Erros customizados são tipos que implementam a interface error (método Error() string). Eles permitem adicionar informações extras, como códigos de erro, campos estruturados ou métodos auxiliares. É comum usar ponteiros para struct para que o tipo seja comparável corretamente.
Exemplo:
package main
import (
"fmt"
)
type ValidationError struct {
Field string
Value interface{}
Msg string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("validação falhou no campo %s: %s", e.Field, e.Msg)
}
func validateAge(age int) error {
if age < 0 {
return &ValidationError{Field: "age", Value: age, Msg: "idade não pode ser negativa"}
}
return nil
}
func main() {
err := validateAge(-5)
if err != nil {
fmt.Println(err)
// Acessando campos do erro customizado
if vErr, ok := err.(*ValidationError); ok {
fmt.Printf("Campo: %s, Valor: %v\n", vErr.Field, vErr.Value)
}
}
}Para usar com errors.As, o tipo deve ser exportado (maiúsculo) e geralmente implementado como ponteiro. Boa prática: fornecer uma função construtora e, se possível, uma função para verificar o tipo.
Sentinel errors
Sentinel errors são erros pré-definidos que representam condições específicas, como io.EOF ou sql.ErrNoRows. Eles são criados com errors.New e comparados com == ou errors.Is. São úteis para sinalizar casos esperados sem depender de strings.
Exemplo:
package main
import (
"errors"
"fmt"
)
var ErrNotFound = errors.New("item não encontrado")
var ErrPermission = errors.New("permissão negada")
func findItem(id int) error {
if id == 0 {
return ErrNotFound
}
return nil
}
func main() {
err := findItem(0)
if errors.Is(err, ErrNotFound) {
fmt.Println("Tratando erro: item não encontrado")
}
}Cuidados: sentinel errors devem ser públicos e imutáveis. Evite comparar com == se o erro foi wrappado; use errors.Is para percorrer a cadeia. Sentinel errors são uma prática comum, mas podem acoplar pacotes; em alguns casos, erros customizados são preferíveis.
Boas práticas
- Use
%wpara adicionar contexto sem perder o erro original. - Prefira
errors.Iseerrors.Asem vez de type assertion quando houver wrapping. - Para erros customizados, implemente a interface
errorcom ponteiro para struct. - Sentinel errors devem ser documentados e usados com moderação.
- Evite comparar erros com
==se houver wrapping; useerrors.Is.
Referências
- Go 1.13 Errors (blog oficial)
- Pacote errors - documentação oficial
- fmt.Errorf - documentação oficial
- Effective Go: Errors
- DigitalOcean: Creating Custom Errors in Go
Exercícios
Crie uma função que leia um arquivo e retorne um erro wrappado com o nome do arquivo. Use
%we teste com um arquivo inexistente.✓ Resposta:package main import ( "fmt" "os" ) func readFile(name string) error { _, err := os.ReadFile(name) if err != nil { return fmt.Errorf("erro ao ler %s: %w", name, err) } return nil } func main() { err := readFile("inexistente.txt") if err != nil { fmt.Println(err) } }Defina um sentinel error
ErrNegativeSqrte escreva uma funçãoSqrtque retorne esse erro para números negativos. Useerrors.Ispara testar.✓ Resposta:package main import ( "errors" "fmt" "math" ) var ErrNegativeSqrt = errors.New("não é possível calcular raiz quadrada de número negativo") func Sqrt(x float64) (float64, error) { if x < 0 { return 0, ErrNegativeSqrt } return math.Sqrt(x), nil } func main() { _, err := Sqrt(-1) if errors.Is(err, ErrNegativeSqrt) { fmt.Println("Erro esperado capturado") } }Crie um tipo de erro customizado
HTTPErrorcom camposStatusCodeeMessage. Implemente a interfaceerror. Escreva uma função que retorne esse erro e useerrors.Aspara extrair os campos.✓ 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 fetch(url string) error { return &HTTPError{StatusCode: 404, Message: "Not Found"} } func main() { err := fetch("http://exemplo.com/naoexiste") var httpErr *HTTPError if errors.As(err, &httpErr) { fmt.Printf("Código: %d, Mensagem: %s\n", httpErr.StatusCode, httpErr.Message) } }Escreva uma função que chame duas outras funções que retornam erros. Use wrapping com
%wpara adicionar contexto a cada erro. Depois, useerrors.Ispara verificar se um sentinel error específico está presente na cadeia.✓ Resposta:package main import ( "errors" "fmt" ) var ErrNotFound = errors.New("not found") func step1() error { return fmt.Errorf("step1: %w", ErrNotFound) } func step2() error { return fmt.Errorf("step2: %w", errors.New("outro erro")) } func doWork() error { err1 := step1() if err1 != nil { return fmt.Errorf("doWork: %w", err1) } err2 := step2() if err2 != nil { return fmt.Errorf("doWork: %w", err2) } return nil } func main() { err := doWork() if errors.Is(err, ErrNotFound) { fmt.Println("Erro 'not found' encontrado na cadeia") } }Crie um erro customizado
TimeoutErrorcom campoDuration. Escreva uma função que retorne esse erro e useerrors.Aspara verificar o tipo e acessar o campo. Depois, envolva esse erro comfmt.Errorfe%we verifique seerrors.Asainda consegue desempacotá-lo.✓ Resposta:package main import ( "errors" "fmt" "time" ) type TimeoutError struct { Duration time.Duration } func (e *TimeoutError) Error() string { return fmt.Sprintf("timeout após %v", e.Duration) } func connect() error { return &TimeoutError{Duration: 5 * time.Second} } func main() { err := connect() // Envolve o erro wrappedErr := fmt.Errorf("conexão falhou: %w", err) var timeoutErr *TimeoutError if errors.As(wrappedErr, &timeoutErr) { fmt.Printf("Duration: %v\n", timeoutErr.Duration) } }