Erros em Go: o tipo error e a convenção de retorno

1. Filosofia de erros em Go: sem exceções, tudo é valor

1.1. Diferença fundamental entre Go e linguagens com try/catch

Diferente de linguagens como Java, Python ou JavaScript, Go não possui mecanismos de exceção (try/catch/finally). Em vez disso, Go adota uma abordagem explícita e direta: erros são valores comuns que retornam das funções. Essa filosofia elimina a necessidade de blocos especiais de tratamento e torna o fluxo de erro visível no código.

1.2. Erro como retorno explícito: controle de fluxo no código

Em Go, o erro é tratado como qualquer outro valor de retorno. Isso força o programador a lidar com o erro no ponto onde ele ocorre, tornando o código mais previsível e legível. Não há surpresas: se uma função pode falhar, ela retorna um erro.

1.3. A importância da verificação constante: if err != nil

O padrão mais comum em Go é verificar erros imediatamente após chamar uma função que pode falhar:

file, err := os.Open("arquivo.txt")
if err != nil {
    // trata o erro
    return err
}
defer file.Close()

Essa verificação constante pode parecer repetitiva, mas é intencional: cada erro deve ser tratado ou propagado explicitamente.

2. O tipo error como interface nativa

2.1. Definição da interface error: o método Error() string

O tipo error é uma interface nativa do Go, definida como:

type error interface {
    Error() string
}

Qualquer tipo que implemente o método Error() que retorna uma string satisfaz automaticamente essa interface.

2.2. Qualquer tipo que implemente Error() é um erro

Isso significa que você pode criar seus próprios tipos de erro personalizados:

type MyError struct {
    Code    int
    Message string
}

func (e *MyError) Error() string {
    return fmt.Sprintf("erro %d: %s", e.Code, e.Message)
}

2.3. O valor nil como representação de "sem erro"

O valor nil representa a ausência de erro. Funções que executam com sucesso retornam nil no lugar do erro. Lembre-se: error é uma interface, então nil só é verdadeiro quando tanto o tipo quanto o valor são nil.

var err error = nil
fmt.Println(err == nil) // true

var e *MyError = nil
err = e
fmt.Println(err == nil) // false! (interface com tipo não-nil)

3. Convenção de retorno de erros em funções

3.1. Padrão: último valor de retorno é error

A convenção estabelece que o erro deve ser o último valor retornado por uma função:

func Dividir(a, b float64) (float64, error) {
    if b == 0 {
        return 0, fmt.Errorf("divisão por zero")
    }
    return a / b, nil
}

3.2. Retornar nil para sucesso, valor não-nil para falha

func BuscarUsuario(id int) (*Usuario, error) {
    if id <= 0 {
        return nil, errors.New("ID inválido")
    }
    usuario := &Usuario{ID: id, Nome: "João"}
    return usuario, nil
}

3.3. Exemplos práticos: funções que retornam (T, error)

func ConverteInteiro(s string) (int, error) {
    valor, err := strconv.Atoi(s)
    if err != nil {
        return 0, fmt.Errorf("falha ao converter '%s': %w", s, err)
    }
    return valor, nil
}

4. Criando erros simples com errors.New e fmt.Errorf

4.1. errors.New("mensagem"): erros literais

import "errors"

var ErrSaldoInsuficiente = errors.New("saldo insuficiente para saque")

4.2. fmt.Errorf("formato %v", valor): erros com formatação

func Sacar(conta *Conta, valor float64) error {
    if valor > conta.Saldo {
        return fmt.Errorf("saque de %.2f excede saldo de %.2f", valor, conta.Saldo)
    }
    conta.Saldo -= valor
    return nil
}

4.3. Boas práticas: mensagens descritivas e sem capitalização

Mensagens de erro em Go geralmente começam com letra minúscula e não terminam com pontuação, para facilitar a composição com outras mensagens.

5. Inspeção e comparação de erros

5.1. Comparação direta com ==: limitações

Comparar erros com == funciona apenas para erros idênticos (mesmo ponteiro):

if err == ErrSaldoInsuficiente {
    // trata erro específico
}

5.2. Uso de errors.Is para verificar sentinel errors

errors.Is verifica se um erro corresponde a um erro alvo, percorrendo a cadeia de wrappers:

if errors.Is(err, ErrSaldoInsuficiente) {
    fmt.Println("Erro de saldo insuficiente detectado")
}

5.3. Uso de errors.As para verificar tipos específicos

errors.As encontra o primeiro erro na cadeia que corresponde a um tipo específico:

var netErr *net.DNSError
if errors.As(err, &netErr) {
    fmt.Printf("Erro de DNS: %v\n", netErr)
}

6. Erros customizados: implementando a interface error

6.1. Structs que implementam Error() string

type ErroHTTP struct {
    StatusCode int
    Mensagem   string
}

func (e *ErroHTTP) Error() string {
    return fmt.Sprintf("HTTP %d: %s", e.StatusCode, e.Mensagem)
}

6.2. Adicionando campos extras (código HTTP, detalhes, etc.)

func FazerRequisicao(url string) ([]byte, error) {
    resp, err := http.Get(url)
    if err != nil {
        return nil, fmt.Errorf("requisição falhou: %w", err)
    }
    defer resp.Body.Close()

    if resp.StatusCode != http.StatusOK {
        return nil, &ErroHTTP{
            StatusCode: resp.StatusCode,
            Mensagem:   "resposta não esperada",
        }
    }

    return io.ReadAll(resp.Body)
}

6.3. Quando criar um tipo customizado vs. usar erros simples

Use erros simples (errors.New / fmt.Errorf) para mensagens descartáveis. Crie tipos customizados quando precisar de informações adicionais para tratamento específico.

7. Sentinela de erros e boas práticas de design

7.1. Definindo sentinel errors como variáveis de pacote

package banco

var (
    ErrSaldoInsuficiente = errors.New("saldo insuficiente")
    ErrContaBloqueada    = errors.New("conta bloqueada")
    ErrValorInvalido     = errors.New("valor inválido para operação")
)

7.2. Evitando exposição de detalhes internos

Não exponha detalhes de implementação nos erros. Use wrapping seletivo:

func processarDados() error {
    err := acessoInterno()
    if err != nil {
        return fmt.Errorf("processamento falhou: %w", err)
    }
    return nil
}

7.3. Erros como parte da API pública: documentação e previsibilidade

Documente quais erros sua função pode retornar. Isso ajuda quem consome sua API a tratar casos de erro adequadamente.

8. Padrões avançados de retorno e tratamento

8.1. Retorno múltiplo com erro: defer e cleanup em caso de falha

func ProcessarArquivo(nome string) error {
    f, err := os.Open(nome)
    if err != nil {
        return err
    }
    defer f.Close()

    // Se algo falhar, o defer garante o fechamento
    dados, err := io.ReadAll(f)
    if err != nil {
        return fmt.Errorf("leitura falhou: %w", err)
    }

    return processar(dados)
}

8.2. Erros em funções variádicas e closures

func SomarValores(valores ...int) (int, error) {
    if len(valores) == 0 {
        return 0, errors.New("pelo menos um valor é necessário")
    }

    total := 0
    for _, v := range valores {
        if v < 0 {
            return 0, fmt.Errorf("valor negativo não permitido: %d", v)
        }
        total += v
    }

    return total, nil
}

8.3. Tratamento silencioso vs. propagação: quando ignorar ou repassar

// Ignorar intencionalmente (raro e documentado)
_, _ = fmt.Println("ignorando erro") // não faça isso sem motivo

// Propagar com contexto adicional
if err := operacao(); err != nil {
    return fmt.Errorf("etapa crítica falhou: %w", err)
}

Referências