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
ouerrors.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 usarfmt.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
ouerrors.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")
}