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çaEsse 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).
- Ouça o áudio com a tela desligada
- Ganhe Certificado após a conclusão
- + de 5000 cursos para você explorar!
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
%wapenas para embrulhar umerror. - Em uma chamada de
fmt.Errorf, apenas um argumento pode ser embrulhado com%w. - Se você usar
%vem vez de%w, você perde a capacidade de inspeção viaerrors.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/Aspara lógica. - Use aspas em valores textuais:
%qajuda a visualizar espaços e caracteres especiais. - Evite dados sensíveis: não inclua senhas, tokens, segredos.
Exemplos: ruim vs melhor
| Ruim | Melhor |
|---|---|
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
ValidationErrorpara 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
%we 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ãoExercí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
*FieldErrornos casos inválidos. - Mostre como o chamador usa
errors.Aspara extrair o campo e a mensagem.
// Assinaturas sugeridas:
// type FieldError struct { Field, Msg string }
// func (e *FieldError) Error() string
// func ValidatePassword(pw string) errorExercí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:
DecodeInputretorna um erro base (pode sererrors.Newou um erro de conversão).ValidateInputchamaDecodeInpute faz wrapping comfmt.Errorf("validate input: %w", err).HandleRequestchamaValidateInpute faz wrapping comfmt.Errorf("handle request: %w", err).- No final, demonstre que
errors.Isouerrors.Asainda funciona para detectar a causa raiz.
// Dica: crie uma sentinela ErrBadFormat e use errors.Is no final.
// var ErrBadFormat = errors.New("bad format")