O que é Error em Go?
Em Go, error é uma interface nativa da linguagem que representa uma condição de falha. Diferente de linguagens que usam exceções (try/catch), Go trata erros como valores retornados explicitamente pelas funções. Esse design torna o fluxo de tratamento de erros visível, previsível e impossível de ignorar acidentalmente.
A interface error é surpreendentemente simples:
type error interface {
Error() string
}
Qualquer tipo que implemente o método Error() string satisfaz a interface error. Isso permite criar tipos de erro customizados com informações estruturadas — códigos HTTP, campos de contexto, erros encadeados — mantendo compatibilidade com toda a ecosystem Go.
O padrão idiomático em Go é retornar o erro como último valor de retorno e verificar imediatamente:
resultado, err := algumaOperacao()
if err != nil {
return fmt.Errorf("falha na operação: %w", err)
}
// usar resultado com segurança
Esse padrão repetitivo é intencional — torna explícito onde erros podem ocorrer e força o desenvolvedor a decidir conscientemente como tratar cada um.
Criando erros
errors.New — Erros simples
import "errors"
var ErrUsuarioNaoEncontrado = errors.New("usuário não encontrado")
var ErrSenhaInvalida = errors.New("senha inválida")
func buscarUsuario(id int) (*Usuario, error) {
usuario := db.Find(id)
if usuario == nil {
return nil, ErrUsuarioNaoEncontrado
}
return usuario, nil
}
fmt.Errorf — Erros com contexto formatado
import "fmt"
func conectarBanco(host string, porta int) error {
conn, err := dial(host, porta)
if err != nil {
return fmt.Errorf("falha ao conectar em %s:%d: %w", host, porta, err)
}
return nil
}
O verbo %w (wrap) é crucial — ele encadeia o erro original, permitindo que chamadores usem errors.Is() e errors.As() para inspecionar a causa raiz.
Diferença entre %v e %w
// %v — formata o erro como string (perde a referência ao original)
err1 := fmt.Errorf("contexto: %v", errOriginal)
errors.Is(err1, errOriginal) // false!
// %w — wraps mantendo referência ao original
err2 := fmt.Errorf("contexto: %w", errOriginal)
errors.Is(err2, errOriginal) // true!
Sempre use %w quando quiser preservar a cadeia de erros para inspeção.
Sentinel errors (erros sentinela)
Sentinel errors são variáveis de erro predefinidas que representam condições específicas e conhecidas:
package repository
import "errors"
// Sentinelas — convenção: prefixo Err
var (
ErrNaoEncontrado = errors.New("registro não encontrado")
ErrDuplicado = errors.New("registro já existe")
ErrSemPermissao = errors.New("sem permissão para esta operação")
)
A standard library usa sentinelas extensivamente:
import "io"
// io.EOF sinaliza fim de stream
for {
_, err := reader.Read(buf)
if errors.Is(err, io.EOF) {
break // fim normal da leitura
}
if err != nil {
return err // erro real
}
}
Outros sentinelas comuns: sql.ErrNoRows, context.Canceled, context.DeadlineExceeded, os.ErrNotExist.
Error wrapping com %w
O wrapping de erros cria uma cadeia que preserva contexto em cada camada:
// Camada de banco de dados
func (r *UserRepo) FindByEmail(email string) (*User, error) {
row := r.db.QueryRow("SELECT ...", email)
if err := row.Scan(&user); err != nil {
return nil, fmt.Errorf("UserRepo.FindByEmail(%s): %w", email, err)
}
return &user, nil
}
// Camada de serviço
func (s *AuthService) Login(email, senha string) (*Token, error) {
user, err := s.repo.FindByEmail(email)
if err != nil {
return nil, fmt.Errorf("AuthService.Login: %w", err)
}
// ...
}
// Resultado:
// "AuthService.Login: UserRepo.FindByEmail(ana@ex.com): sql: no rows in result set"
Cada camada adiciona contexto sem perder a causa original. Para projetos com Clean Architecture, esse padrão é essencial.
errors.Is e errors.As
errors.Is — Comparando erros na cadeia
err := buscarUsuario(42) // retorna wrapped error
// Verifica se o erro (ou qualquer erro na cadeia) é o sentinela
if errors.Is(err, ErrUsuarioNaoEncontrado) {
// tratar caso específico
http.Error(w, "Usuário não encontrado", 404)
return
}
errors.Is percorre toda a cadeia de wrapping procurando o erro alvo. Nunca compare erros com == diretamente — use sempre errors.Is.
errors.As — Extraindo tipo específico
type ValidationError struct {
Campo string
Mensagem string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("validação falhou no campo %s: %s", e.Campo, e.Mensagem)
}
// Uso:
var valErr *ValidationError
if errors.As(err, &valErr) {
// Acesso aos campos estruturados
fmt.Printf("Campo inválido: %s\n", valErr.Campo)
fmt.Printf("Motivo: %s\n", valErr.Mensagem)
}
errors.As é o type assertion para erros encadeados — percorre a cadeia e extrai o primeiro erro que corresponde ao tipo alvo.
Custom error types
Para cenários que precisam de mais informação estruturada:
type AppError struct {
Code int // código HTTP ou interno
Message string // mensagem para o usuário
Err error // erro original (causa)
}
func (e *AppError) Error() string {
if e.Err != nil {
return fmt.Sprintf("[%d] %s: %v", e.Code, e.Message, e.Err)
}
return fmt.Sprintf("[%d] %s", e.Code, e.Message)
}
func (e *AppError) Unwrap() error {
return e.Err
}
// Construtor
func NewAppError(code int, msg string, cause error) *AppError {
return &AppError{Code: code, Message: msg, Err: cause}
}
O método Unwrap() permite que errors.Is e errors.As percorram a cadeia através do seu tipo customizado.
Erros com múltiplas causas (Go 1.20+)
type MultiError struct {
Erros []error
}
func (e *MultiError) Error() string {
msgs := make([]string, len(e.Erros))
for i, err := range e.Erros {
msgs[i] = err.Error()
}
return strings.Join(msgs, "; ")
}
// Go 1.20+: Unwrap retornando slice
func (e *MultiError) Unwrap() []error {
return e.Erros
}
Com errors.Join (Go 1.20+):
err := errors.Join(err1, err2, err3)
// errors.Is(err, err1) → true
// errors.Is(err, err2) → true
Padrões de tratamento de erros
Early return
func processarPedido(p *Pedido) error {
if p == nil {
return errors.New("pedido não pode ser nil")
}
if p.Total <= 0 {
return fmt.Errorf("total inválido: %f", p.Total)
}
if err := validarEstoque(p); err != nil {
return fmt.Errorf("validação de estoque: %w", err)
}
if err := cobrar(p); err != nil {
return fmt.Errorf("cobrança: %w", err)
}
return nil
}
Defer para cleanup
Combinando com defer para garantir limpeza:
func copiarArquivo(origem, destino string) error {
src, err := os.Open(origem)
if err != nil {
return fmt.Errorf("abrir origem: %w", err)
}
defer src.Close()
dst, err := os.Create(destino)
if err != nil {
return fmt.Errorf("criar destino: %w", err)
}
defer dst.Close()
if _, err := io.Copy(dst, src); err != nil {
return fmt.Errorf("copiar dados: %w", err)
}
return dst.Sync()
}
Error handling em HTTP handlers
func (h *Handler) CriarUsuario(w http.ResponseWriter, r *http.Request) {
var req CriarUsuarioRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "JSON inválido", http.StatusBadRequest)
return
}
usuario, err := h.service.Criar(r.Context(), req)
if err != nil {
var valErr *ValidationError
if errors.As(err, &valErr) {
http.Error(w, valErr.Error(), http.StatusUnprocessableEntity)
return
}
if errors.Is(err, ErrDuplicado) {
http.Error(w, "Usuário já existe", http.StatusConflict)
return
}
// Erro inesperado — log + resposta genérica
slog.Error("criar usuário", "error", err)
http.Error(w, "Erro interno", http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(usuario)
}
Para logging estruturado, veja o guia de slog.
Boas práticas
- Sempre verifique erros — nunca ignore com
_ - Adicione contexto com
fmt.Errorf("...: %w", err)em cada camada - Use sentinelas para condições esperadas e documentadas
- Use tipos customizados quando precisa de dados estruturados no erro
- Não faça log E retorne — escolha um dos dois para evitar logs duplicados
- Prefira errors.Is/As a comparações diretas com
==ou type assertions
Para mais sobre tratamento de erros, explore o guia completo de erros em Go e os padrões de concorrência onde error handling com goroutines e channels exige atenção especial.
Perguntas frequentes (FAQ)
Por que Go não usa exceções como Java ou Python?
Go usa erros como valores retornados para tornar o fluxo de erro explícito e previsível. Exceções criam fluxo de controle invisível — o desenvolvedor não sabe quais funções podem lançar exceções sem ler documentação. Em Go, a assinatura da função diz tudo: se retorna error, pode falhar. Isso resulta em código mais robusto e fácil de manter.
Quando usar panic vs retornar error?
Use panic apenas para bugs — situações que nunca deveriam acontecer em código correto (índice fora do range, nil pointer em local impossível). Para todas as condições de erro recuperáveis — arquivo não encontrado, rede indisponível, input inválido — retorne error. Em APIs REST e microsserviços, praticamente nunca use panic.
Como testar tratamento de erros?
Crie sentinelas ou tipos customizados e verifique com errors.Is/errors.As nos testes. Injete dependências que retornam erros controlados. Para testes de tabela, inclua cenários de erro com o erro esperado como campo da struct de teste. O fuzzing nativo do Go também pode encontrar edge cases de erro.
Qual a diferença entre errors.Is e errors.As?
errors.Is verifica se um erro na cadeia é igual a um valor específico (sentinela) — responde “esse erro É aquele?”. errors.As extrai um erro de tipo específico da cadeia — responde “esse erro CONTÉM um ValidationError?”. Use Is para sentinelas (io.EOF, sql.ErrNoRows) e As para tipos customizados com campos adicionais.