Em Go, o tratamento de erros é uma parte fundamental da linguagem, projetado para ser explícito e simples, sem exceções ou mecanismos complexos. Diferente de outras linguagens que usam try-catch, Go trata erros como valores comuns que podem ser retornados e verificados. Isso encoraja o programador a lidar com erros de forma direta, tornando o código mais previsível e fácil de depurar.

Nesta aula, vamos mergulhar no tipo error, na prática idiomática de verificar erros com if err != nil, na criação de erros personalizados e nas convenções que tornam o código Go claro e consistente.

O tipo error

Em Go, error é uma interface nativa definida como:

type error interface {
    Error() string
}

Qualquer tipo que implemente o método Error() string satisfaz a interface error. Isso significa que você pode criar seus próprios tipos de erro, desde que eles tenham esse método. A simplicidade da interface permite que erros sejam valores leves e flexíveis.

Funções que podem falhar geralmente retornam um valor do tipo error como último valor de retorno. Por convenção, se a operação for bem-sucedida, o erro é nil; caso contrário, contém informações sobre o problema.

if err != nil

A verificação if err != nil é a maneira mais comum de tratar erros em Go. Após chamar uma função que retorna um erro, você verifica se o erro é diferente de nil e age de acordo. Esse padrão é tão frequente que se tornou uma marca registrada do código Go.

Exemplo típico:

f, err := os.Open("arquivo.txt")
if err != nil {
    log.Fatal(err) // ou trate o erro de outra forma
}
defer f.Close()
// usa o arquivo...

É importante tratar o erro imediatamente, sem ignorá-lo. Ignorar erros com _ pode levar a comportamentos inesperados e é considerado uma má prática, a menos que você tenha certeza absoluta de que o erro é irrelevante.

Criando erros (errors.New, fmt.Errorf)

Para criar seus próprios erros, a biblioteca padrão oferece duas funções principais:

  • errors.New(texto string) error: Cria um erro simples com a mensagem fornecida.
  • fmt.Errorf(format string, a ...interface{}) error: Permite formatar a mensagem de erro com fmt.Sprintf, retornando um error.

Exemplo com errors.New:

err := errors.New("ocorreu um erro")
fmt.Println(err) // ocorreu um erro

Exemplo com fmt.Errorf (mais flexível):

nome := "João"
idade := 30
err := fmt.Errorf("%s tem %d anos", nome, idade)
fmt.Println(err) // João tem 30 anos

Ambas as funções retornam um valor que implementa a interface error. Use errors.New para mensagens estáticas e fmt.Errorf quando precisar de formatação.

Idiomático

Go possui convenções claras para tratamento de erros que tornam o código consistente e legível:

  • Erro como último valor de retorno: Funções que podem falhar retornam o erro como último parâmetro.
  • Verificação imediata: Após a chamada, verifique if err != nil antes de prosseguir.
  • Mensagens de erro em letra minúscula: Por convenção, as mensagens de erro não começam com letra maiúscula nem terminam com pontuação, a menos que incluam nomes próprios.
  • Erros sentinela: Use variáveis de erro globais para erros específicos (ex.: var ErrNotFound = errors.New("not found")).
  • Empacotamento de erros: Use fmt.Errorf com %w para criar uma cadeia de erros (wrapping), permitindo que errors.Is e errors.As funcionem.

Exemplo de empacotamento:

if err != nil {
    return fmt.Errorf("falha ao processar: %w", err)
}

Isso preserva o erro original para inspeção posterior.

Boas práticas

Algumas dicas para escrever código Go idiomático no tratamento de erros:

  • Não use panic para erros comuns; reserve-o para situações excepcionais que indicam bugs.
  • Documente os erros que sua função pode retornar, especialmente se forem erros sentinela.
  • Evite repetir o contexto do erro; use wrapping para adicionar informações relevantes.
  • Prefira tratar erros no nível mais adequado (não propague erros desnecessariamente para camadas superiores).

Referências

Exercícios

  1. Crie uma função chamada divide(a, b int) (int, error) que retorna um erro se b for zero, usando errors.New. Em seguida, chame a função e trate o erro.

    ✓ Resposta:
    package main
    
    import (
        "errors"
        "fmt"
    )
    
    func divide(a, b int) (int, error) {
        if b == 0 {
            return 0, errors.New("divisão por zero")
        }
        return a / b, nil
    }
    
    func main() {
        result, err := divide(10, 0)
        if err != nil {
            fmt.Println("Erro:", err)
        } else {
            fmt.Println("Resultado:", result)
        }
    }
  2. Modifique a função divide para usar fmt.Errorf e incluir os valores de a e b na mensagem de erro.

    ✓ Resposta:
    func divide(a, b int) (int, error) {
        if b == 0 {
            return 0, fmt.Errorf("divisão por zero: %d / %d", a, b)
        }
        return a / b, nil
    }
  3. Crie um erro sentinela chamado ErrDivisionByZero e use-o na função divide. Depois, verifique se o erro é igual a esse sentinela usando errors.Is.

    ✓ Resposta:
    package main
    
    import (
        "errors"
        "fmt"
    )
    
    var ErrDivisionByZero = errors.New("divisão por zero")
    
    func divide(a, b int) (int, error) {
        if b == 0 {
            return 0, ErrDivisionByZero
        }
        return a / b, nil
    }
    
    func main() {
        _, err := divide(10, 0)
        if errors.Is(err, ErrDivisionByZero) {
            fmt.Println("Erro: divisão por zero detectada")
        }
    }
  4. Implemente uma função readFile(filename string) (string, error) que tenta ler um arquivo e retorna seu conteúdo como string. Use os.ReadFile e empacote o erro com contexto usando fmt.Errorf com %w.

    ✓ Resposta:
    package main
    
    import (
        "fmt"
        "os"
    )
    
    func readFile(filename string) (string, error) {
        data, err := os.ReadFile(filename)
        if err != nil {
            return "", fmt.Errorf("falha ao ler arquivo %s: %w", filename, err)
        }
        return string(data), nil
    }
  5. Crie um tipo de erro personalizado chamado ValidationError que tenha campos Field e Message. Implemente o método Error() string. Depois, use esse tipo em uma função de validação.

    ✓ Resposta:
    package main
    
    import "fmt"
    
    type ValidationError struct {
        Field   string
        Message string
    }
    
    func (e *ValidationError) Error() string {
        return fmt.Sprintf("validação falhou no campo %s: %s", e.Field, e.Message)
    }
    
    func validateName(name string) error {
        if name == "" {
            return &ValidationError{Field: "name", Message: "não pode ser vazio"}
        }
        return nil
    }
    
    func main() {
        err := validateName("")
        if err != nil {
            fmt.Println(err)
        }
    }