Go

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

11 min de leitura

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

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

<h2>O Mecanismo de Erros em Go</h2>

<p>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 <strong>valores</strong>. 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.</p>

<p>A base desse sistema é a <strong>interface <code>error</code></strong>, um tipo simples mas poderoso. Qualquer tipo que implementa o método <code>Error() string</code> satisfaz essa interface. Vejamos como funciona na prática:</p>

<pre><code class="language-go">package main

import (

&quot;fmt&quot;

)

// Implementando a interface error manualmente

type MeuErro struct {

mensagem string

codigo int

}

func (e MeuErro) Error() string {

return fmt.Sprintf(&quot;Erro [%d]: %s&quot;, e.codigo, e.mensagem)

}

func conectarBancoDados(host string) (string, error) {

if host == &quot;&quot; {

return &quot;&quot;, MeuErro{

mensagem: &quot;Host não pode estar vazio&quot;,

codigo: 400,

}

}

return &quot;Conexão estabelecida&quot;, nil

}

func main() {

resultado, err := conectarBancoDados(&quot;&quot;)

if err != nil {

fmt.Println(err) // Output: Erro [400]: Host não pode estar vazio

return

}

fmt.Println(resultado)

}</code></pre>

<p>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 <code>error</code>.</p>

<h2>Sentinel Errors: Comparações por Identidade</h2>

<p>Sentinel errors são <strong>valores de erro específicos</strong> que você define para representar situações bem conhecidas e esperadas. O termo &quot;sentinel&quot; refere-se a um valor especial usado como marcador. Em Go, você cria constants ou variables de erro e as compara por identidade usando <code>==</code>. Esse padrão é extremamente útil quando você quer que o chamador distingua entre diferentes tipos de falha.</p>

<p>A biblioteca padrão de Go usa este padrão extensivamente. Considere <code>io.EOF</code> — é um valor de erro predefinido que indica fim de arquivo. Você não precisa verificar se a mensagem contém &quot;EOF&quot;; você compara diretamente:</p>

<pre><code class="language-go">package main

import (

&quot;errors&quot;

&quot;fmt&quot;

)

// Definindo sentinel errors no escopo do pacote

var (

ErrUsuarioNaoEncontrado = errors.New(&quot;usuário não encontrado&quot;)

ErrSenhaInvalida = errors.New(&quot;senha inválida&quot;)

ErrBancoIndisponivel = errors.New(&quot;banco de dados indisponível&quot;)

)

func autenticar(username, senha string) error {

if username == &quot;&quot; {

return ErrUsuarioNaoEncontrado

}

if senha != &quot;correctPassword123&quot; {

return ErrSenhaInvalida

}

return nil

}

func main() {

// Comparação por identidade

err := autenticar(&quot;joao&quot;, &quot;senhaErrada&quot;)

if err == ErrSenhaInvalida {

fmt.Println(&quot;Acesso negado: credenciais incorretas&quot;)

} else if err == ErrUsuarioNaoEncontrado {

fmt.Println(&quot;Acesso negado: usuário inexistente&quot;)

} else if err == ErrBancoIndisponivel {

fmt.Println(&quot;Tente novamente mais tarde&quot;)

}

}</code></pre>

<p>O grande benefício dos sentinel errors é a <strong>performance e clareza</strong>. 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.</p>

<h2>Erros Customizados: Estruturas com Contexto</h2>

<p>Quando sentinel errors não são suficientes, você cria tipos de erro que implementam a interface <code>error</code> 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.</p>

<h3>Estrutura Básica de um Erro Customizado</h3>

<pre><code class="language-go">package main

import (

&quot;fmt&quot;

&quot;time&quot;

)

// 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(

&quot;Falha ao conectar em %s:%d após %d tentativas. Última tentativa: %s. Erro original: %v&quot;,

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 &lt;= 3; tentativa++ {

// Simulando falha

if tentativa &lt; 3 {

continue

}

}

return &amp;ErroConexao{

Host: host,

Porta: porta,

Tentativas: 3,

UltimaConexao: time.Now(),

MensagemOriginal: fmt.Errorf(&quot;connection timeout&quot;),

}

}

func main() {

err := conectarComRetentativa(&quot;database.example.com&quot;, 5432)

// Extração de informações usando type assertion

if erroConn, ok := err.(*ErroConexao); ok {

fmt.Println(&quot;Erro de conexão detectado&quot;)

fmt.Println(&quot;Host:&quot;, erroConn.Host)

fmt.Println(&quot;É recuperável?&quot;, erroConn.Recuperavel())

fmt.Println(&quot;Mensagem:&quot;, err)

}

}</code></pre>

<h3>Validação e Erros Customizados</h3>

<p>Um caso muito comum é validação de dados. Você pode criar tipos de erro que armazenam múltiplos erros de validação:</p>

<pre><code class="language-go">package main

import (

&quot;fmt&quot;

&quot;strings&quot;

)

type ErroValidacao struct {

Campo string

Motivo string

}

func (e ErroValidacao) Error() string {

return fmt.Sprintf(&quot;Campo &#039;%s&#039; inválido: %s&quot;, e.Campo, e.Motivo)

}

// Agregar múltiplos erros

type ErroValidacaoMultipla struct {

Erros []ErroValidacao

}

func (e ErroValidacaoMultipla) Error() string {

if len(e.Erros) == 0 {

return &quot;Nenhum erro de validação&quot;

}

mensagens := make([]string, len(e.Erros))

for i, err := range e.Erros {

mensagens[i] = err.Error()

}

return strings.Join(mensagens, &quot;; &quot;)

}

type Usuario struct {

Nome string

Email string

Idade int

}

func validarUsuario(u Usuario) error {

var erros []ErroValidacao

if u.Nome == &quot;&quot; {

erros = append(erros, ErroValidacao{&quot;Nome&quot;, &quot;não pode estar vazio&quot;})

}

if !strings.Contains(u.Email, &quot;@&quot;) {

erros = append(erros, ErroValidacao{&quot;Email&quot;, &quot;formato inválido&quot;})

}

if u.Idade &lt; 18 {

erros = append(erros, ErroValidacao{&quot;Idade&quot;, &quot;deve ser maior que 18&quot;})

}

if len(erros) &gt; 0 {

return ErroValidacaoMultipla{Erros: erros}

}

return nil

}

func main() {

user := Usuario{

Nome: &quot;&quot;,

Email: &quot;email-invalido&quot;,

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(&quot;Quantidade de erros:&quot;, len(multi.Erros))

for _, e := range multi.Erros {

fmt.Printf(&quot;- %s\n&quot;, e)

}

}

}</code></pre>

<h2>Práticas Recomendadas e Padrões Avançados</h2>

<h3>Wrapping de Erros com Context</h3>

<p>A partir do Go 1.13, você pode usar <code>fmt.Errorf</code> com o verbo <code>%w</code> para envolver erros mantendo a cadeia de erro. Isso permite que ferramentas como <code>errors.Is()</code> e <code>errors.As()</code> funcionem corretamente:</p>

<pre><code class="language-go">package main

import (

&quot;errors&quot;

&quot;fmt&quot;

)

var ErrBancoDados = errors.New(&quot;erro no banco de dados&quot;)

func buscarUsuario(id int) (string, error) {

// Simulando erro no banco de dados

if id &lt; 0 {

// Envolvendo o erro com contexto adicional

return &quot;&quot;, fmt.Errorf(&quot;falha ao buscar usuário com ID %d: %w&quot;, id, ErrBancoDados)

}

return &quot;João Silva&quot;, nil

}

func processarPedido(userID int) error {

usuario, err := buscarUsuario(userID)

if err != nil {

// Adicionando mais camadas de contexto

return fmt.Errorf(&quot;não foi possível processar pedido: %w&quot;, err)

}

fmt.Printf(&quot;Processando pedido para: %s\n&quot;, usuario)

return nil

}

func main() {

err := processarPedido(-1)

// Verificar se o erro original está na cadeia

if errors.Is(err, ErrBancoDados) {

fmt.Println(&quot;Problema no banco de dados detectado na cadeia de erro&quot;)

}

// Extrair o erro específico

var erroDB error

if errors.As(err, &amp;erroDB) {

fmt.Printf(&quot;Erro extraído: %v\n&quot;, erroDB)

}

fmt.Println(&quot;Erro completo:&quot;, err)

}</code></pre>

<h3>Padrão de Erro com Tipo Específico</h3>

<p>Para casos onde você quer permitir que o chamador tome ações específicas, você pode criar tipos de erro que implementam interfaces adicionais:</p>

<pre><code class="language-go">package main

import (

&quot;fmt&quot;

&quot;time&quot;

)

// 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(&quot;%s. Tente novamente após %s&quot;, 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: &quot;Taxa de requisições excedida&quot;,

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 &amp;&amp; temp.Temporario() {

fmt.Printf(&quot;Erro temporário. Aguarde até %s antes de tentar novamente\n&quot;, temp.Apos())

} else {

fmt.Printf(&quot;Erro permanente: %v\n&quot;, err)

}

}

}

func main() {

executarComRetentativa(fazerRequisicao)

}</code></pre>

<h2>Conclusão</h2>

<p>Dominar o sistema de erros em Go significa entender três conceitos distintos e quando usar cada um. <strong>Primeiro, a interface <code>error</code></strong> é a base — qualquer tipo que implemente <code>Error() string</code> é um erro válido, oferecendo flexibilidade máxima. <strong>Segundo, sentinel errors</strong> são a escolha para situações bem definidas quando você só precisa sinalizar &quot;qual tipo de erro&quot;, sem contexto adicional — use <code>==</code> para compará-los com eficiência. <strong>Terceiro, erros customizados</strong> são quando você precisa carregar contexto e informações dinâmicas — crie structs que implementem <code>error</code> e potencialmente outras interfaces para fornecer comportamentos específicos.</p>

<p>A prática de usar <code>fmt.Errorf</code> com <code>%w</code> para envolver erros (Go 1.13+) é fundamental para código moderno, permitindo que bibliotecas como <code>errors.Is()</code> funcionem corretamente e facilitando debugging em camadas profundas da aplicação.</p>

<h2>Referências</h2>

<ul>

<li><a href="https://golang.org/doc/effective_go#errors" target="_blank" rel="noopener noreferrer">Effective Go - Error handling</a></li>

<li><a href="https://go.dev/blog/error-handling-and-go" target="_blank" rel="noopener noreferrer">Go Blog - Error Handling and Go</a></li>

<li><a href="https://go.dev/blog/go1.13-errors" target="_blank" rel="noopener noreferrer">Working with Errors in Go 1.13</a></li>

<li><a href="https://gobyexample.com/errors" target="_blank" rel="noopener noreferrer">Go by Example - Errors</a></li>

<li><a href="https://pkg.go.dev/errors" target="_blank" rel="noopener noreferrer">Standard Library - errors Package</a></li>

</ul>

<p>&lt;!-- FIM --&gt;</p>

Comentários

Mais em Go

O que Todo Dev Deve Saber sobre Embedding em Go: Composição de Structs e Interfaces
O que Todo Dev Deve Saber sobre Embedding em Go: Composição de Structs e Interfaces

Embedding em Go: O que é e Por Que Importa Embedding é um mecanismo poderoso...

Como Usar Benchmarks e Profiling em Go: testing.B e pprof na Prática em Produção
Como Usar Benchmarks e Profiling em Go: testing.B e pprof na Prática em Produção

Introdução aos Benchmarks em Go Quando desenvolvemos software em Go, é comum...

Pacote io em Go: Readers, Writers e a Filosofia de Streams na Prática
Pacote io em Go: Readers, Writers e a Filosofia de Streams na Prática

A Filosofia de Streams em Go A programação tradicional frequentemente trabalh...