A primeira coisa que surpreende programadores vindos de Python, Java ou C# ao estudar Go é a ausência de exceções. Não existe try, não existe catch, não existe throw. Em Go, erros são valores comuns — retornados por funções, armazenados em variáveis, passados como argumentos e inspecionados com if.
Essa escolha não é uma limitação. É uma decisão filosófica com consequências práticas muito positivas: o fluxo de erros se torna explícito, visível no código e auditável. Para usar o resultado de uma função, o programador precisa receber também o erro, e o descarte fica escrito no código, com _. A garantia tem um limite que vale conhecer desde o início: uma chamada usada como instrução, sem nada à esquerda — os.Remove(caminho) sozinho numa linha —, descarta o erro em silêncio, e nem o compilador nem o go vet reclamam. O linter errcheck, ativo por padrão no golangci-lint, existe para fechar essa brecha.
A interface error
O tipo error em Go é uma interface simples definida na biblioteca padrão:
type error interface {
Error() string
}
Qualquer tipo que implemente o método Error() string é um error. Essa simplicidade é o que permite que erros sejam tão flexíveis — podem carregar qualquer informação adicional necessária além da mensagem.
Criando erros simples
O pacote errors da biblioteca padrão oferece a forma mais direta de criar um valor de erro:
package main
import (
"errors"
"fmt"
)
func dividir(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("divisão por zero")
}
return a / b, nil
}
func main() {
resultado, err := dividir(10, 2)
if err != nil {
fmt.Println("Erro:", err)
return
}
fmt.Printf("Resultado: %.2f\n", resultado) // 5.00
_, err = dividir(5, 0)
if err != nil {
fmt.Println("Erro:", err) // Erro: divisão por zero
}
}
O valor nil como erro significa ausência de falha — a operação foi bem-sucedida. A convenção é sempre retornar nil quando não há erro.
Formatando erros com fmt.Errorf
O pacote fmt oferece fmt.Errorf para criar erros com mensagens formatadas, semelhante ao fmt.Sprintf:
func buscarUsuario(id int) (*Usuario, error) {
if id <= 0 {
return nil, fmt.Errorf("id inválido: %d", id)
}
// ...
return nil, fmt.Errorf("usuário %d não encontrado", id)
}
Erros personalizados com contexto
Quando um erro precisa carregar informações estruturadas além de uma mensagem textual, cria-se um tipo personalizado que implementa a interface error:
package main
import "fmt"
type ErroValidacao struct {
Campo string
Valor any
Motivo string
}
func (e *ErroValidacao) Error() string {
return fmt.Sprintf("validação falhou no campo '%s' (valor: %v): %s",
e.Campo, e.Valor, e.Motivo)
}
type ErroHTTP struct {
Codigo int
Metodo string
URL string
}
func (e *ErroHTTP) Error() string {
return fmt.Sprintf("HTTP %d ao chamar %s %s", e.Codigo, e.Metodo, e.URL)
}
func validarIdade(idade int) error {
if idade < 0 {
return &ErroValidacao{
Campo: "idade",
Valor: idade,
Motivo: "não pode ser negativa",
}
}
if idade > 150 {
return &ErroValidacao{
Campo: "idade",
Valor: idade,
Motivo: "valor improvável",
}
}
return nil
}
func main() {
if err := validarIdade(-5); err != nil {
fmt.Println(err)
// Inspecionando o tipo concreto
if e, ok := err.(*ErroValidacao); ok {
fmt.Printf("Campo problemático: %s\n", e.Campo)
}
}
}
Wrapping: embrulhando erros com contexto
Uma prática essencial em Go é adicionar contexto ao propagar erros pela pilha de chamadas. Em vez de retornar o erro original diretamente, embrulha-se ele com informações sobre o que estava acontecendo:
func lerArquivo(caminho string) ([]byte, error) {
dados, err := os.ReadFile(caminho)
if err != nil {
return nil, fmt.Errorf("lerArquivo: falha ao ler %s: %w", caminho, err)
}
return dados, nil
}
func carregarConfig(caminho string) (*Config, error) {
dados, err := lerArquivo(caminho)
if err != nil {
return nil, fmt.Errorf("carregarConfig: %w", err)
}
// ...
}
O verbo %w — disponível desde Go 1.13 — embrulha o erro original, preservando-o para inspeção posterior. A diferença para %v é decisiva: fmt.Errorf("ctx: %v", err) produz a mesma mensagem, mas guarda só o texto, e errors.Is deixa de encontrar o erro original. Desde o Go 1.20, um mesmo fmt.Errorf aceita mais de um %w, e os dois erros embrulhados são encontrados por errors.Is. O resultado é uma cadeia de erros com contexto acumulado:
carregarConfig: lerArquivo: falha ao ler config.json: open config.json: no such file or directory
Cada camada adiciona seu contexto, formando uma trilha que facilita o diagnóstico sem ocultar a causa raiz.
errors.Is: verificando erros específicos
Com erros embrulhados, verificar err == ErrEspecifico não funciona — o erro está embrulhado em outros. A função errors.Is percorre toda a cadeia de wrapping:
package main
import (
"errors"
"fmt"
"os"
)
func lerConfig(caminho string) error {
_, err := os.ReadFile(caminho)
if err != nil {
return fmt.Errorf("lerConfig: %w", err)
}
return nil
}
func main() {
err := lerConfig("inexistente.json")
if errors.Is(err, os.ErrNotExist) {
fmt.Println("Arquivo não encontrado — usando configuração padrão")
} else if err != nil {
fmt.Println("Erro inesperado:", err)
}
}
errors.Is verifica se algum erro na cadeia é igual ao alvo — tanto por comparação direta quanto pelo método Is(error) bool se o tipo o implementar.
errors.As: extraindo o tipo concreto da cadeia
Enquanto errors.Is verifica identidade, errors.As extrai o primeiro erro na cadeia que corresponde a um tipo específico:
package main
import (
"errors"
"fmt"
)
type ErroConexao struct {
Host string
Porta int
}
func (e *ErroConexao) Error() string {
return fmt.Sprintf("falha ao conectar em %s:%d", e.Host, e.Porta)
}
func conectar(host string, porta int) error {
return fmt.Errorf("iniciarServico: %w",
&ErroConexao{Host: host, Porta: porta})
}
func main() {
err := conectar("localhost", 5432)
var erroConn *ErroConexao
if errors.As(err, &erroConn) {
fmt.Printf("Problema de conexão detectado\n")
fmt.Printf("Host: %s\n", erroConn.Host)
fmt.Printf("Porta: %d\n", erroConn.Porta)
}
}
errors.As percorre a cadeia de wrapping e preenche a variável alvo com o erro encontrado, já com o tipo correto — sem necessidade de type assertion manual. Desde o Go 1.26, a versão genérica errors.AsType dispensa a variável declarada antes: if e, ok := errors.AsType[*ErroConexao](err); ok { ... }.
Erros sentinela: valores de erro pré-definidos
Erros sentinela são variáveis de erro exportadas que representam condições conhecidas. Permitem que os chamadores verifiquem condições específicas com errors.Is:
package main
import (
"errors"
"fmt"
)
// Erros sentinela exportados
var (
ErrNaoEncontrado = errors.New("registro não encontrado")
ErrAcessoNegado = errors.New("acesso negado")
ErrDadosInvalidos = errors.New("dados inválidos")
)
type BancoDados struct {
dados map[int]string
}
func (db *BancoDados) Buscar(id int, usuarioAdmin bool) (string, error) {
if !usuarioAdmin {
return "", fmt.Errorf("Buscar id=%d: %w", id, ErrAcessoNegado)
}
valor, existe := db.dados[id]
if !existe {
return "", fmt.Errorf("Buscar id=%d: %w", id, ErrNaoEncontrado)
}
return valor, nil
}
func main() {
db := &BancoDados{dados: map[int]string{1: "Ricardo", 2: "Ana"}}
_, err := db.Buscar(1, false)
fmt.Println(errors.Is(err, ErrAcessoNegado)) // true
fmt.Println(errors.Is(err, ErrNaoEncontrado)) // false
_, err = db.Buscar(99, true)
fmt.Println(errors.Is(err, ErrNaoEncontrado)) // true
fmt.Println(err) // Buscar id=99: registro não encontrado
}
Múltiplos erros: errors.Join
Desde Go 1.20, errors.Join permite combinar múltiplos erros em um único valor, útil para validações que precisam reportar todos os problemas de uma vez:
package main
import (
"errors"
"fmt"
)
type Formulario struct {
Nome string
Email string
Idade int
}
func (f Formulario) Validar() error {
var erros []error
if f.Nome == "" {
erros = append(erros, errors.New("nome é obrigatório"))
}
if f.Email == "" {
erros = append(erros, errors.New("email é obrigatório"))
}
if f.Idade < 18 {
erros = append(erros, fmt.Errorf("idade mínima é 18, recebido: %d", f.Idade))
}
return errors.Join(erros...)
}
func main() {
f := Formulario{Nome: "", Email: "", Idade: 15}
if err := f.Validar(); err != nil {
fmt.Println("Erros encontrados:")
fmt.Println(err)
}
}
panic e recover: para situações excepcionais
Go possui panic e recover, mas seu uso é reservado para situações verdadeiramente excepcionais — erros de programação irrecuperáveis, como índice fora dos limites ou desreferência de ponteiro nil. Não devem ser usados como substitutos para o fluxo normal de tratamento de erros.
panic interrompe a execução da goroutine atual e começa a desenrolar a pilha. recover pode capturar o pânico dentro de uma função defer:
package main
import "fmt"
func operacaoArriscada() {
defer func() {
if r := recover(); r != nil {
fmt.Println("Pânico recuperado:", r)
}
}()
var s []int
fmt.Println(s[0]) // índice fora dos limites — causa pânico
}
func main() {
operacaoArriscada()
fmt.Println("Execução continua após recover")
}
A regra prática é clara: use error para falhas esperadas e recuperáveis. Reserve panic para condições que indicam bugs no código — estado corrompido, invariantes violadas, uso incorreto de APIs internas.
Boas práticas consolidadas
Adicione contexto ao propagar erros. Cada camada da aplicação deve adicionar informação relevante com %w:
return fmt.Errorf("processarPedido id=%d: %w", id, err)
Trate erros no nível correto. Um erro deve ser tratado — logado, convertido ou respondido ao usuário — apenas uma vez, na camada que tem contexto suficiente para fazer isso.
Não ignore erros com _ em código de produção. O compilador permite descartar erros com _, mas isso deve ser exceção extremamente rara com comentário explicando o motivo.
Erros sentinela para condições conhecidas. Defina variáveis de erro exportadas quando chamadores precisam distinguir condições específicas.
Tipos de erro para dados estruturados. Crie tipos personalizados quando o erro precisa carregar informações além da mensagem.
Tratar erros em Go é, na maior parte do tempo, escrever três coisas: devolver o erro, acrescentar contexto com %w e, na borda, decidir o que ele significa com errors.Is e errors.As. O resto é detalhe que evita armadilhas — o %v que corta a cadeia, o == que não enxerga o erro embrulhado, o log repetido em cada camada, o recover que só vale na própria goroutine. O panic continua existindo, mas como sinal de defeito no programa, não como forma de avisar que algo esperado deu errado.
Fontes e leituras recomendadas
-
Go Blog: Error handling and Go — Artigo fundamental sobre a filosofia de erros em Go. https://go.dev/blog/error-handling-and-go
-
Go Blog: Working with Errors in Go 1.13 — Introdução ao wrapping, errors.Is e errors.As. https://go.dev/blog/go1.13-errors
-
Go Blog: Errors are values — Rob Pike sobre como tratar erros como valores comuns. https://go.dev/blog/errors-are-values
-
Documentação do pacote errors — Referência completa de New, Is, As e Join. https://pkg.go.dev/errors
-
Go by Example: Errors — Exemplos práticos comentados de tratamento de erros. https://gobyexample.com/errors
-
Effective Go — Errors — Boas práticas oficiais para tratamento de erros. https://go.dev/doc/effective_go#errors
Exercícios
Exercício 1
Uma camada de serviço faz return fmt.Errorf("buscando cliente %d: %v", id, err). A mensagem no log fica perfeita, mas o handler HTTP, que faz if errors.Is(err, ErrNaoEncontrado) para devolver 404, passou a devolver 500 para clientes inexistentes. O que mudou?
Ver resposta
✓ Resposta: O verbo. Com %v, o fmt.Errorf formata o erro como texto e cria um erro novo que guarda só essa string; o original se perde. Com %w, a mensagem é idêntica, mas o erro novo guarda uma referência ao original e implementa Unwrap, e é por esse método que errors.Is e errors.As percorrem a cadeia. Num teste, errors.Is(fmt.Errorf("ctx: %v", ErrA), ErrA) dá false, e com %w dá true. Como a mensagem não muda, o defeito passa despercebido no log e só aparece no comportamento, aqui transformando 404 em 500. A regra é usar %w sempre que o chamador puder precisar reconhecer a causa. O %v é a escolha deliberada para esconder a causa, quando um detalhe interno, como o erro específico do driver do banco, não deve virar parte do contrato do pacote.
Exercício 2
Para tratar arquivo inexistente, alguém escreve if err == fs.ErrNotExist depois de um os.Open. O arquivo não existe, e mesmo assim o if não entra. Por quê, se os.Open nem passou por nenhum fmt.Errorf?
Ver resposta
✓ Resposta: Porque o erro devolvido por os.Open já é um embrulho: um *fs.PathError, que guarda a operação (open), o caminho e o erro do sistema, no Linux o ENOENT. É desse tipo que vem a mensagem open nao-existe.json: no such file or directory. A comparação com == olha só o valor mais externo, que é o *PathError, e dá false; errors.Is(err, fs.ErrNotExist) desce pela cadeia, chega ao erro do sistema, que declara equivaler a ErrNotExist, e dá true. Num teste com o mesmo arquivo inexistente, as duas formas deram exatamente false e true. A lição é que == contra erro sentinela só é seguro quando a documentação garante que o valor volta sem embrulho, como io.EOF do Read; em todos os outros casos, a forma certa é errors.Is. Para obter o caminho que falhou, errors.As com um *fs.PathError, ou errors.AsType desde o Go 1.26, extrai o embrulho inteiro.
Exercício 3
A função Validar() do artigo devolve errors.Join(erros...). Um colega teme dois problemas: que ela devolva um erro "vazio", mas não nil, quando não há falhas; e que a mensagem quebre o formato de uma linha por evento do log. Os temores procedem?
Ver resposta
✓ Resposta: O primeiro não procede: errors.Join ignora os argumentos nil e devolve nil quando não sobra nenhum erro. errors.Join(nil, nil) == nil dá true, e o mesmo vale para uma fatia vazia, então o if err := f.Validar(); err != nil funciona como esperado. O segundo procede: a mensagem é a concatenação das mensagens com quebra de linha entre elas; no teste, "nome é obrigatório\nemail é obrigatório". Num log de texto, isso vira duas linhas que o agregador trata como dois eventos, e a segunda aparece sem data nem contexto. Há duas saídas. Uma é registrar o erro como campo de um log estruturado, em que a quebra de linha fica escapada dentro do valor. A outra, quando os erros individuais importam, como numa resposta de API que lista os campos inválidos, é percorrê-los: o erro devolvido por Join tem o método Unwrap() []error, e cada item pode ser formatado do jeito que o destino exige.
Exercício 4
Em um projeto, cada camada faz log.Printf("erro: %v", err) e depois return fmt.Errorf("...: %w", err). Numa falha de banco, o mesmo problema aparece cinco vezes no log, com prefixos diferentes. O que a seção de boas práticas do artigo diz sobre isso, e como reorganizar?
Ver resposta
✓ Resposta: Diz que um erro deve ser tratado apenas uma vez, e registrar no log é tratar. Cada camada que loga e também devolve está tratando e repassando ao mesmo tempo, e o resultado são cinco registros do mesmo incidente, que multiplicam o volume de log e confundem quem investiga, porque parecem cinco falhas diferentes. A reorganização é separar os papéis. As camadas intermediárias só acrescentam contexto com %w e devolvem, sem logar. A borda, que no caso de um servidor HTTP é o handler ou um middleware, recebe a cadeia completa, decide a resposta (usando errors.Is e errors.As para escolher entre 404, 409 ou 500) e loga uma vez só. Como o %w acumulou o contexto de cada camada, essa única linha, algo como processarPedido id=7: salvando itens: conexão recusada, conta a história inteira. A exceção são erros que uma camada decide engolir, como uma falha de cache que não impede a operação: esses são tratados ali mesmo, e o log naquele ponto é a forma de não perdê-los.
Exercício 5
Para "nunca derrubar o processo", um serviço registra um defer com recover no main. Mesmo assim, o processo cai com panic: assignment to entry in nil map vindo de um worker iniciado com go func(). Por que o recover do main não pegou, e onde ele deveria estar?
Ver resposta
✓ Resposta: Porque o recover só intercepta pânicos da mesma goroutine em que está a função adiada. O pânico desenrola a pilha da goroutine onde aconteceu, e a do worker não passa pelo main; quando ela termina sem ninguém recuperar, o runtime encerra o processo inteiro. Num teste com o recover no main e a escrita num map nil dentro de uma goroutine, a saída é o pânico com a pilha, código de saída 2, e nem a mensagem do recover nem a do fim do main aparecem. Para proteger os workers, o defer com recover precisa estar dentro da função de cada goroutine, de preferência num pequeno invólucro reutilizável que registre o pânico e a pilha, com debug.Stack(), e converta o problema em erro para quem coordena. É o que o net/http faz por conta própria para cada requisição. Vale a advertência do artigo: isso é rede de segurança para bugs, e o pânico recuperado precisa aparecer em alerta e ser corrigido, não virar rotina.