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()
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 {
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)
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
, : 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:
, , . 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)
, " ", 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
, .