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 %w para adicionar contexto sem perder o erro original.
  • Prefira errors.Is e errors.As em vez de type assertion quando houver wrapping.
  • Para erros customizados, implemente a interface error com ponteiro para struct.
  • Sentinel errors devem ser documentados e usados com moderação.
  • Evite comparar erros com == se houver wrapping; use errors.Is.

Referências

Exercícios

  1. Crie uma função que leia um arquivo e retorne um erro wrappado com o nome do arquivo. Use %w e 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)
        }
    }
  2. Defina um sentinel error ErrNegativeSqrt e escreva uma função Sqrt que retorne esse erro para números negativos. Use errors.Is para 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")
        }
    }
  3. Crie um tipo de erro customizado HTTPError com campos StatusCode e Message. Implemente a interface error. Escreva uma função que retorne esse erro e use errors.As para 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)
        }
    }
  4. Escreva uma função que chame duas outras funções que retornam erros. Use wrapping com %w para adicionar contexto a cada erro. Depois, use errors.Is para 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")
        }
    }
  5. Crie um erro customizado TimeoutError com campo Duration. Escreva uma função que retorne esse erro e use errors.As para verificar o tipo e acessar o campo. Depois, envolva esse erro com fmt.Errorf e %w e verifique se errors.As ainda 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)
        }
    }