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
- Erros sentinela mutáveis:
io.EOFpode ser encapsulado com contexto (fmt.Errorf("read 1.txt eof: %w", io.EOF)), mas também pode ser sobrescrito (io.EOF = fmt.Errorf("atrapalhar")). - Dificuldade em identificar tipos de erro:
net.Erroré uma interface, eOpErroré uma implementação que contém um campoOp(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