Liberando a manipulação de erros eliminando erros



O Go2 visa reduzir a sobrecarga do tratamento de erros, mas você sabia o que é melhor do que a sintaxe aprimorada para o tratamento de erros?

Não há necessidade de lidar com erros. Não digo "excluir seu código de tratamento de erros"; em vez disso, sugiro alterá-lo para que você não tenha muitos erros.

Este artigo é inspirado no capítulo "Definir erros fora da existência" "do livro" Uma filosofia de design de software ", de John Ousterhout. Vou tentar aplicar o seu conselho ao Go.

Primeiro exemplo


Aqui está a função para contar o número de linhas em um arquivo:

func CountLines(r io.Reader) (int, error) { var ( br = bufio.NewReader(r) lines int err error ) for { _, err = br.ReadString('\n') lines++ if err != nil { break } } if err != io.EOF { return 0, err } return lines, nil } 

Criamos o bufio.Reader, sentamos em loop, chamando o método ReadString, aumentando o contador até chegarmos ao final do arquivo e retornamos o número de linhas lidas. Este é o código que queremos escrever, em vez disso, o CountLines é complicado pelo tratamento de erros.

Por exemplo, há uma construção tão estranha:

 _, err = br.ReadString('\n') lines++ if err != nil { break } 

Aumentamos o número de linhas antes de verificar se há erros - isso parece estranho. A razão pela qual devemos escrever dessa maneira é porque o ReadString retornará um erro se encontrar o final do arquivo - io.EOF - antes de pressionar o caractere de nova linha. Isso também pode acontecer se não houver nova linha.

Para resolver esse problema, reorganizamos a lógica para aumentar o número de linhas e, em seguida, verificamos se precisamos sair do loop (essa lógica ainda não está correta, você pode encontrar um erro?).

Mas ainda não terminamos de verificar se há erros. O ReadString retornará io.EOF quando atingir o final do arquivo. Isso é esperado, o ReadString precisa de uma maneira de dizer parar, não há mais nada para ler. Portanto, antes de retornar o erro ao chamador do CountLine, precisamos verificar se o erro io.EOF não foi encontrado e, nesse caso, devolvê-lo ao chamador, caso contrário, retornaremos nulo quando tudo estiver bem. É por isso que a última linha da função não é fácil

 return lines, err 

Penso que este é um bom exemplo da observação de Russ Cox de que o tratamento de erros pode dificultar a função . Vejamos a versão melhorada.

 func CountLines(r io.Reader) (int, error) { sc := bufio.NewScanner(r) lines := 0 for sc.Scan() { lines++ } return lines, sc.Err() } 

Essa versão aprimorada faz a transição do uso do bufio.Reader para o bufio.Scanner. Sob o capô, o bufio.Scanner usa o bufio.Reader, adicionando uma camada de abstração que ajuda a remover o tratamento de erros, o que dificulta o trabalho da versão anterior do CountLines (o bufio.Scanner pode digitalizar qualquer modelo, por padrão, procura novas linhas).

O método sc.Scan () retorna true se o scanner encontrou uma linha de texto e não encontrou um erro. Assim, o corpo do nosso loop for será chamado apenas quando houver uma linha de texto no buffer do scanner. Isso significa que nosso CountLines refeito lida corretamente com o caso quando não há um caractere de nova linha à direita. Agora também o caso em que o arquivo está vazio é tratado corretamente.

Em segundo lugar, como o sc.Scan retorna false quando ocorre um erro, nosso loop for terminará quando o final do arquivo for atingido ou ocorrer um erro. O tipo bufio.Scanner se lembra do primeiro erro detectado e corrigimos esse erro após sair do loop usando o método sc.Err ().

Por fim, o buffo.Scanner cuida do processamento de io.EOF e o converte em zero se o final do arquivo for alcançado sem erro.

Segundo exemplo


Meu segundo exemplo é inspirado nos erros de Rob Pikes são os valores postados no blog.

Ao trabalhar com a abertura, gravação e fechamento de arquivos, a manipulação de erros é, mas não muito impressionante, porque as operações podem ser concluídas em auxiliares como ioutil.ReadFile e ioutil.WriteFile. No entanto, ao trabalhar com protocolos de rede de baixo nível, muitas vezes é necessário construir uma resposta diretamente usando primitivas de E / S, para que o tratamento de erros possa começar a se repetir. Considere este fragmento de um servidor HTTP que cria uma resposta HTTP / 1.1:

 type Header struct { Key, Value string } type Status struct { Code int Reason string } func WriteResponse(w io.Writer, st Status, headers []Header, body io.Reader) error { _, err := fmt.Fprintf(w, "HTTP/1.1 %d %s\r\n", st.Code, st.Reason) if err != nil { return err } for _, h := range headers { _, err := fmt.Fprintf(w, "%s: %s\r\n", h.Key, h.Value) if err != nil { return err } } if _, err := fmt.Fprint(w, "\r\n"); err != nil { return err } _, err = io.Copy(w, body) return err } 

Primeiro, criamos uma barra de status usando fmt.Fprintf e verificamos se há um erro. Em seguida, para cada cabeçalho, registramos a chave e o valor do cabeçalho, sempre verificando se há um erro. Por fim, finalizamos a seção do cabeçalho com \ r \ n adicional, verificamos o erro e copiamos o corpo da resposta para o cliente. Finalmente, embora não seja necessário verificar o erro de io.Copy, precisamos convertê-lo de um formulário com dois valores de retorno, que io.Copy retorna ao valor de retorno único que WriteResponse espera.

Isso não é apenas muito trabalho repetitivo, cada operação, que está essencialmente gravando bytes no io.Writer, possui uma forma diferente de tratamento de erros. Mas podemos facilitar nossa tarefa introduzindo um pequeno invólucro.

 type errWriter struct { io.Writer err error } func (e *errWriter) Write(buf []byte) (int, error) { if e.err != nil { return 0, e.err } var n int n, e.err = e.Writer.Write(buf) return n, nil } 

O errWriter cumpre o contrato io.Writer, portanto, ele pode ser usado para migrar um io.Writer existente. O errWriter transfere as gravações para o gravador subjacente até que um erro seja detectado. A partir de agora, ele descarta todas as entradas e retorna o erro anterior.

 func WriteResponse(w io.Writer, st Status, headers []Header, body io.Reader) error { ew := &errWriter{Writer: w} fmt.Fprintf(ew, "HTTP/1.1 %d %s\r\n", st.Code, st.Reason) for _, h := range headers { fmt.Fprintf(ew, "%s: %s\r\n", h.Key, h.Value) } fmt.Fprint(ew, "\r\n") io.Copy(ew, body) return ew.err } 

A aplicação de errWriter ao WriteResponse melhora muito a clareza do código. Cada uma das operações não precisa mais se limitar à verificação de erros. A mensagem de erro é movida para o final da função, verificando o campo ew.err e evitando a conversão irritante dos io io copiados.

Conclusão


Ao encontrar um tratamento excessivo de erros, tente extrair algumas operações como um tipo de wrapper auxiliar.

Sobre o autor


O autor deste artigo, Dave Cheney , é o autor de muitos pacotes populares do Go, como github.com/pkg/errors e github.com/davecheney/httpstat .

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


All Articles