Lidando com bugs no Go 1.13


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 { // something went wrong } 

À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 { // something wasn't found } 

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 { // e.Name wasn't found } 

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 { // query failed because of a permission problem } 

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.

 // Similar to: // if err == ErrNotFound { … } if errors.Is(err, ErrNotFound) { // something wasn't found } 

A função As verifica se o erro é de um tipo específico.

 // Similar to: // if e, ok := err.(*QueryError); ok { … } var e *QueryError if errors.As(err, &e) { // err is a *QueryError, and e is set to the error's value } 

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 { // query failed because of a permission problem } 

Usando a função errors.Is , errors.Is pode escrever isso:

 if errors.Is(err, ErrPermission) { // err, or some error that it wraps, is a permission problem } 

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 { // Return an error which unwraps to err. return fmt.Errorf("decompress %v: %w", name, err) } 

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"}) { // err's User field is "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") // FetchItem returns the named item. // // If no item with the name exists, FetchItem returns an error // wrapping ErrNotFound. func FetchItem(name string) (*Item, error) { if itemNotFound(name) { return nil, fmt.Errorf("%q: %w", name, ErrNotFound) } // ... } 

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 { // The *os.PathError returned by os.Open is an internal detail. // To avoid exposing it to the caller, repackage it as a new // error with the same text. We use the %v formatting verb, since // %w would permit the caller to unwrap the original *os.PathError. return fmt.Errorf("%v", err) } 

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") // DoSomething returns an error wrapping ErrPermission if the user // does not have permission to do something. func DoSomething() { if !userHasPermission() { // If we return ErrPermission directly, callers might come // to depend on the exact error value, writing code like this: // // if err := pkg.DoSomething(); err == pkg.ErrPermission { … } // // This will cause problems if we want to add additional // context to the error in the future. To avoid this, we // return an error wrapping the sentinel so that users must // always unwrap it: // // if err := pkg.DoSomething(); errors.Is(err, pkg.ErrPermission) { ... } return fmt.Errorf("%w", ErrPermission) } // ... } 

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.

Source: https://habr.com/ru/post/pt473658/


All Articles