Tratamento de Erros em Go: Como Lidar de Forma Eficaz e Elegante

Muita gente reclama do tratamento de erros em Go. Pessoalmente, depois de alguns meses usando a linguagem, comecei a gostar justamente por isso: o padrão if err != nil me deixa tranquilo, quase sem pensar. Já vi sugestões de ter um atalho no teclado para encurtar isso, mas discordo totalmente—na verdade, acho que precisaríamos de três atalhos diferentes (ou pelo menos dois):

if err != nil {
    return nil, nil, err
}
if err != nil {
    return nil, err
}
if err != nil {
    return err
}

Como Trato Erros Hoje

Na maioria dos casos, a abordagem é: se houver erro, propaga para cima; a camada mais alta registra no log. Exceções surgem com erros sentinela como gorm.ErrNoRecord, io.EOF, http.ErrServerClosed, ou então verificações como os.IsTimeout(err) ou ne, ok := err.(net.Error); ok && ne.Temporary() (apesar de Temporary() estar deprecated, ainda não encontrei substituto adequado). Basicamente, identifico o erro específico do contexto e decido se devo propagá-lo ou tratá-lo ali mesmo. Um exemplo é o io.Copy, que retorna io.EOF como erro—para ele, é um sentinela esperado. Em um servidor TCP, um erro temporário não deve derrubar o serviço; o melhor é tentar novamente depois. No fundo, meu estilo é uma imitação modesta do que a biblioteca padrão faz.

Problemas que Encontrei na Prática

  1. Erros sentinela mutáveis: io.EOF pode ser encapsulado com contexto (fmt.Errorf("read 1.txt eof: %w", io.EOF)), mas também pode ser sobrescrito (io.EOF = fmt.Errorf("atrapalhar")).
  2. Dificuldade em identificar tipos de erro: net.Error é uma interface, e OpError é uma implementação que contém um campo Op (dial, read, write, close). Na prática, acabo comparando strings (strings.Contains(err.Error(), "use of closed network connection")) para detectar certos erros—feio, mas muitas vezes é o único caminho.

Práticas Concretas (Baseadas em Leituras e Experiência)

1. Nunca ignore erros

Cada erro deve ser avaliado antes de ser descartado ou repassado. É verboso, mas confiável:

err = db.Find(&dest1).Error
if err != nil {
    return err
}
err = db.Find(&dest1).Error
if err != nil {
    return err
}
err = db.Take(&optionalItem).Error
if err != nil && err != gorm.ErrNoRecord {
    return err
}

Há casos em que o erro é inevitável, como no fechamento de uma conexão—a biblioteca padrão também não trata de forma especial:

func handle(conn net.Conn) error {
    defer func() {
        if errClose := conn.Close(); errClose != nil {
            fmt.Println("erro ao fechar:", errClose)
        }
    }()
    // lógica principal
    return nil
}

2. Adicione stack trace e contexto quando necessário

Em projetos complexos, uma string de erro simples não ajuda a localizar a origem. A biblioteca github.com/pkg/errors oferece WithStack, WithMessage, Is. Exemplo:

func lerArquivo(caminho string) error {
    return errors.Wrap(io.EOF, "erro ao ler "+caminho)
}

func TestErros(t *testing.T) {
    err := lerArquivo("dados.txt")
    fmt.Println(errors.Is(err, io.EOF)) // true
    fmt.Printf("%+v", err)
    // Saída inclui stack trace e mensagem
}

3. Não trate o mesmo erro duas vezes

Se a função lerArquivo deecide propagar o erro, ela não deve registrá-lo também. Isso evita duplicação.

Minha Própria Biblioteca: errx

Por que criei uma biblioteca em cima do pkg/errors? O motivo principal foi evitar stack traces duplicados. O errx.Wrap verifica se o erro já possui stack trace antes de adicionar um novo, eliminando redundância. Uso praticamente só fmt.Errorf e errx.Wrap, além de errors.Is. Exemplo:

package errx

import (
    "fmt"
    "net/http"
    "testing"
)

func TestWrap(t *testing.T) {
    _, err := abrir2("xx")
    fmt.Println(Format(err))
}

func abrir(caminho string) ([]byte, error) {
    _, err := http.Get(caminho)
    if err != nil {
        return nil, Wrap(err, "abrir")
    }
    return []byte("OK"), nil
}

func abrir2(caminho string) ([]byte, error) {
    resultado, err := abrir(caminho)
    if err != nil {
        return resultado, Wrap(err, "abrir2 falhou")
    }
    return nil, nil
}

func abrir3(caminho string) ([]byte, error) {
    resultado, err := abrir2(caminho)
    if err != nil {
        return resultado, err
    }
    return nil, nil
}

// Saída esperada: mensagem com stack trace sem duplicação

Repositório do errx: github.com/xiaotusaoxia/errx

Tags: go Tratamento de Erros erros sentinela pkg/errors stack trace

Publicado em 7-6 03:39