Proposta: try - função de verificação de erro integrada

Sumário


É proposta uma nova construção de try projetada especificamente para eliminar expressões if geralmente associadas ao tratamento de erros no Go. Esta é a única mudança no idioma. Os autores suportam o uso das funções de defer e biblioteca padrão para enriquecer ou agrupar erros. Essa pequena extensão é adequada para a maioria dos cenários, praticamente sem complicar o idioma.


A construção try é fácil de explicar, fácil de implementar, essa funcionalidade é ortogonal a outras construções de linguagem e é totalmente compatível com versões anteriores. Também é extensível se o desejarmos no futuro.


O restante deste documento está organizado da seguinte maneira: após uma breve introdução, damos uma definição da função interna e explicamos seu uso na prática. A seção de discussão analisa sugestões alternativas e o design atual. No final, serão dadas as conclusões e o plano de implementação com exemplos e uma seção de perguntas e respostas.


1. Introdução


Na última conferência Gophercon em Denver, membros da equipe Go (Russ Cox, Marcel van Lohuizen) apresentaram algumas novas idéias sobre como reduzir a tediosidade do tratamento manual de erros no Go ( design de rascunho ). Desde então, recebemos uma enorme quantidade de feedback.


Como Russ Cox explicou em sua análise do problema , nosso objetivo é tornar o tratamento de erros mais leve, reduzindo a quantidade de código dedicado especificamente à verificação de erros. Também queremos tornar mais conveniente escrever código de tratamento de erros, aumentando a probabilidade de que os desenvolvedores ainda dediquem tempo para corrigir o tratamento de erros. Ao mesmo tempo, queremos deixar o código de tratamento de erros claramente visível no código do programa.


As idéias discutidas no rascunho estão concentradas em torno da nova declaração de check unária, que simplifica a verificação explícita do valor de erro obtido de alguma expressão (geralmente uma chamada de função), bem como a declaração dos manipuladores de erros ( handle ) e um conjunto de regras que conectam essas duas novas construções de linguagem.


A maior parte do feedback que recebemos se concentrou nos detalhes e na complexidade do design da handle , e a ideia de um operador de verificação se mostrou mais atraente. De fato, vários membros da comunidade adotaram a idéia de um operador de check e a expandiram. Aqui estão algumas postagens mais semelhantes à nossa oferta:



A proposta atual, embora diferente em detalhes, baseou-se nesses três e, em geral, no feedback recebido sobre o projeto de projeto proposto no ano passado.


Para completar a imagem, queremos observar que ainda mais sugestões de manipulação de erros podem ser encontradas nesta página da wiki . Também é importante notar que Liam Breck veio com um extenso conjunto de requisitos para o mecanismo de tratamento de erros.


Finalmente, após a publicação desta proposta, descobrimos que Ryan Hileman implementou a try há cinco anos usando a ferramenta og rewriter e a usou com sucesso em projetos reais. Consulte ( https://news.ycombinator.com/item?id=20101417 ).


Função de tentativa incorporada


Oferta


Sugerimos adicionar um novo elemento de linguagem semelhante à função chamado try e chamado com uma assinatura


 func try(expr) (T1, T2, ... Tn) 

onde expr significa uma expressão de um parâmetro de entrada (geralmente uma chamada de função) que retorna n + 1 valores dos tipos T1, T2, ... Tn e error para o último valor. Se expr for um valor único (n = 0), esse valor deverá ser do tipo error e a try não retornará um resultado. Chamar try com uma expressão que não retorna o último valor do error de tipo resulta em um erro de compilação.


A construção try só pode ser usada em uma função que retorna pelo menos um valor e cujo último valor de retorno é do tipo error . Chamar try em outros contextos leva a um erro de compilação.


Chame try com a função f() como no exemplo


 x1, x2, … xn = try(f()) 

leva ao seguinte código:


 t1, … tn, te := f() // t1, … tn,  ()   if te != nil { err = te //  te    error return //     } x1, … xn = t1, … tn //     //     

Em outras palavras, se o último tipo de error retornado por expr for nil , try simplesmente retornar os primeiros n valores, removendo o nil final.


Se o último valor retornado por expr não for nil , então:


  • O valor de retorno de error da função envolvente (no pseudocódigo acima nomeado err , embora possa ser qualquer identificador ou valor de retorno sem nome) recebe o valor de erro retornado de expr
  • existe uma saída da função de envelope
  • se a função envolvente tiver parâmetros de retorno adicionais, esses parâmetros reterão os valores que estavam contidos neles antes da chamada de try .
  • se a função envolvente tiver parâmetros de retorno adicionais sem nome, os valores zero correspondentes serão retornados para eles (o que é idêntico a salvar seus valores zero originais com os quais foram inicializados).

Se try usado em várias atribuições, como no exemplo acima, e um erro diferente de zero (daqui em diante, nulo - aprox. Por.) For detectado, a atribuição (pelas variáveis ​​do usuário) não será executada e nenhuma das variáveis ​​no lado esquerdo da atribuição será alterada. Ou seja, try se comportar como uma chamada de função: seus resultados estarão disponíveis apenas se try retornar o controle ao chamador (em oposição ao caso com um retorno da função que o encerra). Como resultado, se as variáveis ​​no lado esquerdo da atribuição forem parâmetros de retorno, o uso de try resultará em um comportamento diferente do código típico encontrado agora. Por exemplo, se a,b, err forem nomeados parâmetros de retorno de uma função de fechamento, aqui está este código:


 a, b, err = f() if err != nil { return } 

sempre atribuirá valores às variáveis a, b err , independentemente de a chamada para f() retornado um erro ou não. Desafio contrário


 a, b = try(f()) 

em caso de erro, deixe b inalterados. Apesar de ser uma nuance sutil, acreditamos que esses casos são bastante raros. Se o comportamento de atribuição incondicional for necessário, você deverá continuar usando expressões if .


Use


A definição de try explica explicitamente como usá-lo: muitas expressões que verificam retornos de erro podem ser substituídas por try . Por exemplo:


 f, err := os.Open(filename) if err != nil { return …, err //       } 

pode ser simplificado para


 f := try(os.Open(filename)) 

Se a função de chamada não retornar um erro, a try não poderá ser usada (consulte a seção Discussão). Nesse caso, o erro deve, em qualquer caso, ser processado localmente (já que não há retorno de erro) e, nesse caso, if permanecer o mecanismo apropriado para verificar se há erros.


De um modo geral, nosso objetivo não é substituir todas as possíveis verificações de erro por uma try . Código que requer diferentes semânticas pode e deve continuar a ser usado if expressões e variáveis ​​explícitas com valores de erro.


Testando e tentando


Em uma de nossas tentativas anteriores de escrever uma especificação (consulte a seção de iteração de design abaixo), o try foi projetado para entrar em pânico quando ocorre um erro quando usado dentro de uma função sem um erro de retorno. Isso permitiu o uso de testes em unidade com base no pacote de testing da biblioteca padrão.


Como uma das opções, é possível usar funções de teste com assinaturas no pacote de testing


 func TestXxx(*testing.T) error func BenchmarkXxx(*testing.B) error 

para permitir o uso de try em testes. Uma função de teste que retorna um erro diferente de zero chama implicitamente t.Fatal(err) ou b.Fatal(err) . Essa é uma pequena alteração na biblioteca que evita a necessidade de comportamentos diferentes (retorno ou pânico) para a try , dependendo do contexto.


Uma das desvantagens dessa abordagem é que t.Fatal e b.Fatal não poderão retornar o número da linha na qual o teste caiu. Outra desvantagem é que devemos, de alguma forma, mudar os subtestes também. A solução para esse problema é uma questão em aberto; não propomos alterações específicas no pacote de testing neste documento.


Consulte também o número 21111 , que sugere permitir que funções de exemplo retornem um erro.


Tratamento de erros


O projeto de rascunho original preocupava-se principalmente com o suporte ao idioma para ocultar ou aumentar erros. O rascunho propôs um novo handle palavras-chave e uma nova maneira de declarar manipuladores de erros . Esse novo construto de linguagem atraiu problemas como moscas devido a semânticas não triviais, principalmente ao considerar seu efeito no fluxo de execução. Em particular, a funcionalidade do handle cruzou-se miseravelmente com a função defer , que tornou o novo idioma não ortogonal a todo o resto.


Esta proposta reduz o design do rascunho original à sua essência. Se for necessário enriquecer ou agrupar erros, existem duas abordagens: anexar a if err != nil { return err} ou "declarar" um manipulador de erros dentro da expressão defer :


 defer func() { if err != nil { //      -   err = … // /  } }() 

Neste exemplo, err é o nome do parâmetro de retorno do tipo error função envolvente.


Na prática, imaginamos funções auxiliares como


 func HandleErrorf(err *error, format string, args ...interface{}) { if *err != nil { *err = fmt.Errorf(format + ": %v", append(args, *err)...) } } 

ou algo semelhante. O pacote fmt pode se tornar um local natural para esses auxiliares (ele já fornece o fmt.Errorf ). Usando auxiliares, a definição de um manipulador de erros será, em muitos casos, reduzida a uma única linha. Por exemplo, para enriquecer o erro da função "copiar", você pode escrever


 defer fmt.HandleErrorf(&err, "copy %s %s", src, dst) 

Se fmt.HandleErrorf adicionar implicitamente informações de erro, essa construção é bastante fácil de ler e tem a vantagem de poder ser implementada sem a adição de novos elementos da sintaxe da linguagem.


A principal desvantagem dessa abordagem é que o parâmetro de erro retornado deve ser nomeado, o que potencialmente leva a uma API menos precisa (consulte as Perguntas frequentes sobre este tópico). Acreditamos que nos acostumaremos a isso quando o estilo apropriado de escrever código for estabelecido.


Diferimento de eficiência


Uma consideração importante ao usar o defer como manipulador de erros é a eficiência. A expressão defer é considerada lenta . Não queremos escolher entre código eficiente e boa manipulação de erros. Independentemente dessa proposta, as equipes de tempo de execução e compilador Go discutiram métodos de implementação alternativos e acreditamos que podemos criar maneiras típicas de usar o adiamento para manipular erros comparáveis ​​em eficiência ao código "manual" existente. Esperamos adicionar uma implementação mais rápida do defer no Go 1.14 (consulte também o bilhete CL 171158 , que é o primeiro passo nessa direção).


Casos especiais go try(f), defer try(f)


A construção try parece com uma função e, por causa disso, espera-se que ela possa ser usada em qualquer lugar em que uma chamada de função seja aceitável. No entanto, se a chamada try for usada na instrução go , as coisas ficarão complicadas:


 go try(f()) 

Aqui f() é executado quando a expressão go é executada na goroutine atual, os resultados da chamada f são passados ​​como argumentos a serem tentados, que começam na nova goroutine. Se f retornar um erro diferente de zero, espera-se que a try retorne da função de fechamento; no entanto, não há função (e não há parâmetro de retorno do tipo error ), porque o código é executado em uma goroutine separada. Por esse motivo, propomos desativar a try em uma expressão go .


Situação com


 defer try(f()) 

parece semelhante, mas aqui a semântica do defer significa que a execução do try será adiada até que retorne da função envolvente. Como antes, f() avaliado quando o defer e seus resultados são passados ​​para a try adiada.


try verifica o erro f() retornado apenas no último momento antes de retornar da função de fechamento. Sem alterar o comportamento de try , esse erro pode substituir outro valor de erro que a função envolvente está tentando retornar. Isso, na melhor das hipóteses, confunde, na pior das hipóteses, provoca erros. Por esse motivo, propomos que você proíba a try chamar também na declaração defer . Sempre podemos reconsiderar essa decisão se houver uma aplicação razoável dessa semântica.


Por fim, como o restante das construções try , o try só pode ser usado como uma chamada; ele não pode ser usado como uma função de valor ou em uma expressão de atribuição de variável como em f := try (assim como f := print f := new são proibidos).


A discussão


Iterações de Design


A seguir, é apresentada uma breve discussão de projetos anteriores que levaram à proposta mínima atual. Esperamos que isso possa esclarecer as decisões de design selecionadas.


Nossa primeira iteração desta frase foi inspirada em duas idéias do artigo “Partes principais do tratamento de erros”, ou seja, usando a função interna em vez do operador e a função Go usual para lidar com erros em vez da nova construção de idioma. Ao contrário dessa publicação, nosso manipulador de erros tinha um func(error) error assinatura fixo para simplificar as coisas. Um manipulador de erros seria chamado pela função try se houvesse um erro antes que o try saia da função envolvente. Aqui está um exemplo:


 handler := func(err error) error { return fmt.Errorf("foo failed: %v", err) //   } f := try(os.Open(filename), handler) //      

Embora essa abordagem tenha permitido a definição de manipuladores de erro eficazes definidos pelo usuário, também levantou muitas questões que obviamente não tinham as respostas corretas: O que deveria acontecer se nada fosse passado para o manipulador? Você deve try entrar try pânico ou considerar isso como uma falta de um manipulador? E se o manipulador for chamado com um erro diferente de zero e retornar um resultado nulo? Isso significa que o erro foi "cancelado"? Ou uma função de fechamento deve retornar um erro vazio? Havia também dúvidas de que a transferência opcional de um manipulador de erros encorajaria os desenvolvedores a ignorar os erros em vez de corrigi-los. Também seria fácil executar o tratamento correto de erros em qualquer lugar, mas pule um uso de try . E assim por diante.


Na próxima iteração, a capacidade de passar um manipulador de erros personalizado foi removida em favor do uso do defer para quebrar erros. Essa parecia uma abordagem melhor porque tornava os manipuladores de erro muito mais visíveis no código-fonte. Essa etapa também eliminou todos os problemas relacionados à transferência opcional de funções do manipulador, mas exigiu que os parâmetros retornados com o tipo de error fossem nomeados se o acesso fosse necessário (decidimos que isso era normal). Além disso, em uma tentativa de tornar a try útil, não apenas nas funções que retornam erros, era necessário tornar o comportamento da try sensível ao contexto: se a try usada no nível do pacote ou se foi chamada dentro de uma função que não retorna um erro, try entrar em pânico automaticamente quando um erro foi detectado. (E, como efeito colateral, devido a essa propriedade, o construto de linguagem foi chamado must vez de try nessa frase.) O comportamento contextual de try (ou must ) parecia natural e também bastante útil: eliminaria muitas funções definidas pelo usuário usadas nas expressões inicializando variáveis ​​do pacote. Também abriu a possibilidade de usar try em testes de unidade com o pacote de testing .


No entanto, o comportamento da try sensível ao contexto estava repleto de erros: por exemplo, o comportamento de uma função usando try pode mudar silenciosamente (entre em pânico ou não) ao adicionar ou remover um erro de retorno à assinatura da função. Parecia uma propriedade muito perigosa. A solução óbvia foi dividir a funcionalidade try em duas funções must e try separadas (muito parecidas com a sugerida na # 31442 ). No entanto, isso exigiria duas funções internas, enquanto apenas try diretamente relacionado ao melhor suporte para o tratamento de erros.


Portanto, na iteração atual, em vez de incluir a segunda função interna, decidimos remover a semântica dupla de try e, portanto, permitir seu uso apenas em funções que retornam um erro.


Características do projeto proposto


Essa sugestão é bastante curta e pode parecer um passo atrás em comparação com o rascunho do ano passado. Acreditamos que as soluções selecionadas são justificadas:


  • Primeiramente, o try tem exatamente a mesma semântica da declaração de check proposta no original, sem handle . Isso confirma a fidelidade do rascunho original em um dos aspectos importantes.


  • Escolher uma função interna em vez de operadores tem várias vantagens. Não requer uma nova palavra-chave como check , o que tornaria o design incompatível com os analisadores existentes. Também não há necessidade de expandir a sintaxe das expressões com um novo operador. Adicionar uma nova função interna é relativamente trivial e completamente ortogonal a outros recursos da linguagem.


  • Usar uma função embutida em vez de um operador requer o uso de parênteses. Deveríamos escrever try(f()) vez de try f() . Esse é o preço (pequeno) que devemos pagar pela compatibilidade com os analisadores existentes. No entanto, isso também torna o design compatível com versões futuras: se decidirmos ao longo do caminho que passar de alguma forma uma função de tratamento de erros ou adicionar um parâmetro adicional para try com esse objetivo é uma boa idéia, adicionar um argumento adicional à chamada try será trivial.


  • Como se viu, a necessidade de escrever colchetes tem suas vantagens. Em expressões mais complexas com várias chamadas de try , os parênteses melhoram a legibilidade, eliminando a necessidade de lidar com a precedência do operador, como nos exemplos a seguir:



 info := try(try(os.Open(file)).Stat()) //   try info := try (try os.Open(file)).Stat() //  try   info := try (try (os.Open(file)).Stat()) //  try   

try , : try , .. try (receiver) .Stat ( os.Open ).


try , : os.Open(file) .. try ( , try os , , try try ).


, .. .


  • . , . , , , .

Conclusões


. , . defer , .


Go - , . , Go append . append , . , . , try .


, , Go : panic recover . error try .


, try , , — — , . Go:


  • , try
  • -

, , . if -.


Implementação


:


  • Go.
  • try . , . .
  • go/types try . .
  • gccgo . ( , ).
  • .

- , . , . .


Robert Griesemer go/types , () cmd/compile . , Go 1.14, 1 2019.


, Ian Lance Taylor gccgo , .


"Go 2, !" , .


1 , , , Go 1.14 .



CopyFile :


 func CopyFile(src, dst string) (err error) { defer func() { if err != nil { err = fmt.Errorf("copy %s %s: %v", src, dst, err) } }() r := try(os.Open(src)) defer r.Close() w := try(os.Create(dst)) defer func() { w.Close() if err != nil { os.Remove(dst) //    “try”    } }() try(io.Copy(w, r)) try(w.Close()) return nil } 

, " ", defer :


 defer fmt.HandleErrorf(&err, "copy %s %s", src, dst) 

( defer -), defer , .


printSum


 func printSum(a, b string) error { x := try(strconv.Atoi(a)) y := try(strconv.Atoi(b)) fmt.Println("result:", x + y) return nil } 

:


 func printSum(a, b string) error { fmt.Println( "result:", try(strconv.Atoi(a)) + try(strconv.Atoi(b)), ) return nil } 

main :


 func localMain() error { hex := try(ioutil.ReadAll(os.Stdin)) data := try(parseHexdump(string(hex))) try(os.Stdout.Write(data)) return nil } func main() { if err := localMain(); err != nil { log.Fatal(err) } } 

- try , :


 n, err := src.Read(buf) if err == io.EOF { break } try(err) 


, .


: ?


: check handle , . , handle defer , handle .


: try ?


: try Go . - , . , . , " ". try , .. .


: try try?


: , check , must do . try , . try check (, ), - . . must ; try — . , Rust Swift try ( ). .


: ? Rust?


: Go ; , Go ( ; - ). , ? , . , , , (package, interface, if, append, recover, ...), , (struct, var, func, int, len, image, ..). Rust ? try — Go, , ( ) . , ? . , , (, ..) . . , .


: ( error) , defer , go doc. ?


: go doc , - ( _ ) , . , func f() (_ A, _ B, err error) go doc func f() (A, B, error) . , , , . , , . , , , -, (deferred) . Jonathan Geddes try() .


: defer ?


: defer . , , defer "" . . CL 171758 , defer 30%.


: ?


: , . , ( , ), . defer , . defer - https://golang.org/issue/29934 ( Go 2), .


: , try, error. , ?


: error ( ) , , nil . try . ( , . - ).


: Go , try ?


: try , try . super return -, try Go . try . .


: try , . ?


: try ; , . try ( ), . , if .


: , . try, defer . ?


: , . .


: try ( catch )?


: try — ("") , , ( ) . try ; . . "" . , . , try — . , , throw try-catch Go. , (, ), ( ) , . "" try-catch , . , , . Go . panic , .

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


All Articles