Go Admin

Erros em Go: error Interface, Sentinel Errors e Erros Customizados na Prática Já leu

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 , um tipo simples mas poderoso. Qualquer tipo que implementa o método satisfaz essa interface. Vejamos como funciona na prática: 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 . Sentinel Errors: Comparações por Identidade Sentinel errors

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.

Referências


Artigos relacionados