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")
    }
}

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

Exercícios

  1. Crie uma função que leia um arquivo e retorne um erro empacotado com %w adicionando o nome do arquivo. Em seguida, use errors.Is para 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")
        }
    }
  2. Defina um tipo de erro customizado chamado HTTPError com campos StatusCode int e Message string. Implemente a interface error e crie uma função que retorne um HTTPError. Use errors.As para 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)
        }
    }
  3. Crie um erro sentinela ErrInvalidInput e uma função que o retorne quando um valor for negativo. Use errors.Is para 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")
        }
    }
  4. 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, use errors.Unwrap para 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)
        }
    }
  5. Crie um tipo de erro customizado que implemente Unwrap() error e contenha um erro interno. Use errors.Is para 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")
        }
    }