Na última década, exploramos com sucesso o fato de que o Go lida com
erros como valores . Embora a biblioteca padrão tenha suporte mínimo para erros: somente as funções
fmt.Errorf
e
fmt.Errorf
que geram um erro contendo apenas uma mensagem - a interface interna permite que os programadores da Go adicionem qualquer informação. Tudo que você precisa é de um tipo que implemente o método
Error
:
type QueryError struct { Query string Err error } func (e *QueryError) Error() string { return e.Query + ": " + e.Err.Error() }
Esses tipos de erros são encontrados em todos os idiomas e armazenam uma grande variedade de informações, de carimbos de data e hora a nomes de arquivos e endereços de servidores. Erros de baixo nível que fornecem contexto adicional são frequentemente mencionados.
O padrão, quando um erro contém outro, é encontrado com tanta frequência no Go que, após uma
discussão acalorada no Go 1.13, seu suporte explícito foi adicionado. Neste artigo, examinaremos as adições à biblioteca padrão que fornecem o suporte mencionado: três novas funções no pacote de erros e um novo comando de formatação para
fmt.Errorf
.
Antes de discutir as alterações em detalhes, vamos falar sobre como os erros foram investigados e construídos nas versões anteriores do idioma.
Erros antes do Go 1.13
Pesquisa de erro
Erros no Go são significados. Os programas tomam decisões com base nesses valores de maneiras diferentes. Na maioria das vezes, o erro é comparado a zero para verificar se a operação falhou.
if err != nil {
Às vezes, comparamos o erro para descobrir o valor do
controle e ver se ocorreu um erro específico.
var ErrNotFound = errors.New("not found") if err == ErrNotFound {
O valor do erro pode ser de qualquer tipo que satisfaça a interface de erro definida no idioma. Um programa pode usar uma instrução de tipo ou uma opção de tipo para exibir o valor do erro de um tipo mais específico.
type NotFoundError struct { Name string } func (e *NotFoundError) Error() string { return e.Name + ": not found" } if e, ok := err.(*NotFoundError); ok {
Adicionando informações
Freqüentemente, uma função passa um erro para a pilha de chamadas, adicionando informações a ela, por exemplo, uma breve descrição do que aconteceu quando o erro ocorreu. Isso é fácil, basta construir um novo erro que inclua o texto do erro anterior:
if err != nil { return fmt.Errorf("decompress %v: %v", name, err) }
Ao criar um novo erro usando
fmt.Errorf
descartamos tudo, exceto o texto do erro original. Como vimos no exemplo
QueryError
, às vezes você precisa definir um novo tipo de erro que contenha o erro original para salvá-lo para análise usando o código:
type QueryError struct { Query string Err error }
Os programas podem procurar dentro do
*QueryError
e tomar uma decisão com base no erro original. Isso às vezes é chamado de desembrulhar um erro.
if e, ok := err.(*QueryError); ok && e.Err == ErrPermission {
O tipo
os.PathError
da biblioteca padrão é outro exemplo de como um erro contém outro.
Erros no Go 1.13
Desembrulhar o método
No Go 1.13, os pacotes de biblioteca padrão
errors
e
fmt
simplificaram o
fmt
erros que contêm outros erros. O mais importante é a convenção, não a mudança: um erro contendo outro erro pode implementar o método
Unwrap
, que retorna o erro original. Se
e1.Unwrap()
retornar
e2
, dizemos que
e1
empacota e2
e você pode
descompactar e1
para obter
e2
.
De acordo com esta convenção, você pode atribuir o tipo
QueryError
descrito acima ao método
QueryError
, que retorna o erro contido nele:
func (e *QueryError) Unwrap() error { return e.Err }
O resultado da descompactação de erro também pode conter o método
Unwrap
. A sequência de erros obtidos através da descompactação repetida, chamamos de
cadeia de erros .
Investigação de erro com Is e As
No Go 1.13, o pacote de
errors
contém duas novas funções para investigar erros:
Is
e
As
.
A função
errors.Is
compara um erro com um valor.
A função As verifica se o erro é de um tipo específico.
No caso mais simples, os
errors.Is
função se comporta como uma comparação com um erro de controle e os
errors.As
função se comporta como uma instrução de tipo. No entanto, ao trabalhar com erros compactados, essas funções avaliam todos os erros na cadeia. Vejamos o exemplo de
QueryError
acima para examinar o erro original:
if e, ok := err.(*QueryError); ok && e.Err == ErrPermission {
Usando a função
errors.Is
,
errors.Is
pode escrever isso:
if errors.Is(err, ErrPermission) {
O pacote de
errors
também contém uma nova função
Unwrap
que retorna o resultado da chamada do método
Unwrap
do erro ou retorna nulo se o erro não tiver o método
Unwrap
. Geralmente, é melhor usar
errors.Is
ou
errors.As
. Como eles permitem que você examine toda a cadeia com uma única chamada.
Erro ao compactar com% w
Como mencionei, é prática comum usar a função
fmt.Errorf
para adicionar informações adicionais ao erro.
if err != nil { return fmt.Errorf("decompress %v: %v", name, err) }
No Go 1.13, a função
fmt.Errorf
suporta o novo comando
%w
. Se for, o erro retornado por
fmt.Errorf
conterá o método
Unwrap
que retorna o argumento
%w
, que deve ser um erro. Em todos os outros casos,
%w
idêntico a
%v
.
if err != nil {
A compactação do erro com
%w
torna disponível para
errors.As
.
err := fmt.Errorf("access denied: %w", ErrPermission) ... if errors.Is(err, ErrPermission) ...
Quando fazer as malas?
Ao adicionar um contexto adicional ao erro usando
fmt.Errorf
ou uma implementação de tipo personalizado, você precisa decidir se o novo erro conterá o original. Não existe uma resposta única para isso, tudo depende do contexto em que o novo erro é criado. Pack para mostrar a sua chamada. Não empacote o erro se isso levar à divulgação dos detalhes da implementação.
Por exemplo, imagine uma função
Parse
que lê uma estrutura de dados complexa no
io.Reader
. Se ocorrer um erro, vamos descobrir o número da linha e coluna em que ocorreu. Se ocorreu um erro durante a leitura do
io.Reader
, precisaremos
io.Reader
-lo para descobrir o motivo. Como o chamador foi fornecido com a função
io.Reader
, faz sentido mostrar o erro que ele gerou.
Outro caso: uma função que faz várias chamadas ao banco de dados provavelmente não deve retornar um erro no qual o resultado de uma dessas chamadas é compactado. Se o banco de dados usado por essa função fizer parte da implementação, a divulgação desses erros violará a abstração. Por exemplo, se a função
LookupUser
do pacote
pkg
usar o pacote Go
database/sql
, poderá encontrar o erro
sql.ErrNoRows
. Se você retornar um erro usando
fmt.Errorf("accessing DB: %v", err)
, o chamador não poderá olhar para dentro e encontrar
sql.ErrNoRows
. Mas se a função retornar
fmt.Errorf("accessing DB: %w", err)
, o chamador poderá escrever:
err := pkg.LookupUser(...) if errors.Is(err, sql.ErrNoRows) …
Nesse caso, a função sempre deve retornar
sql.ErrNoRows
se você não desejar interromper os clientes, mesmo ao alternar para um pacote com um banco de dados diferente. Em outras palavras, o empacotamento faz parte de um erro da sua API. Se você não deseja confirmar o suporte para esse erro no futuro como parte da API, não o empacote.
É importante lembrar que, independentemente de você o levar ou não, o erro permanecerá inalterado.
Uma pessoa que entenderá terá as mesmas informações. A tomada de decisões sobre o empacotamento depende da necessidade de informações adicionais para os
programas, para que possam tomar decisões mais informadas; ou se você deseja ocultar essas informações para manter o nível de abstração.
Configurando o teste de erros usando os métodos Is e As
A função
errors.Is
verifica todos os erros da cadeia em relação ao valor alvo. Por padrão, um erro corresponde a esse valor se eles forem equivalentes. Além disso, um erro na cadeia pode declarar sua conformidade com o valor de destino usando a implementação do
método Is
.
Considere o erro causado
pelo pacote Upspin , que compara o erro com o modelo e avalia apenas campos diferentes de zero:
type Error struct { Path string User string } func (e *Error) Is(target error) bool { t, ok := target.(*Error) if !ok { return false } return (e.Path == t.Path || t.Path == "") && (e.User == t.User || t.User == "") } if errors.Is(err, &Error{User: "someuser"}) {
A função
errors.As
também aconselha o método As, se houver.
APIs de erros e pacotes
Um pacote que retorna erros (e a maioria dos pacotes faz isso) deve descrever as propriedades desses erros nos quais um programador pode confiar. Um pacote bem projetado também evitará retornar erros com propriedades que não podem ser consideradas.
O mais simples é dizer se a operação foi bem-sucedida, retornando, respectivamente, o valor nulo ou não nulo. Em muitos casos, nenhuma outra informação é necessária.
Se você precisar que a função retorne um estado de erro identificável, por exemplo, "elemento não encontrado", poderá retornar um erro no qual o valor do sinal está compactado.
var ErrNotFound = errors.New("not found")
Existem outros padrões para fornecer erros que o chamador pode examinar semanticamente. Por exemplo, retorne diretamente um valor de controle, um tipo específico ou um valor que possa ser analisado usando uma função predicativa.
De qualquer forma, não divulgue os detalhes internos ao usuário. Conforme mencionado no capítulo “Quando vale a pena embalar?”, Se você retornar um erro de outro pacote, converta-o para não revelar o erro original, a menos que pretenda se comprometer a retornar esse erro específico no futuro.
f, err := os.Open(filename) if err != nil {
Se uma função retornar um erro com um valor ou tipo de sinal compactado, não retorne diretamente o erro original.
var ErrPermission = errors.New("permission denied")
Conclusão
Embora tenhamos discutido apenas três funções e um comando de formatação, esperamos que eles ajudem a melhorar bastante o tratamento de erros nos programas Go. Esperamos que o empacotamento com o objetivo de fornecer contexto adicional se torne uma prática normal, ajudando os programadores a tomar melhores decisões e encontrar bugs mais rapidamente.
Como Russ Cox disse em seu
discurso na GopherCon 2019 , no caminho para o Go 2, experimentamos, simplificamos e enviamos. E agora, depois de enviar essas mudanças, iniciamos novos experimentos.