O Mecanismo de Erros em Go
Go adota uma abordagem pragmática para tratamento de erros, diferente de linguagens que utilizam exceções (como Java ou Python). Em vez de usar try-catch ou mecanismos de exceção, Go trata erros como valores. Isso significa que você passa erros como retorno de função, exatamente como faria com qualquer outro valor. Essa filosofia simplifica o raciocínio sobre o fluxo do programa, pois o tratamento de erro é explícito e visível no código.
A base desse sistema é a interface error, um tipo simples mas poderoso. Qualquer tipo que implementa o método Error() string satisfaz essa interface. Vejamos como funciona na prática:
package main
import (
"fmt"
)
// Implementando a interface error manualmente
type MeuErro struct {
mensagem string
codigo int
}
func (e MeuErro) Error() string {
return fmt.Sprintf("Erro [%d]: %s", e.codigo, e.mensagem)
}
func conectarBancoDados(host string) (string, error) {
if host == "" {
return "", MeuErro{
mensagem: "Host não pode estar vazio",
codigo: 400,
}
}
return "Conexão estabelecida", nil
}
func main() {
resultado, err := conectarBancoDados("")
if err != nil {
fmt.Println(err) // Output: Erro [400]: Host não pode estar vazio
return
}
fmt.Println(resultado)
}
O padrão acima mostra que qualquer tipo pode ser um erro em Go. Não há herança ou classe base mágica — apenas o contrato da interface. Isso oferece flexibilidade e clareza: você sabe exatamente o que pode dar errado em uma função porque ela declara seu retorno como error.
Sentinel Errors: Comparações por Identidade
Sentinel errors são valores de erro específicos que você define para representar situações bem conhecidas e esperadas. O termo "sentinel" refere-se a um valor especial usado como marcador. Em Go, você cria constants ou variables de erro e as compara por identidade usando ==. Esse padrão é extremamente útil quando você quer que o chamador distingua entre diferentes tipos de falha.
A biblioteca padrão de Go usa este padrão extensivamente. Considere io.EOF — é um valor de erro predefinido que indica fim de arquivo. Você não precisa verificar se a mensagem contém "EOF"; você compara diretamente:
package main
import (
"errors"
"fmt"
)
// Definindo sentinel errors no escopo do pacote
var (
ErrUsuarioNaoEncontrado = errors.New("usuário não encontrado")
ErrSenhaInvalida = errors.New("senha inválida")
ErrBancoIndisponivel = errors.New("banco de dados indisponível")
)
func autenticar(username, senha string) error {
if username == "" {
return ErrUsuarioNaoEncontrado
}
if senha != "correctPassword123" {
return ErrSenhaInvalida
}
return nil
}
func main() {
// Comparação por identidade
err := autenticar("joao", "senhaErrada")
if err == ErrSenhaInvalida {
fmt.Println("Acesso negado: credenciais incorretas")
} else if err == ErrUsuarioNaoEncontrado {
fmt.Println("Acesso negado: usuário inexistente")
} else if err == ErrBancoIndisponivel {
fmt.Println("Tente novamente mais tarde")
}
}
O grande benefício dos sentinel errors é a performance e clareza. Você não precisa fazer parsing de strings de erro ou usar reflexão — é uma comparação de ponteiro rápida. No entanto, há uma limitação importante: sentinel errors não carregam contexto. Se você precisar adicionar informações dinâmicas (como qual recurso não foi encontrado, ou qual foi o timeout), você precisará avançar para tipos de erro customizados.
Erros Customizados: Estruturas com Contexto
Quando sentinel errors não são suficientes, você cria tipos de erro que implementam a interface error e carregam informações adicionais. Erros customizados permitem que você forneça contexto rico sobre o que deu errado, facilitando debugging e tratamento mais sofisticado.
Estrutura Básica de um Erro Customizado
package main
import (
"fmt"
"time"
)
// Erro customizado que carrega informações úteis
type ErroConexao struct {
Host string
Porta int
Tentativas int
UltimaConexao time.Time
MensagemOriginal error
}
func (e *ErroConexao) Error() string {
return fmt.Sprintf(
"Falha ao conectar em %s:%d após %d tentativas. Última tentativa: %s. Erro original: %v",
e.Host, e.Porta, e.Tentativas, e.UltimaConexao.Format(time.RFC3339), e.MensagemOriginal,
)
}
// Método auxiliar para extrair informações específicas
func (e *ErroConexao) Recuperavel() bool {
// Erros de conexão geralmente são recuperáveis
return true
}
func conectarComRetentativa(host string, porta int) error {
for tentativa := 1; tentativa <= 3; tentativa++ {
// Simulando falha
if tentativa < 3 {
continue
}
}
return &ErroConexao{
Host: host,
Porta: porta,
Tentativas: 3,
UltimaConexao: time.Now(),
MensagemOriginal: fmt.Errorf("connection timeout"),
}
}
func main() {
err := conectarComRetentativa("database.example.com", 5432)
// Extração de informações usando type assertion
if erroConn, ok := err.(*ErroConexao); ok {
fmt.Println("Erro de conexão detectado")
fmt.Println("Host:", erroConn.Host)
fmt.Println("É recuperável?", erroConn.Recuperavel())
fmt.Println("Mensagem:", err)
}
}
Validação e Erros Customizados
Um caso muito comum é validação de dados. Você pode criar tipos de erro que armazenam múltiplos erros de validação:
package main
import (
"fmt"
"strings"
)
type ErroValidacao struct {
Campo string
Motivo string
}
func (e ErroValidacao) Error() string {
return fmt.Sprintf("Campo '%s' inválido: %s", e.Campo, e.Motivo)
}
// Agregar múltiplos erros
type ErroValidacaoMultipla struct {
Erros []ErroValidacao
}
func (e ErroValidacaoMultipla) Error() string {
if len(e.Erros) == 0 {
return "Nenhum erro de validação"
}
mensagens := make([]string, len(e.Erros))
for i, err := range e.Erros {
mensagens[i] = err.Error()
}
return strings.Join(mensagens, "; ")
}
type Usuario struct {
Nome string
Email string
Idade int
}
func validarUsuario(u Usuario) error {
var erros []ErroValidacao
if u.Nome == "" {
erros = append(erros, ErroValidacao{"Nome", "não pode estar vazio"})
}
if !strings.Contains(u.Email, "@") {
erros = append(erros, ErroValidacao{"Email", "formato inválido"})
}
if u.Idade < 18 {
erros = append(erros, ErroValidacao{"Idade", "deve ser maior que 18"})
}
if len(erros) > 0 {
return ErroValidacaoMultipla{Erros: erros}
}
return nil
}
func main() {
user := Usuario{
Nome: "",
Email: "email-invalido",
Idade: 15,
}
if err := validarUsuario(user); err != nil {
fmt.Println(err)
}
// Type assertion para acessar todos os erros
if multi, ok := err.(ErroValidacaoMultipla); ok {
fmt.Println("Quantidade de erros:", len(multi.Erros))
for _, e := range multi.Erros {
fmt.Printf("- %s\n", e)
}
}
}
Práticas Recomendadas e Padrões Avançados
Wrapping de Erros com Context
A partir do Go 1.13, você pode usar fmt.Errorf com o verbo %w para envolver erros mantendo a cadeia de erro. Isso permite que ferramentas como errors.Is() e errors.As() funcionem corretamente:
package main
import (
"errors"
"fmt"
)
var ErrBancoDados = errors.New("erro no banco de dados")
func buscarUsuario(id int) (string, error) {
// Simulando erro no banco de dados
if id < 0 {
// Envolvendo o erro com contexto adicional
return "", fmt.Errorf("falha ao buscar usuário com ID %d: %w", id, ErrBancoDados)
}
return "João Silva", nil
}
func processarPedido(userID int) error {
usuario, err := buscarUsuario(userID)
if err != nil {
// Adicionando mais camadas de contexto
return fmt.Errorf("não foi possível processar pedido: %w", err)
}
fmt.Printf("Processando pedido para: %s\n", usuario)
return nil
}
func main() {
err := processarPedido(-1)
// Verificar se o erro original está na cadeia
if errors.Is(err, ErrBancoDados) {
fmt.Println("Problema no banco de dados detectado na cadeia de erro")
}
// Extrair o erro específico
var erroDB error
if errors.As(err, &erroDB) {
fmt.Printf("Erro extraído: %v\n", erroDB)
}
fmt.Println("Erro completo:", err)
}
Padrão de Erro com Tipo Específico
Para casos onde você quer permitir que o chamador tome ações específicas, você pode criar tipos de erro que implementam interfaces adicionais:
package main
import (
"fmt"
"time"
)
// Interface para erros que podem ser temporários
type TemporariamenteIndisponivel interface {
error
Temporario() bool
Apos() time.Time
}
type ErroTaxa struct {
mensagem string
tentarApos time.Time
}
func (e ErroTaxa) Error() string {
return fmt.Sprintf("%s. Tente novamente após %s", e.mensagem, e.tentarApos.Format(time.RFC3339))
}
func (e ErroTaxa) Temporario() bool {
return true
}
func (e ErroTaxa) Apos() time.Time {
return e.tentarApos
}
func fazerRequisicao() error {
return ErroTaxa{
mensagem: "Taxa de requisições excedida",
tentarApos: time.Now().Add(5 * time.Minute),
}
}
func executarComRetentativa(fn func() error) {
err := fn()
if err != nil {
// Verificar se é temporário
if temp, ok := err.(TemporariamenteIndisponivel); ok && temp.Temporario() {
fmt.Printf("Erro temporário. Aguarde até %s antes de tentar novamente\n", temp.Apos())
} else {
fmt.Printf("Erro permanente: %v\n", err)
}
}
}
func main() {
executarComRetentativa(fazerRequisicao)
}
Conclusão
Dominar o sistema de erros em Go significa entender três conceitos distintos e quando usar cada um. Primeiro, a interface error é a base — qualquer tipo que implemente Error() string é um erro válido, oferecendo flexibilidade máxima. Segundo, sentinel errors são a escolha para situações bem definidas quando você só precisa sinalizar "qual tipo de erro", sem contexto adicional — use == para compará-los com eficiência. Terceiro, erros customizados são quando você precisa carregar contexto e informações dinâmicas — crie structs que implementem error e potencialmente outras interfaces para fornecer comportamentos específicos.
A prática de usar fmt.Errorf com %w para envolver erros (Go 1.13+) é fundamental para código moderno, permitindo que bibliotecas como errors.Is() funcionem corretamente e facilitando debugging em camadas profundas da aplicação.