Especificação de Tratamento de Erros em Golang

Tipos de Erros

Há poucas opções para declarar erros. Antes de escolher a opção que melhor se adequa ao seu caso de uso, considere o seguinte:

  • O chamador precisa combinar o erro para lidar com ele? Se sim, devemos dar suporte às funções errors.Is ou errors.As declarando variáveis de erro de nível superior ou tipos personalizados.
  • A mensagem de erro é uma string estática ou uma string dinâmica que requer informações contextuais? Para strings estáticas, podemos usar errors.New, mas para a última, devemos usar fmt.Errorf ou um tipo de erro personalizado.
  • Estamos passando novos erros retornados por funções secundárias? Se sim, consulte a seção de encapsulamento de erros.
Combinação de Erros? Mensagem de Erro Orientação
Não estática errors.New
Não dinâmica fmt.Errorf
Sim estática variável de nível superior com errors.New
Sim dinâmica tipo de error personalizado

Por exemplo, use errors.New para representar erros com strings estáticas. Se o chamador precisa combinar e lidar com este erro, exporte-o como uma variável para suportar a combinação com errors.Is.

Sem Combinação de Erros

// pacote foo

func Abrir() error {
  return errors.New("não foi possível abrir")
}

// pacote bar

if err := foo.Abrir(); err != nil {
  // Não é possível lidar com o erro.
  panic("erro desconhecido")
}

Combinação de Erros

// pacote foo

var ErrNaoFoiPossivelAbrir = errors.New("não foi possível abrir")

func Abrir() error {
  return ErrNaoFoiPossivelAbrir
}

// pacote bar

if err := foo.Abrir(); err != nil {
  if errors.Is(err, foo.ErrNaoFoiPossivelAbrir) {
    // lidar com o erro
  } else {
    panic("erro desconhecido")
  }
}

Para erros com strings dinâmicas, use fmt.Errorf se o chamador não precisa combiná-lo. Se o chamador realmente precisa combiná-lo, então use um error personalizado.

Sem Combinação de Erros

// pacote foo

func Abrir(arquivo string) error {
  return fmt.Errorf("arquivo %q não encontrado", arquivo)
}

// pacote bar

if err := foo.Abrir("arquivo_teste.txt"); err != nil {
  // Não é possível lidar com o erro.
  panic("erro desconhecido")
}

Combinação de Erros

// pacote foo

type ErroNaoEncontrado struct {
  Arquivo string
}

func (e *ErroNaoEncontrado) Error() string {
  return fmt.Sprintf("arquivo %q não encontrado", e.Arquivo)
}

func Abrir(arquivo string) error {
  return &ErroNaoEncontrado{Arquivo: arquivo}
}

// pacote bar

if err := foo.Abrir("arquivo_teste.txt"); err != nil {
  var naoEncontrado *ErroNaoEncontrado
  if errors.As(err, &naoEncontrado) {
    // lidar com o erro
  } else {
    panic("erro desconhecido")
  }
}

Observe que se você exportar variáveis de erro ou tipos de um pacote, eles se tornarão parte da API pública do pacote.

Envoltório de Erro

Quando um erro ocorre ao chamar outro método, geralmente existem três formas de lidar com isso:

  • Retornar o erro original como está.
  • Usar fmt.Errorf com %w para adicionar contexto ao erro e então retorná-lo.
  • Usar fmt.Errorf com %v para adicionar contexto ao erro e então retorná-lo.

Se não houver contexto adicional para adicionar, retorne o erro original como está. Isso preservará o tipo e a mensagem de erro original. Isso é particularmente adequado quando a mensagem de erro subjacente contém informações suficientes para rastrear de onde o erro se originou.

Caso contrário, adicione contexto à mensagem de erro o máximo possível para que erros ambíguos, como "conexão recusada", não ocorram. Em vez disso, você receberá erros mais úteis, como "chamando serviço foo: conexão recusada".

Use fmt.Errorf para adicionar contexto aos seus erros e escolha entre os verbos %w ou %v com base em se o chamador deve ser capaz de corresponder e extrair a causa raiz.

  • Use %w se o chamador deve ter acesso ao erro subjacente. Este é um bom padrão para a maioria dos erros de envoltório, mas esteja ciente de que o chamador pode começar a depender desse comportamento. Portanto, para erros de envoltório que são variáveis ou tipos conhecidos, registre-os e teste-os como parte do contrato da função.
  • Use %v para obscurecer o erro subjacente. O chamador não poderá correspondê-lo, mas você pode mudar para %w no futuro, se necessário.

Ao adicionar contexto ao erro retornado, evite usar frases como "falha ao" para manter o contexto conciso. Quando o erro se propaga pela pilha, ele será empilhado camada por camada:

Não recomendado:

s, err := store.New()
if err != nil {
    return fmt.Errorf(
        "falha ao criar nova loja: %w", err)
}

// falha ao x: falha ao y: falha ao criar nova loja: o erro

Recomendado:

s, err := store.New()
if err != nil {
    return fmt.Errorf(
        "nova loja: %w", err)
}
// x: y: nova loja: o erro

No entanto, uma vez que o erro é enviado para outro sistema, deve ficar claro que a mensagem é um erro (por exemplo, uma marca "err" ou um prefixo "Falhou" nos logs).

Nomenclatura Incorreta

Para valores de erro armazenados como variáveis globais, use o prefixo Err ou err com base em se eles são exportados. Consulte as diretrizes. Para constantes e variáveis de nível superior não exportadas, use um sublinhado (_) como prefixo.

var (
  // Exporte os dois seguintes erros para que os usuários deste pacote possam correspondê-los com errors.Is.
  ErrLinkQuebrado = errors.New("link está quebrado")
  ErrNaoFoiPossivelAbrir = errors.New("não foi possível abrir")

  // Este erro não é exportado porque não queremos que faça parte de nossa API pública. Ainda podemos usá-lo dentro do pacote com errors.
  errNaoEncontrado = errors.New("não encontrado")
)

Para tipos de erro personalizados, use o sufixo Error.

// Da mesma forma, este erro é exportado para que os usuários deste pacote possam correspondê-lo com errors.As.
type ErroNaoEncontrado struct {
  Arquivo string
}

func (e *ErroNaoEncontrado) Error() string {
  return fmt.Sprintf("arquivo %q não encontrado", e.Arquivo)
}

// Este erro não é exportado porque não queremos que faça parte da API pública. Ainda podemos usá-lo dentro de um pacote com errors.As.
type erroResolvido struct {
  Caminho string
}

func (e *erroResolvido) Error() string {
  return fmt.Sprintf("resolver %q", e.Caminho)
}

Tratamento de Erros

Quando o chamador recebe um erro do chamado, ele pode lidar com o erro de várias maneiras com base no entendimento do erro.

Isso inclui, mas não se limita a:

  • Corresponder o erro com errors.Is ou errors.As se o chamado concordou com uma definição de erro específica e lidar com o ramificação de maneiras diferentes
  • Registrar o erro e degradar graciosamente se o erro for recuperável
  • Retornar um erro bem definido se ele representar uma condição de falha específica do domínio
  • Retornar o erro, seja ele envelopado ou sem modificações

Independentemente de como o chamador lida com o erro, normalmente deve lidar com cada erro apenas uma vez. Por exemplo, o chamador não deve registrar o erro e depois retorná-lo, pois seu chamador também pode lidar com o erro.

Por exemplo, considere os seguintes cenários:

Mal: Registrar o erro e retorná-lo

Outros chamadores mais acima na pilha podem tomar ações semelhantes a este erro. Isso criaria muito ruído nos logs do aplicativo com pouco benefício.

u, err := getUser(id)
if err != nil {
  // RUIM: Ver descrição
  log.Printf("Não foi possível obter o usuário %q: %v", id, err)
  return err
}

Bom: Envolver o erro e retorná-lo

Os erros mais acima na pilha lidarão com este erro. Usar %w garante que eles possam corresponder o erro com errors.Is ou errors.As se for relevante.

u, err := getUser(id)
if err != nil {
  return fmt.Errorf("obter usuário %q: %w", id, err)
}

Bom: Registrar o erro e degradar graciosamente

Se a operação não for absolutamente necessária, podemos fornecer degradação graciosa recuperando sem interromper a experiência.

if err := emitMetrics(); err != nil {
  // Falha ao gravar as métricas não deve
  // quebrar o aplicativo.
  log.Printf("Não foi possível emitir as métricas: %v", err)
}

Bom: Correspondendo o erro e degradando graciosamente apropriadamente

Se o chamador tiver definido um erro específico em seu acordo e a falha for recuperável, corresponda esse caso de erro e degradação graciosa. Para todos os outros casos, envolva o erro e retorne-o. Os erros mais acima na pilha lidarão com outros erros.

tz, err := getUserTimeZone(id)
if err != nil {
  if errors.Is(err, ErrUserNotFound) {
    // O usuário não existe. Use UTC.
    tz = time.UTC
  } else {
    return fmt.Errorf("obter usuário %q: %w", id, err)
  }
}

Lidando com Falhas de Assertividade

Asserções de tipo irão gerar um pânico com um único valor de retorno em caso de detecção de tipo incorreto. Portanto, sempre use o "comma, ok" idioma.

Não Recomendado:

t := i.(string)

Recomendado:

t, ok := i.(string)
if !ok {
  // Lidar com o erro graciosamente
}

Evite usar o pânico

O código em execução no ambiente de produção deve evitar o uso do pânico. O pânico é a principal fonte de falhas em cascata. Se ocorrer um erro, a função deve retornar o erro e permitir que o chamador decida como lidar com ele.

Não recomendado:

func run(args []string) {
  if len(args) == 0 {
    panic("um argumento é necessário")
  }
  // ...
}

func main() {
  run(os.Args[1:])
}

Recomendado:

func run(args []string) error {
  if len(args) == 0 {
    return errors.New("um argumento é necessário")
  }
  // ...
  return nil
}

func main() {
  if err := run(os.Args[1:]); err != nil {
    fmt.Fprintln(os.Stderr, err)
    os.Exit(1)
  }
}

O pânico/recuperação não é uma estratégia de tratamento de erro. Deve ocorrer um pânico apenas quando ocorre um evento irreparável (por exemplo, referência nula). Uma exceção ocorre durante a inicialização do programa: situações que causariam um pânico no programa devem ser tratadas durante o início do programa.

var _statusTemplate = template.Must(template.New("nome").Parse("_statusHTML"))

Mesmo no código de teste, é preferível usar t.Fatal ou t.FailNow em vez de pânico para garantir que as falhas sejam marcadas.

Não recomendado:

// func TestFoo(t *testing.T)

f, err := os.CreateTemp("", "teste")
if err != nil {
  panic("falha ao configurar o teste")
}

Recomendado:

// func TestFoo(t *testing.T)

f, err := os.CreateTemp("", "teste")
if err != nil {
  t.Fatal("falha ao configurar o teste")
}