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

  1. Sempre verifique erros — nunca ignore com _
  2. Adicione contexto com fmt.Errorf("...: %w", err) em cada camada
  3. Use sentinelas para condições esperadas e documentadas
  4. Use tipos customizados quando precisa de dados estruturados no erro
  5. Não faça log E retorne — escolha um dos dois para evitar logs duplicados
  6. 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.