Erros em Go: padrão error, wrapping, sentinelas e mensagens úteis

Capítulo 10

Tempo estimado de leitura: 8 minutos

+ Exercício

O fluxo idiomático de erros em Go

Em Go, erros são valores retornados (geralmente como o último retorno de uma função) e devem ser tratados explicitamente. O padrão é: a função retorna um error; quem chama verifica imediatamente; se não souber resolver, propaga o erro adicionando contexto. Isso evita “exceções” implícitas e torna o fluxo de falhas previsível.

Padrão básico: retornar error e checar imediatamente

O formato mais comum é (T, error) ou apenas error. Ao chamar, verifique o erro logo após a chamada, antes de usar o valor retornado.

func ParsePort(s string) (int, error) { /* ... */ return 0, nil }

port, err := ParsePort(input)
if err != nil {
	return err // ou trate aqui
}
// use port com segurança

Esse padrão reduz bugs como “usar um valor inválido quando houve falha” e deixa claro onde o erro foi tratado.

Criando erros: errors.New e fmt.Errorf

errors.New: erro simples e direto

Use quando você precisa apenas de uma mensagem fixa.

import "errors"

var ErrEmptyName = errors.New("name cannot be empty")

fmt.Errorf: mensagem formatada e (opcionalmente) wrapping

Use quando precisa incluir valores na mensagem. Se você estiver propagando um erro existente, prefira wrapping com %w (ver seção seguinte).

Continue em nosso aplicativo e ...
  • Ouça o áudio com a tela desligada
  • Ganhe Certificado após a conclusão
  • + de 5000 cursos para você explorar!
ou continue lendo abaixo...
Download App

Baixar o aplicativo

import "fmt"

func ValidateAge(age int) error {
	if age < 0 {
		return fmt.Errorf("age must be >= 0, got %d", age)
	}
	return nil
}

Wrapping: preservando a causa do erro com %w

Wrapping é a técnica de “embrulhar” um erro com contexto adicional sem perder o erro original. Em Go, isso é feito com fmt.Errorf e o verbo %w. Assim, quem chama consegue inspecionar a causa raiz com errors.Is e errors.As.

Quando fazer wrapping

  • Quando você está propagando um erro que veio de outra função/camada.
  • Quando quer acrescentar contexto útil (qual operação falhou, qual parâmetro, qual recurso).
  • Quando faz sentido que camadas acima consigam detectar o erro original.

Exemplo prático: propagando com contexto

import (
	"errors"
	"fmt"
	"os"
)

func ReadConfig(path string) ([]byte, error) {
	b, err := os.ReadFile(path)
	if err != nil {
		return nil, fmt.Errorf("read config %q: %w", path, err)
	}
	return b, nil
}

func LoadApp() error {
	_, err := ReadConfig("./config.json")
	if err != nil {
		return fmt.Errorf("load app: %w", err)
	}
	return nil
}

func ExampleInspect() {
	err := LoadApp()
	if err == nil {
		return
	}
	// Ainda é possível detectar a causa original:
	if errors.Is(err, os.ErrNotExist) {
		// tratar arquivo ausente
	}
}

Note como a mensagem final fica mais informativa (com “load app” e “read config”), mas a cadeia de erros mantém a causa original.

Regras importantes do %w

  • Use %w apenas para embrulhar um error.
  • Em uma chamada de fmt.Errorf, apenas um argumento pode ser embrulhado com %w.
  • Se você usar %v em vez de %w, você perde a capacidade de inspeção via errors.Is/As.

Inspeção de erros: errors.Is e errors.As

Ao receber um erro, evite comparar mensagens. Mensagens são para humanos; detecção programática deve usar sentinelas, tipos ou interfaces.

errors.Is: checar se um erro é (ou contém) outro

Use para sentinelas (variáveis de erro) e para erros padrão do Go (por exemplo, os.ErrNotExist), mesmo quando o erro foi embrulhado.

import (
	"errors"
	"os"
)

if errors.Is(err, os.ErrNotExist) {
	// arquivo não existe
}

errors.As: extrair um tipo específico de erro

Use quando você precisa acessar campos/métodos de um tipo de erro concreto (por exemplo, obter o caminho, operação, etc.).

import (
	"errors"
	"fmt"
	"os"
)

var pe *os.PathError
if errors.As(err, &pe) {
	fmt.Printf("op=%s path=%s\n", pe.Op, pe.Path)
}

errors.As percorre a cadeia de wrapping até encontrar um erro que seja atribuível ao tipo alvo.

Sentinelas vs tipos de erro: quando usar cada um

Erros sentinela

Um erro sentinela é uma variável (geralmente exportada) que representa uma condição específica. É útil quando:

  • Existem poucas condições bem definidas.
  • Você quer que o chamador faça errors.Is(err, ErrX).
  • Não há necessidade de carregar detalhes adicionais além da condição.
import "errors"

var ErrInvalidEmail = errors.New("invalid email")

Uso:

if errors.Is(err, ErrInvalidEmail) {
	// pedir para o usuário corrigir
}

Tipos de erro (com dados)

Use um tipo de erro quando você precisa transportar contexto estruturado (campo, valor, regra violada) e quer que o chamador possa extrair esses dados com errors.As.

type ValidationError struct {
	Field string
	Rule  string
	Value any
}

func (e *ValidationError) Error() string {
	return fmt.Sprintf("%s: %s (value=%v)", e.Field, e.Rule, e.Value)
}

Uso:

var ve *ValidationError
if errors.As(err, &ve) {
	// ve.Field, ve.Rule, ve.Value
}

Combinação comum: sentinela + wrapping

Você pode ter uma sentinela para a condição e ainda assim adicionar contexto com wrapping.

var ErrUnauthorized = errors.New("unauthorized")

func Authenticate(token string) error {
	if token == "" {
		return fmt.Errorf("missing token: %w", ErrUnauthorized)
	}
	return nil
}

Mensagens de erro úteis (acionáveis)

Uma mensagem de erro útil ajuda quem está depurando (ou o usuário) a agir. Em geral, inclua: o que você tentou fazer, com qual entrada/recurso, e por que falhou (quando apropriado). Ao mesmo tempo, evite vazar dados sensíveis.

Boas práticas

  • Seja específico: “parse port” é melhor que “invalid input”.
  • Inclua contexto: nome do campo, identificador, caminho, operação.
  • Evite duplicar contexto: cada camada adiciona apenas o que ela sabe.
  • Não compare strings: use errors.Is/As para lógica.
  • Use aspas em valores textuais: %q ajuda a visualizar espaços e caracteres especiais.
  • Evite dados sensíveis: não inclua senhas, tokens, segredos.

Exemplos: ruim vs melhor

RuimMelhor
return errors.New("error")return fmt.Errorf("parse port %q: %w", s, ErrInvalidPort)
return fmt.Errorf("failed")return fmt.Errorf("open report %q: %w", path, err)
return fmt.Errorf("invalid")return &ValidationError{Field:"email", Rule:"must contain @", Value: email}

Passo a passo: função de validação que preserva contexto ao propagar erros

Objetivo: validar entradas de um “cadastro” e retornar erros que sejam fáceis de tratar e diagnosticar. Vamos criar:

  • Uma sentinela para “entrada inválida” em geral.
  • Um tipo ValidationError para detalhes por campo.
  • Uma função que valida e, ao chamar subfunções, faz wrapping com contexto.

1) Defina sentinelas e tipo de erro

import (
	"errors"
	"fmt"
	"strings"
)

var ErrInvalidInput = errors.New("invalid input")

type ValidationError struct {
	Field string
	Rule  string
	Value any
}

func (e *ValidationError) Error() string {
	return fmt.Sprintf("%s: %s (value=%v)", e.Field, e.Rule, e.Value)
}

2) Crie validadores pequenos por campo

func validateName(name string) error {
	name = strings.TrimSpace(name)
	if name == "" {
		return &ValidationError{Field: "name", Rule: "cannot be empty", Value: name}
	}
	if len(name) < 3 {
		return &ValidationError{Field: "name", Rule: "min length is 3", Value: name}
	}
	return nil
}

func validateEmail(email string) error {
	email = strings.TrimSpace(email)
	if email == "" {
		return &ValidationError{Field: "email", Rule: "cannot be empty", Value: email}
	}
	if !strings.Contains(email, "@") {
		return &ValidationError{Field: "email", Rule: "must contain @", Value: email}
	}
	return nil
}

3) Função principal: agrega contexto e permite inspeção

type SignupInput struct {
	Name  string
	Email string
}

func ValidateSignup(in SignupInput) error {
	if err := validateName(in.Name); err != nil {
		// Wrapping adiciona contexto de operação e preserva o tipo do erro.
		return fmt.Errorf("validate signup: %w", err)
	}
	if err := validateEmail(in.Email); err != nil {
		return fmt.Errorf("validate signup: %w", err)
	}
	return nil
}

4) Chamador: decide o que fazer com errors.As e errors.Is

import "errors"

err := ValidateSignup(SignupInput{Name: "Al", Email: "alice.example.com"})
if err != nil {
	var ve *ValidationError
	if errors.As(err, &ve) {
		// resposta acionável: destacar campo e regra
		// ve.Field, ve.Rule
	}
	// fallback: logar/propagar
}

Repare que o chamador não precisa “parsear” a mensagem para saber qual campo falhou.

Exercícios (prática guiada)

Exercício 1: validar porta e propagar erro com contexto

Implemente ParseAndValidatePort que recebe uma string, converte para inteiro e valida o intervalo 1..65535. Requisitos:

  • Crie uma sentinela ErrInvalidPort.
  • Se a conversão falhar, faça wrapping do erro original com %w e inclua o valor recebido com %q.
  • Se estiver fora do intervalo, retorne um erro que permita detecção com errors.Is(err, ErrInvalidPort).
// Assinatura sugerida:
// func ParseAndValidatePort(s string) (int, error)

// Casos para testar mentalmente:
// "8080" -> ok
// "0" -> ErrInvalidPort
// "70000" -> ErrInvalidPort
// "abc" -> erro com wrapping da conversão

Exercício 2: tipo de erro para validação de campo

Crie um tipo FieldError com campos Field e Msg. Depois implemente ValidatePassword:

  • Senha não pode ser vazia.
  • Senha deve ter pelo menos 8 caracteres.
  • Retorne *FieldError nos casos inválidos.
  • Mostre como o chamador usa errors.As para extrair o campo e a mensagem.
// Assinaturas sugeridas:
// type FieldError struct { Field, Msg string }
// func (e *FieldError) Error() string
// func ValidatePassword(pw string) error

Exercício 3: encadeamento de contexto (wrapping em múltiplas camadas)

Implemente três funções: DecodeInput, ValidateInput, HandleRequest. A ideia é que cada uma adicione contexto e propague:

  • DecodeInput retorna um erro base (pode ser errors.New ou um erro de conversão).
  • ValidateInput chama DecodeInput e faz wrapping com fmt.Errorf("validate input: %w", err).
  • HandleRequest chama ValidateInput e faz wrapping com fmt.Errorf("handle request: %w", err).
  • No final, demonstre que errors.Is ou errors.As ainda funciona para detectar a causa raiz.
// Dica: crie uma sentinela ErrBadFormat e use errors.Is no final.
// var ErrBadFormat = errors.New("bad format")

Agora responda o exercício sobre o conteúdo:

Ao propagar um erro entre camadas em Go, qual abordagem mantém a causa original inspecionável e ainda adiciona contexto útil para diagnóstico?

Você acertou! Parabéns, agora siga para a próxima página

Você errou! Tente novamente.

O wrapping com %w adiciona contexto sem perder o erro original. Assim, errors.Is e errors.As conseguem percorrer a cadeia e identificar a causa raiz, ao contrário de comparar/reescrever mensagens.

Próximo capitúlo

Testes básicos em Go: pacote testing, table-driven tests e cobertura

Arrow Right Icon
Capa do Ebook gratuito Go (Golang) para Iniciantes: Fundamentos, Concorrência e Estrutura de Projetos
56%

Go (Golang) para Iniciantes: Fundamentos, Concorrência e Estrutura de Projetos

Novo curso

18 páginas

Baixe o app para ganhar Certificação grátis e ouvir os cursos em background, mesmo com a tela desligada.