Go: Proposta: uma função de verificação de erros Go integrada, "tentar"

Criado em 5 jun. 2019  ·  808Comentários  ·  Fonte: golang/go

Proposta: Uma função de verificação de erros Go integrada, try

Esta proposta foi encerrada .

Antes de comentar, leia o documento de design detalhado e veja o resumo da discussão de 6 de junho , o resumo de 10 de junho e _mais importante o conselho sobre como manter o foco _. Sua pergunta ou sugestão pode já ter sido respondida ou feita. Obrigado.

Propomos uma nova função interna chamada try , projetada especificamente para eliminar as instruções padrão if normalmente associadas ao tratamento de erros em Go. Nenhuma outra alteração de idioma é sugerida. Defendemos o uso da instrução defer existente e funções de biblioteca padrão para ajudar a aumentar ou agrupar erros. Essa abordagem mínima aborda os cenários mais comuns ao mesmo tempo em que adiciona muito pouca complexidade à linguagem. O try embutido é fácil de explicar, simples de implementar, ortogonal a outras construções de linguagem e totalmente compatível com versões anteriores. Também deixa em aberto um caminho para estender o mecanismo, caso desejemos fazê-lo no futuro.

[O texto abaixo foi editado para refletir o documento de design com mais precisão.]

A função try recebe uma única expressão como argumento. A expressão deve ser avaliada para n+1 valores (onde n pode ser zero) onde o último valor deve ser do tipo error . Ele retorna os primeiros n valores (se houver) se o argumento de erro (final) for nil, caso contrário, ele retorna da função delimitadora com esse erro. Por exemplo, códigos como

f, err := os.Open(filename)
if err != nil {
    return …, err  // zero values for other results, if any
}

pode ser simplificado para

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

try só pode ser usado em uma função que retorna um resultado error , e esse resultado deve ser o último parâmetro de resultado da função delimitadora.

Esta proposta reduz o projeto original apresentado na GopherCon do ano passado à sua essência. Se o aumento ou encapsulamento de erros for desejado, há duas abordagens: Fique com a instrução if testada e comprovada ou, alternativamente, “declare” um manipulador de erros com uma instrução defer :

defer func() {
    if err != nil { // no error may have occurred - check for it
        err = … // wrap/augment error
    }
}()

Aqui, err é o nome do resultado do erro da função delimitadora. Na prática, funções auxiliares adequadas reduzirão a declaração de um manipulador de erros a uma linha. Por exemplo

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

(onde fmt.HandleErrorf decora *err ) lê bem e pode ser implementado sem a necessidade de novos recursos de linguagem.

A principal desvantagem dessa abordagem é que o parâmetro de resultado do erro precisa ser nomeado, possivelmente levando a APIs menos bonitas. Em última análise, isso é uma questão de estilo, e acreditamos que nos adaptaremos à expectativa do novo estilo, assim como nos adaptamos a não ter ponto e vírgula.

Em resumo, try pode parecer incomum no início, mas é simplesmente um açúcar sintático feito sob medida para uma tarefa específica, tratamento de erros com menos clichê e lidar com essa tarefa bem o suficiente. Como tal, ele se encaixa perfeitamente na filosofia do Go. try não foi projetado para resolver _todas_ as situações de tratamento de erros; ele foi projetado para lidar bem com o caso _mais comum_, para manter o design simples e claro.

Créditos

Esta proposta é fortemente influenciada pelo feedback que recebemos até agora. Especificamente, ele empresta ideias de:

Documento de projeto detalhado

https://github.com/golang/proposal/blob/master/design/32437-try-builtin.md

tryhard ferramenta para explorar o impacto de try

https://github.com/griesemer/tryhard

Go2 LanguageChange Proposal error-handling

Comentários muito úteis

Olá a todos,

Nosso objetivo com propostas como esta é ter uma discussão em toda a comunidade sobre implicações, compensações e como proceder, e então usar essa discussão para ajudar a decidir o caminho a seguir.

Com base na resposta esmagadora da comunidade e extensa discussão aqui, estamos marcando esta proposta como recusada antes do previsto .

No que diz respeito ao feedback técnico, esta discussão identificou algumas considerações importantes que perdemos, principalmente as implicações para adicionar impressões de depuração e analisar a cobertura de código.

Mais importante, ouvimos claramente as muitas pessoas que argumentaram que esta proposta não visava um problema que valesse a pena. Ainda acreditamos que o tratamento de erros em Go não é perfeito e pode ser melhorado significativamente, mas está claro que nós, como comunidade, precisamos falar mais sobre quais aspectos específicos do tratamento de erros são problemas que devemos abordar.

No que diz respeito à discussão do problema a ser resolvido, tentamos expor nossa visão do problema em agosto passado na “ Visão geral do problema de tratamento de erros do Go 2 ”, mas, em retrospectiva, não chamamos atenção suficiente para essa parte e não incentivamos o suficiente discussão sobre se o problema específico era o certo. A proposta try pode ser uma boa solução para o problema descrito lá, mas para muitos de vocês simplesmente não é um problema a ser resolvido. No futuro, precisamos fazer um trabalho melhor chamando a atenção para essas declarações iniciais de problemas e certificando-nos de que haja um amplo acordo sobre o problema que precisa ser resolvido.

(Também é possível que a declaração do problema de tratamento de erros tenha sido totalmente ofuscada pela publicação de um rascunho de design genérico no mesmo dia.)

No tópico mais amplo sobre o que melhorar no tratamento de erros do Go, ficaríamos muito felizes em ver relatórios de experiência sobre quais aspectos do tratamento de erros no Go são mais problemáticos para você em suas próprias bases de código e ambientes de trabalho e qual seria o impacto de uma boa solução. tem em seu próprio desenvolvimento. Se você escrever tal relatório, poste um link na página Go2ErrorHandlingFeedback .

Obrigado a todos que participaram desta discussão, aqui e em outros lugares. Como Russ Cox apontou antes, discussões em toda a comunidade como esta são de código aberto no seu melhor . Agradecemos a ajuda de todos ao examinar esta proposta específica e, de forma mais geral, ao discutir as melhores maneiras de melhorar o estado do tratamento de erros em Go.

Robert Griesemer, para o Comitê de Revisão de Propostas.

Todos 808 comentários

Concordo que este é o melhor caminho a seguir: corrigir o problema mais comum com um design simples.

Eu não quero andar de bicicleta (sinta-se à vontade para adiar esta conversa), mas Rust foi até lá e acabou acertando com o operador ? postfix em vez de uma função interna, para maior legibilidade.

A proposta do gophercon cita ? nas ideias consideradas e dá três razões pelas quais foi descartada: a primeira ("transferências de fluxo de controle são geralmente acompanhadas de palavras-chave") e a terceira ("manipuladores são definidos mais naturalmente com uma palavra-chave, portanto, as verificações também devem") não se aplicam mais. A segunda é estilística: diz que, mesmo que o operador postfix funcione melhor para encadeamento, ele ainda pode ler pior em alguns casos como:

check io.Copy(w, check newReader(foo))

em vez de:

io.Copy(w, newReader(foo)?)?

mas agora teríamos:

try(io.Copy(w, try(newReader(foo))))

o que eu acho que é claramente o pior dos três, já que nem é mais óbvio qual é a função principal que está sendo chamada.

Portanto, a essência do meu comentário é que todas as três razões citadas na proposta do gophercon para não usar ? não se aplicam a esta proposta try ; ? é conciso, muito legível, não obscurece a estrutura da instrução (com sua hierarquia interna de chamada de função) e é encadeável. Ele remove ainda mais a desordem da exibição, sem obscurecer o fluxo de controle mais do que o try() proposto já faz.

Esclarecer:

Faz

func f() (n int, err error) {
  n = 7
  try(errors.New("x"))
  // ...
}

return (0, "x") ou (7, "x")? Eu assumiria o último.

O retorno de erro deve ser nomeado no caso em que não há decoração ou manipulação (como em uma função auxiliar interna)? Eu suponho que não.

Seu exemplo retorna 7, errors.New("x") . Isso deve ficar claro no documento completo que será enviado em breve (https://golang.org/cl/180557).

O parâmetro de resultado de erro não precisa ser nomeado para usar try . Ele só precisa ser nomeado se a função precisar se referir a ele em uma função diferida ou em outro lugar.

Estou realmente insatisfeito com um _function_ embutido que afeta o fluxo de controle do chamador. Eu aprecio a impossibilidade de adicionar novas palavras-chave no Go 1, mas resolver esse problema com funções incorporadas mágicas parece errado para mim. O sombreamento de outros internos não tem resultados tão imprevisíveis quanto a alteração do fluxo de controle.

Não gosto da aparência do postfix ? , mas acho que ainda supera try() .

Edit: Bem, eu consegui esquecer completamente que o pânico existe e não é uma palavra-chave.

A proposta detalhada já está aqui (pendente de melhorias de formatação, em breve) e esperamos responder a muitas perguntas.

@dominikh A proposta detalhada discute isso detalhadamente, mas observe que panic e recover são dois componentes internos que também afetam o fluxo de controle.

Um esclarecimento/sugestão de melhoria:

if the last argument supplied to try, of type error, is not nil, the enclosing function’s error result variable (...) is set to that non-nil error value before the enclosing function returns

Isso poderia dizer is set to that non-nil error value and the enclosing function returns ? (s/antes/e)

Na primeira leitura, before the enclosing function returns parecia que _eventualmente_ definiria o valor do erro em algum ponto no futuro logo antes da função retornar - possivelmente em uma linha posterior. A interpretação correta é que try pode fazer com que a função atual retorne. Esse é um comportamento surpreendente para o idioma atual, portanto, um texto mais claro seria bem-vindo.

Eu acho que isso é apenas açúcar, e um pequeno número de oponentes vocais provocou golang sobre o uso repetido de digitar if err != nil ... e alguém levou isso a sério. Eu não acho que seja um problema. As únicas coisas que faltam são esses dois built-ins:

https://github.com/purpleidea/mgmt/blob/a235b760dc3047a0d66bb0b9d63c25bc746ed274/util/errwrap/errwrap.go#L26

Não tenho certeza por que alguém escreveria uma função como essa, mas qual seria a saída prevista para

try(foobar())

Se foobar retornou (error, error)

Retiro minhas preocupações anteriores sobre o fluxo de controle e não sugiro mais usar ? . Peço desculpas pela resposta automática (embora eu gostaria de salientar que isso não teria acontecido se o problema tivesse sido arquivado _depois_ que a proposta completa estivesse disponível).

Discordo da necessidade de tratamento simplificado de erros, mas tenho certeza de que é uma batalha perdida. try conforme estabelecido na proposta parece ser a maneira menos ruim de fazer isso.

@webermaster Apenas o último resultado error é especial para a expressão passada para try , conforme descrito no documento da proposta.

Como @dominikh , também discordo da necessidade de tratamento de erros simplificado.

Ele move a complexidade vertical para a complexidade horizontal, o que raramente é uma boa ideia.

Se eu realmente tivesse que escolher entre simplificar as propostas de tratamento de erros, essa seria minha proposta preferida.

Seria útil se isso pudesse ser acompanhado (em algum estágio de aceitação) por uma ferramenta para transformar o código Go para usar try em algum subconjunto de funções de retorno de erro onde tal transformação pode ser facilmente executada sem mudando a semântica. Três benefícios me ocorrem:

  • Ao avaliar esta proposta, permitiria que as pessoas tivessem uma noção rápida de como try poderia ser usado em sua base de código.
  • Se try chegar a uma versão futura do Go, as pessoas provavelmente desejarão alterar seu código para usá-lo. Ter uma ferramenta para automatizar os casos fáceis vai ajudar muito.
  • Ter uma maneira de transformar rapidamente uma grande base de código para usar try facilitará a análise dos efeitos da implementação em escala. (Correção, desempenho e tamanho do código, digamos.) A implementação pode ser simples o suficiente para tornar isso uma consideração insignificante, no entanto.

Eu só gostaria de expressar que acho que um simples try(foo()) realmente saindo da função de chamada tira de nós a sugestão visual de que o fluxo da função pode mudar dependendo do resultado.

Eu sinto que posso trabalhar com try com o suficiente para nos acostumarmos, mas também sinto que precisaremos de suporte IDE extra (ou algo assim) para destacar try para reconhecer eficientemente o fluxo implícito nas revisões de código /sessões de depuração

A coisa que mais me preocupa é a necessidade de ter valores de retorno nomeados apenas para que a instrução defer seja feliz.

Eu acho que o problema geral de tratamento de erros que a comunidade reclama é uma combinação do clichê de if err != nil E adicionar contexto aos erros. O FAQ afirma claramente que o último é deixado de fora intencionalmente como um problema separado, mas sinto que isso se torna uma solução incompleta, mas estarei disposto a dar uma chance depois de pensar nessas duas coisas:

  1. Declare err no início da função.
    Isto funciona? Lembro-me de problemas com adiar e resultados sem nome. Se não, a proposta precisa considerar isso.
func sample() (string, error) {
  var err error
  defer fmt.HandleErrorf(&err, "whatever")
  s := try(f())
  return s, nil
}
  1. Atribua valores como fizemos no passado, mas use uma função auxiliar wrapf que tenha o clichê if err != nil .
func sample() (string, error) {
  s, err := f()
  try(wrapf(err, "whatever"))
  return s, nil
}
func wrapf(err error, format string, ...v interface{}) error {
  if err != nil {
    // err = wrapped error
  }
  return err
}

Se qualquer um funcionar, eu posso lidar com isso.

func sample() (string, error) {
  var err error
  defer fmt.HandleErrorf(&err, "whatever")
  s := try(f())
  return s, nil
}

Isso não funcionará. O adiamento atualizará a variável local err , que não está relacionada ao valor de retorno.

func sample() (string, error) {
  s, err := f()
  try(wrapf(err, "whatever"))
  return s, nil
}
func wrapf(err error, format string, ...v interface{}) error {
  if err != nil {
    // err = wrapped error
  }
  return err
}

Isso deve funcionar. No entanto, ele chamará wrapf mesmo em um erro nulo.
Isso também (continuará a) funcionar, e a IMO é muito mais clara:

func sample() (string, error) {
  s, err := f()
  if err != nil {
      return "", wrap(err)
  }
  return s, nil
}

Ninguém vai fazer você usar try .

Não tenho certeza por que alguém escreveria uma função como essa, mas qual seria a saída prevista para

try(foobar())

Se foobar retornou (error, error)

Por que você retornaria mais de um erro de uma função? Se você estiver retornando mais de um erro da função, talvez a função deva ser dividida em duas separadas em primeiro lugar, cada uma retornando apenas um erro.

Poderia detalhar com um exemplo?

@cespare : Deve ser possível para alguém escrever um go fix que reescreva o código existente adequado para try modo que use try . Pode ser útil ter uma ideia de como o código existente pode ser simplificado. Não esperamos nenhuma mudança significativa no tamanho ou desempenho do código, já que try é apenas açúcar sintático, substituindo um padrão comum por um código-fonte mais curto que produz essencialmente o mesmo código de saída. Observe também que o código que usa try será obrigado a usar uma versão Go que seja pelo menos a versão na qual try foi introduzido.

@lestrat : Concordou que terá que aprender que try pode alterar o fluxo de controle. Suspeitamos que os IDEs poderiam destacar isso com bastante facilidade.

@Goodwine : Como @randall77 já apontou, sua primeira sugestão não funcionará. Uma opção que pensamos (mas não discutida no documento) é a possibilidade de ter alguma variável pré-declarada que denota o resultado error (se estiver presente em primeiro lugar). Isso eliminaria a necessidade de nomear esse resultado apenas para que ele possa ser usado em um defer . Mas isso seria ainda mais mágico; não parece justificado. O problema em nomear o resultado de retorno é essencialmente cosmético, e onde isso mais importa é nas APIs geradas automaticamente servidas por go doc e amigos. Seria fácil resolver isso nessas ferramentas (veja também o FAQ do documento de design detalhado sobre este assunto).

@nictuku : Em relação à sua sugestão de esclarecimento (s/antes/e/): acho que o código imediatamente antes do parágrafo ao qual você está se referindo deixa claro o que acontece exatamente, mas vejo seu ponto, s/antes/e/pode tornar a prosa mais clara. Eu vou fazer a mudança.

Veja CL 180637 .

Na verdade, gosto muito desta proposta. No entanto, tenho uma crítica. O ponto de saída das funções em Go sempre foi marcado por um return . Pânicos também são pontos de saída, no entanto, esses são erros catastróficos que normalmente não devem ser encontrados.

Fazer um ponto de saída de uma função que não é return , e deve ser comum, pode levar a um código muito menos legível. Eu tinha ouvido falar sobre isso em uma palestra e é difícil não ver a beleza de como esse código está estruturado:

func CopyFile(src, dst string) error {
    r, err := os.Open(src)
    if err != nil {
        return fmt.Errorf("copy %s %s: %v", src, dst, err)
    }
    defer r.Close()

    w, err := os.Create(dst)
    if err != nil {
        return fmt.Errorf("copy %s %s: %v", src, dst, err)
    }

    if _, err := io.Copy(w, r); err != nil {
        w.Close()
        os.Remove(dst)
        return fmt.Errorf("copy %s %s: %v", src, dst, err)
    }

    if err := w.Close(); err != nil {
        os.Remove(dst)
        return fmt.Errorf("copy %s %s: %v", src, dst, err)
    }
}

Este código pode parecer uma grande bagunça e foi _significado_ pelo rascunho de tratamento de erros, mas vamos compará-lo com a mesma coisa com try .

func CopyFile(src, dst string) error {
    defer func() {
        err = fmt.Errorf("copy %s %s: %v", src, dst, err)
    }()
    r, err := try(os.Open(src))
    defer r.Close()

    w, err := try(os.Create(dst))

    defer w.Close()
    defer os.Remove(dst)
    try(io.Copy(w, r))
    try(w.Close())

    return nil
}

Você pode olhar para isso à primeira vista e achar que parece melhor, porque há muito menos código repetido. No entanto, foi muito fácil identificar todos os pontos que a função retornou no primeiro exemplo. Eles foram todos recuados e começaram com return , seguido por um espaço. Isso se deve ao fato de que todos os retornos condicionais _devem_ estar dentro de blocos condicionais, sendo assim indentados pelos padrões gofmt . return também é, como dito anteriormente, a única maneira de sair de uma função sem dizer que ocorreu um erro catastrófico. No segundo exemplo, há apenas um único return , então parece que a única coisa que a função _ever_ deve retornar é nil . As duas últimas chamadas try são fáceis de ver, mas as duas primeiras são um pouco mais difíceis, e seriam ainda mais difíceis se estivessem aninhadas em algum lugar, ou seja, algo como proc := try(os.FindProcess(try(strconv.Atoi(os.Args[1])))) .

Retornar de uma função parece ter sido uma coisa "sagra" a se fazer, e é por isso que eu pessoalmente acho que todos os pontos de saída de uma função devem ser marcados por return .

Alguém já implementou isso há 5 anos. Se você estiver interessado, você pode
experimente este recurso

https://news.ycombinator.com/item?id=20101417

Implementei try() no Go há cinco anos com um pré-processador AST e usei em projetos reais, ficou bem legal: https://github.com/lunixbochs/og

Aqui estão alguns exemplos de mim usando-o em funções pesadas de verificação de erros: https://github.com/lunixbochs/poxd/blob/master/tls.go#L13

Eu aprecio o esforço que foi feito para isso. Eu acho que é a solução mais go-ey que eu vi até agora. Mas eu acho que introduz um monte de trabalho ao depurar. Desembrulhar tente e adicionar um bloco if toda vez que depurar e reembalá-lo quando terminar é tedioso. E também tenho um certo receio sobre a variável mágica de erro que preciso considerar. Eu nunca me incomodei com a verificação explícita de erros, então talvez eu seja a pessoa errada para perguntar. Sempre me pareceu "pronto para depurar".

@griesemer
Meu problema com sua proposta de uso de defer como forma de lidar com o empacotamento de erros é que o comportamento do snippet que mostrei (repetido abaixo) não é muito comum AFAICT, e por ser muito raro, posso imaginar pessoas escrevendo isso pensando que funciona quando não.

Tipo.. um iniciante não saberia disso, se eles tiverem um bug por causa disso eles não vão "claro, eu preciso de um retorno nomeado", eles ficariam estressados ​​porque deveria funcionar e não funciona.

var err error
defer fmt.HandleErrorf(err);

try já é muito mágico, então você pode ir até o fim e adicionar esse valor de erro implícito. Pense nos iniciantes, não naqueles que conhecem todas as nuances do Go. Se não estiver claro o suficiente, não acho que seja a solução certa.

Ou... Não sugira usar adiar assim, tente outra maneira que seja mais segura, mas ainda legível.

@deanveloper É verdade que esta proposta (e, nesse caso, qualquer proposta que tente a mesma coisa) removerá instruções return explicitamente visíveis do código-fonte - esse é o objetivo da proposta, afinal, não é? Para remover o clichê das instruções if e returns que são todas iguais. Se você quiser manter os return , não use try .

Estamos acostumados a reconhecer imediatamente instruções return (e panic 's) porque é assim que esse tipo de fluxo de controle é expresso em Go (e muitas outras linguagens). Não parece muito improvável que também reconheçamos try como fluxo de controle de mudança depois de nos acostumarmos com isso, assim como fazemos para return . Não tenho dúvidas de que um bom suporte IDE também ajudará nisso.

Tenho duas preocupações:

  • retornos nomeados têm sido muito confusos, e isso os encoraja com um novo e importante caso de uso
  • isso irá desencorajar a adição de contexto aos erros

Na minha experiência, adicionar contexto aos erros imediatamente após cada site de chamada é fundamental para ter um código que possa ser facilmente depurado. E os retornos nomeados causaram confusão para quase todos os desenvolvedores de Go que conheço em algum momento.

Uma preocupação menor e estilística é que é lamentável quantas linhas de código agora serão envolvidas em try(actualThing()) . Eu posso imaginar ver a maioria das linhas em uma base de código envolta em try() . Isso parece lamentável.

Acho que essas preocupações seriam abordadas com um ajuste:

a, b, err := myFunc()
check(err, "calling myFunc on %v and %v", a, b)

check() se comportaria como try() , mas descartaria o comportamento de passar pelos valores de retorno da função genericamente e, em vez disso, forneceria a capacidade de adicionar contexto. Ainda provocaria um retorno.

Isso manteria muitas das vantagens de try() :

  • é um embutido
  • segue o fluxo de controle existente WRT para adiar
  • ele se alinha com a prática existente de adicionar contexto aos erros bem
  • ele se alinha com as propostas e bibliotecas atuais para quebra de erros, como errors.Wrap(err, "context message")
  • resulta em um site de chamadas limpo: não há clichê na linha a, b, err := myFunc()
  • descrever erros com defer fmt.HandleError(&err, "msg") ainda é possível, mas não precisa ser encorajado.
  • a assinatura de check é um pouco mais simples, porque não precisa retornar um número arbitrário de argumentos da função que está envolvendo.

@s4n-gt Obrigado por este link. Eu não estava ciente disso.

@Goodwine Ponto tomado. A razão para não fornecer suporte mais direto ao tratamento de erros é discutida em detalhes no documento de design. Também é um fato que ao longo de um ano ou mais (desde os projetos de projetos publicados na Gophercon do ano passado) nenhuma solução satisfatória para o tratamento explícito de erros surgiu. É por isso que esta proposta deixa isso de fora de propósito (e sugere usar um defer ). Esta proposta ainda deixa a porta aberta para futuras melhorias nesse sentido.

A proposta menciona a mudança de teste de pacote para permitir que testes e benchmarks retornem um erro. Embora não seja “uma modesta mudança de biblioteca”, poderíamos considerar aceitar func main() error também. Isso tornaria a escrita de pequenos scripts muito mais agradável. A semântica seria equivalente a:

func main() {
  if err := newmain(); err != nil {
    println(err.Error())
    os.Exit(1)
  }
}

Uma última crítica. Não é propriamente uma crítica à proposta em si, mas sim uma crítica a uma resposta comum ao contra-argumento "função controladora do fluxo".

A resposta para "Eu não gosto que uma função esteja controlando o fluxo" é que " panic também controla o fluxo do programa!". No entanto, existem algumas razões pelas quais é mais correto panic fazer isso que não se aplicam a try .

  1. panic é amigável para programadores iniciantes porque o que ele faz é intuitivo, ele continua desempacotando a pilha. Não se deve nem ter que pesquisar como panic funciona para entender o que ele faz. Programadores iniciantes nem precisam se preocupar com recover , já que os iniciantes normalmente não estão construindo mecanismos de recuperação de pânico, especialmente porque eles são quase sempre menos favoráveis ​​do que simplesmente evitar o pânico em primeiro lugar.

  2. panic é um nome fácil de ver. Isso traz preocupação, e precisa. Se alguém vir panic em uma base de código, deve estar pensando imediatamente em como _evitar_ o pânico, mesmo que seja trivial.

  3. Pegando carona no último ponto, panic não pode ser aninhado em uma chamada, tornando ainda mais fácil de ver.

Não há problema em que o pânico controle o fluxo do programa porque é extremamente fácil de detectar e é intuitivo quanto ao que faz.

A função try não satisfaz nenhum desses pontos.

  1. Não se pode adivinhar o que try faz sem consultar a documentação. Muitos idiomas usam a palavra-chave de maneiras diferentes, dificultando a compreensão do que isso significaria em Go.

  2. try não me chama a atenção, principalmente quando é uma função. _Especialmente_ quando o realce de sintaxe a destacará como uma função. _ESPECIALMENTE_ depois de desenvolver em uma linguagem como Java, onde try é visto como um clichê desnecessário (por causa das exceções verificadas).

  3. try pode ser usado em um argumento para uma chamada de função, conforme meu exemplo no meu comentário anterior proc := try(os.FindProcess(try(strconv.Atoi(os.Args[1])))) . Isso torna ainda mais difícil de detectar.

Meus olhos ignoram as funções try , mesmo quando estou procurando especificamente por elas. Meus olhos vão vê-los, mas imediatamente saltam para as chamadas os.FindProcess ou strconv.Atoi . try é um retorno condicional. O fluxo de controle E os retornos são mantidos em pedestais em Go. Todo o fluxo de controle dentro de uma função é recuado e todos os retornos começam com return . Misturar esses dois conceitos em uma chamada de função fácil de perder parece um pouco estranho.


Este comentário e meu último são minhas únicas críticas reais à ideia. Acho que não estou gostando dessa proposta, mas ainda acho que é uma vitória geral para Go. Esta solução ainda parece mais Go do que as outras soluções. Se isso fosse adicionado eu ficaria feliz, mas acho que ainda pode ser melhorado, só não sei como.

@buchanae interessante. Conforme escrito, porém, ele move a formatação no estilo fmt de um pacote para a própria linguagem, o que abre uma lata de worms.

Conforme escrito, porém, ele move a formatação no estilo fmt de um pacote para a própria linguagem, o que abre uma lata de worms.

Bom ponto. Um exemplo mais simples:

a, b, err := myFunc()
check(err, "calling myFunc")

@buchanae Consideramos tornar o tratamento de erros explícito mais diretamente conectado a try - consulte o documento de design detalhado, especificamente a seção sobre iterações de design. Sua sugestão específica de check só permitiria aumentar os erros por meio de algo como fmt.Errorf como API (como parte do check ), se entendi corretamente. Em geral, as pessoas podem querer fazer todos os tipos de coisas com erros, não apenas criar um novo que se refira ao original por meio de sua string de erro.

Novamente, esta proposta não tenta resolver todas as situações de tratamento de erros. Suspeito que na maioria dos casos try faça sentido para o código que agora se parece basicamente com isso:

a, b, c, ... err := try(someFunctionCall())
if err != nil {
   return ..., err
}

Há uma enorme quantidade de código que se parece com isso. E nem todo código que se parece com isso precisa de mais tratamento de erros. E onde defer não está certo, ainda se pode usar uma instrução if .

Eu não sigo esta linha:

defer fmt.HandleErrorf(&err, “foobar”)

Ele descarta o erro de entrada no chão, o que é incomum. É para ser usado algo mais parecido com isso?

defer fmt.HandleErrorf(&err, “foobar: %v”, err)

A duplicação de err é um pouco gagueira. Isso não é realmente diretamente a propósito da proposta, apenas um comentário lateral sobre o documento.

Compartilho as duas preocupações levantadas por @buchanae , re: retornos nomeados e erros contextuais.

Acho os retornos nomeados um pouco problemáticos; Eu acho que eles são realmente benéficos apenas como documentação. Inclinar-se sobre eles com mais força é uma preocupação. Desculpe ser tão vago, no entanto. Vou pensar mais sobre isso e fornecer alguns pensamentos mais concretos.

Eu acho que há uma preocupação real de que as pessoas se esforcem para estruturar seu código para que try possa ser usado e, portanto, evite adicionar contexto aos erros. Este é um momento particularmente estranho para introduzir isso, já que agora estamos fornecendo melhores maneiras de adicionar contexto a erros por meio de recursos oficiais de encapsulamento de erros.

Eu acho que try conforme proposto torna algum código significativamente melhor. Aqui está uma função que escolhi mais ou menos aleatoriamente da base de código do meu projeto atual, com alguns dos nomes alterados. Estou particularmente impressionado com a forma como try funciona ao atribuir campos de estrutura. (Isso supondo que minha leitura da proposta esteja correta e que isso funcione?)

O código existente:

func NewThing(thingy *foo.Thingy, db *sql.DB, client pb.Client) (*Thing, error) {
        err := dbfile.RunMigrations(db, dbMigrations)
        if err != nil {
                return nil, err
        }
        t := &Thing{
                thingy: thingy,
        }
        t.scanner, err = newScanner(thingy, db, client)
        if err != nil {
                return nil, err
        }
        t.initOtherThing()
        return t, nil
}

Com try :

func NewThing(thingy *foo.Thingy, db *sql.DB, client pb.Client) (*Thing, error) {
        try(dbfile.RunMigrations(db, dbMigrations))
        t := &Thing{
                thingy:  thingy,
                scanner: try(newScanner(thingy, db, client)),
        }
        t.initOtherThing()
        return t, nil
}

Nenhuma perda de legibilidade, exceto talvez que seja menos óbvio que newScanner pode falhar. Mas então em um mundo com try os programadores Go seriam mais sensíveis à sua presença.

@josharian Sobre main retornando um error : Parece-me que sua pequena função auxiliar é tudo o que é necessário para obter o mesmo efeito. Não tenho certeza se a alteração da assinatura de main é justificada.

Em relação ao exemplo "foobar": É apenas um mau exemplo. Eu provavelmente deveria mudar isso. Obrigado por trazê-lo à tona.

defer fmt.HandleErrorf(&err, “foobar: %v”, err)

Na verdade, isso não pode estar certo, porque err será avaliado muito cedo. Existem algumas maneiras de contornar isso, mas nenhuma delas é tão limpa quanto o HandleErrorf original (acho falho). Eu acho que seria bom ter um exemplo trabalhado mais realista ou dois de uma função auxiliar.

EDIT: este bug de avaliação inicial está presente em um exemplo
perto do final do documento:

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

@adg Sim, try pode ser usado como você está usando em seu exemplo. Deixo seus comentários sobre: ​​os retornos nomeados permanecem como estão.

as pessoas podem querer fazer todos os tipos de coisas com erros, não apenas criar um novo que se refira ao original por meio de sua string de erro.

try não tenta lidar com todos os tipos de coisas que as pessoas querem fazer com erros, apenas aquelas que podemos encontrar uma maneira prática de tornar significativamente mais simples. Eu acredito que meu exemplo check caminha na mesma linha.

Na minha experiência, a forma mais comum de código de tratamento de erros é o código que essencialmente adiciona um rastreamento de pilha, às vezes com contexto adicionado. Descobri que o rastreamento de pilha é muito importante para depuração, onde sigo uma mensagem de erro pelo código.

Mas, talvez outras propostas adicionem rastreamentos de pilha a todos os erros? Perdi o rumo.

No exemplo que @adg deu, há duas falhas em potencial, mas nenhum contexto. Se newScanner e RunMigrations não fornecerem mensagens que indiquem qual delas deu errado, então você fica adivinhando.

No exemplo que @adg deu, há duas falhas em potencial, mas nenhum contexto. Se newScanner e RunMigrations não fornecerem mensagens que indiquem qual deu errado, então você ficará adivinhando.

Isso mesmo, e essa é a escolha de design que fizemos neste trecho de código em particular. Envolvemos muitos erros em outras partes do código.

Compartilho a preocupação como @deanveloper e outros de que isso pode dificultar a depuração. É verdade que podemos optar por não usá-lo, mas os estilos de dependências de terceiros não estão sob nosso controle.
Se if err := ... { return err } menos repetitivo for o ponto principal, me pergunto se um "retorno condicional" seria suficiente, como https://github.com/golang/go/issues/27794 proposto.

        return nil, err if f, err := os.Open(...)
        return nil, err if _, err := os.Write(...)

Eu acho que o ? seria um ajuste melhor do que try , e sempre ter que perseguir o defer por erro também seria complicado.

Isso também fecha as portas para exceções usando try/catch para sempre.

Isso também fecha as portas para exceções usando try/catch para sempre.

Estou _mais_ do que bem com isso.

Concordo com algumas das preocupações levantadas acima sobre adicionar contexto a um erro. Estou lentamente tentando mudar de apenas retornar um erro para sempre decorá-lo com um contexto e depois devolvê-lo. Com esta proposta, terei que mudar completamente minha função para usar parâmetros de retorno nomeados (o que acho estranho porque quase não uso retornos nu).

Como diz @griesemer :

Novamente, esta proposta não tenta resolver todas as situações de tratamento de erros. Suspeito que, na maioria dos casos, tente fazer sentido para o código que agora se parece basicamente com isso:
a, b, c, ... err := try(someFunctionCall())
se errar != nil {
voltar..., errar
}
Há uma enorme quantidade de código que se parece com isso. E nem todo código que se parece com isso precisa de mais tratamento de erros. E onde defer não está certo, ainda pode-se usar uma instrução if.

Sim, mas não deve ser bom, o código idiomático sempre envolve/decora seus erros? Acredito que é por isso que estamos introduzindo mecanismos refinados de tratamento de erros para adicionar erros de contexto/empacotamento em stdlib. Pelo que vejo, esta proposta parece considerar apenas o caso de uso mais básico.

Além disso, esta proposta aborda apenas o caso de encapsular/decorar vários sites de retorno de erros possíveis em um _único local_, usando parâmetros nomeados com uma chamada defer.

Mas não faz nada para o caso em que é necessário adicionar contextos diferentes a erros diferentes em uma única função. Por exemplo, é muito essencial decorar os erros do banco de dados para obter mais informações sobre de onde eles estão vindo (supondo que não haja rastreamentos de pilha)

Este é um exemplo de um código real que tenho -

func (p *pgStore) DoWork() error {
    tx, err := p.handle.Begin()
    if err != nil {
        return err
    }
    var res int64
    err = tx.QueryRow(`INSERT INTO table (...) RETURNING c1`, ...).Scan(&res)
    if err != nil {
        tx.Rollback()
        return fmt.Errorf("insert table: %w", err)
    }

    _, err = tx.Exec(`INSERT INTO table2 (...) VALUES ($1)`, res)
    if err != nil {
        tx.Rollback()
        return fmt.Errorf("insert table2: %w", err)
    }
    return tx.Commit()
}

De acordo com a proposta:

Se o aumento ou encapsulamento de erros for desejado, há duas abordagens: Fique com a instrução if testada e comprovada ou, alternativamente, “declare” um manipulador de erros com uma instrução defer:

Acho que isso se enquadrará na categoria de "manter a instrução if testada e comprovada". Espero que a proposta possa ser melhorada para resolver isso também.

Eu sugiro fortemente que a equipe do Go priorize os genéricos , pois é onde o Go ouve mais críticas e espera o tratamento de erros. A técnica de hoje não é tão dolorosa (embora go fmt devesse deixá-la em uma linha).

O conceito try() tem todos os problemas de check de check/handle:

  1. Não se lê como Go. As pessoas querem sintaxe de atribuição, sem o teste nil subsequente, pois isso se parece com Go. Treze respostas separadas para checar/manusear sugeriram isso; veja _Temas recorrentes_ aqui:
    https://github.com/golang/go/wiki/Go2ErrorHandlingFeedback#recurring -themes

    f, #      := os.Open(...) // return on error
    f, #panic := os.Open(...) // panic on error
    f, #hname := os.Open(...) // invoke named handler on error
    // # is any available symbol or unambiguous pair
    
  2. O aninhamento de chamadas de função que retornam erros obscurece a ordem das operações e dificulta a depuração. O estado das coisas quando ocorre um erro e, portanto, a sequência de chamadas devem ser claros, mas aqui não é:
    try(step4(try(step1()), try(step3(try(step2())))))
    Agora lembre-se de que a linguagem proíbe:
    f(t ? a : b) e f(a++)

  3. Seria trivial retornar erros sem contexto. Um dos principais fundamentos da verificação/manuseio foi encorajar a contextualização.

  4. Está vinculado ao tipo error e ao último valor de retorno. Se precisarmos inspecionar outros valores/tipos de retorno para estado excepcional, voltaremos a: if errno := f(); errno != 0 { ... }

  5. Não oferece vários caminhos. O código que chama APIs de armazenamento ou rede trata esses erros de maneira diferente daqueles devidos a entrada incorreta ou estado interno inesperado. Meu código faz um desses com muito mais frequência do que return err :

    • log.Fatal()
    • panic() para erros que nunca deveriam surgir
    • registre uma mensagem e tente novamente

@gopherbot adicionar Go2, LanguageChange

Que tal usar apenas ? para desembrulhar o resultado como rust

A razão pela qual somos céticos sobre chamar try() pode ser duas ligações implícitas. Não podemos ver a ligação para o erro do valor de retorno e os argumentos para try(). Para sobre try(), podemos fazer uma regra que devemos usar try() com função de argumento que tem erro nos valores de retorno. Mas a vinculação a valores de retorno não é. Então, estou pensando que mais expressão é necessária para que os usuários entendam o que esse código está fazendo.

func doSomething() (int, %error) {
  f := try(foo())
  ...
}
  • Não podemos usar try() se doSomething não tiver %error em valores de retorno.
  • Não podemos usar try() se foo() não tiver erro no último dos valores de retorno.

É difícil adicionar novos requisitos/recursos à sintaxe existente.

Para ser honesto, acho que foo() também deveria ter %error.

Adicionar mais 1 regra

  • %error pode ser apenas um na lista de valores de retorno de uma função.

No documento de design detalhado, notei que em uma iteração anterior foi sugerido passar um manipulador de erros para a função try incorporada. Assim:

handler := func(err error) error {
        return fmt.Errorf("foo failed: %v", err)  // wrap error
}

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

ou melhor ainda, assim:

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

Embora, como o documento afirma, isso levanta várias questões, acho que esta proposta seria muito mais desejável e útil se mantivesse essa possibilidade de especificar opcionalmente essa função ou fechamento de manipulador de erros.

Em segundo lugar, não me importo que um built-in possa fazer com que a função retorne, mas, para diminuir um pouco, o nome 'try' é muito curto para sugerir que pode causar um retorno. Então um nome mais longo, como attempt parece melhor para mim.

EDIT: Em terceiro lugar, idealmente, a linguagem go deve ganhar os genéricos primeiro, onde um caso de uso importante seria a capacidade de implementar essa função try como um genérico, para que o bikeshedding possa terminar e todos possam obter o tratamento de erros que preferirem.

As notícias de hackers têm algum ponto: try não se comporta como uma função normal (pode retornar), então não é bom fornecer uma sintaxe semelhante a uma função. Uma sintaxe return ou defer seria mais apropriada:

func CopyFile(src, dst string) (err error) {
        r := try os.Open(src)
        defer r.Close()

        w := try os.Create(dst)
        defer func() {
                w.Close()
                if err != nil {
                        os.Remove(dst) // only if a “try” fails
                }
        }()

        try io.Copy(w, r)
        try w.Close()
        return nil
}

@sheerun o contra-argumento comum para isso é que panic também é uma função interna que altera o fluxo de controle. Eu pessoalmente discordo disso, mas está correto.

  1. Ecoando @deanveloper acima , bem como comentários semelhantes de outras pessoas, temo que estejamos subestimando os custos de adicionar uma palavra-chave nova, um tanto sutil e - especialmente quando incorporada em outras chamadas de função - facilmente ignorada que gerencia o controle da pilha de chamadas fluxo. panic(...) é uma exceção relativamente clara (trocadilho não intencional) à regra de que return é a única saída para uma função. Eu não acho que devemos usar sua existência como justificativa para adicionar um terceiro.
  2. Essa proposta canonizaria o retorno de um erro não encapsulado como o comportamento padrão e relegaria os erros de encapsulamento como algo que você precisa aceitar, com cerimônia adicional. Mas, na minha experiência, isso é precisamente o contrário das boas práticas. Espero que uma proposta neste espaço torne mais fácil, ou pelo menos não mais difícil, adicionar informações contextuais aos erros no local do erro.

talvez possamos adicionar uma variante com função de aumento opcional algo como tryf com esta semântica:

func tryf(t1 T1, t1 T2, … tn Tn, te error, fn func(error) error) (T1, T2, … Tn)

traduz isso

x1, x2, … xn = tryf(f(), func(err error) { return fmt.Errorf("foobar: %q", err) })

nisso

t1, … tn, te := f()
if te != nil {
    if fn != nil {
        te = fn(te)
    }
    err = te
    return
}

uma vez que esta é uma escolha explícita (em vez de usar try ), podemos encontrar respostas razoáveis ​​às perguntas na versão anterior deste design. por exemplo, se a função de aumento for nula, não faça nada e apenas retorne o erro original.

Estou preocupado que try suplantará o tratamento de erros tradicional e, como resultado, tornará a anotação de caminhos de erro mais difícil.

O código que lida com erros registrando mensagens e atualizando contadores de telemetria será considerado defeituoso ou impróprio por linters e desenvolvedores que esperam try tudo.

a, b, err := doWork()
if err != nil {
  updateCounters()
  writeLogs()
  return err
}

Go é uma linguagem extremamente social com expressões idiomáticas comuns impostas por ferramentas (fmt, lint, etc). Por favor, mantenha as ramificações sociais desta ideia em mente - haverá uma tendência de querer usá-la em todos os lugares.

@politician , desculpe, mas a palavra que você está procurando não é _social_, mas _opinionated_. Go é uma linguagem de programação opinativa. De resto, concordo em grande parte com o que você quer dizer.

Ferramentas da comunidade @beoran como Godep e os vários linters demonstram que Go é tanto opinativo quanto social, e muitos dos dramas com a linguagem derivam dessa combinação. Felizmente, nós dois podemos concordar que try não deve ser o próximo drama.

@politician Obrigado por esclarecer, eu não tinha entendido dessa maneira. Posso certamente concordar que devemos tentar evitar o drama.

Estou confuso sobre isso.

Do blog: Erros são valores , na minha perspectiva, foi feito para ser valorizado e não para ser ignorado.

E eu acredito no que Rop Pike disse: "Valores podem ser programados, e como erros são valores, erros podem ser programados".

Não devemos considerar error como exception , é como importar complexidade não apenas para pensar, mas também para codificar se fizermos isso.

"Use a linguagem para simplificar o tratamento de erros." -- Rob Pike

E mais, podemos rever este slide

image

Uma situação em que acho a verificação de erros via if particularmente estranha é ao fechar arquivos (por exemplo, em NFS). Eu acho que, atualmente, devemos escrever o seguinte, se os retornos de erro de .Close() forem possíveis?

r, err := os.Open(src)
if err != nil {
    return err
}
defer func() {
    // maybe check whether a previous error occured?
    return r.Close()
}()

defer try(r.Close()) poderia ser uma boa maneira de ter uma sintaxe gerenciável para lidar com esses erros? Pelo menos, faria sentido ajustar o exemplo CopyFile() na proposta de alguma forma, para não ignorar erros de r.Close() e w.Close() .

@seehuhn Seu exemplo não será compilado porque a função adiada não possui um tipo de retorno.

func doWork() (err error) {
  r, err := os.Open(src)
  if err != nil {
    return err
  }
  defer func() {
    err = r.Close()  // overwrite the return value
  }()
}

Vai funcionar como você espera. A chave é o valor de retorno nomeado.

Eu gosto da proposta, mas acho que o exemplo de @seehuhn deve ser abordado também:

defer try(w.Close())

retornaria o erro de Close() somente se o erro ainda não estivesse definido.
Esse padrão é usado com tanta frequência...

Concordo com as preocupações sobre adicionar contexto aos erros. Eu vejo isso como uma das melhores práticas que mantém as mensagens de erro muito amigáveis ​​(e claras) e facilita o processo de depuração.

A primeira coisa que pensei foi substituir o fmt.HandleErrorf por uma função tryf , que prefixa o erro com contexto adicional.

func tryf(t1 T1, t1 T2, … tn Tn, te error, ts string) (T1, T2, … Tn)

Por exemplo (de um código real que tenho):

func (c *Config) Build() error {
    pkgPath, err := c.load()
    if err != nil {
        return nil, errors.WithMessage(err, "load config dir")
    }
    b := bytes.NewBuffer(nil)
    if err = templates.ExecuteTemplate(b, "main", c); err != nil {
        return nil, errors.WithMessage(err, "execute main template")
    }
    buf, err := format.Source(b.Bytes())
    if err != nil {
        return nil, errors.WithMessage(err, "format main template")
    }
    target := fmt.Sprintf("%s.go", filename(pkgPath))
    if err := ioutil.WriteFile(target, buf, 0644); err != nil {
        return nil, errors.WithMessagef(err, "write file %s", target)
    }
    // ...
}

Pode ser alterado para algo como:

func (c *Config) Build() error {
    pkgPath := tryf(c.load(), "load config dir")
    b := bytes.NewBuffer(nil)
    tryf(emplates.ExecuteTemplate(b, "main", c), "execute main template")
    buf := tryf(format.Source(b.Bytes()), "format main template")
    target := fmt.Sprintf("%s.go", filename(pkgPath))
    tryf(ioutil.WriteFile(target, buf, 0644), fmt.Sprintf("write file %s", target))
    // ...
}

Ou, se eu pegar o exemplo de @agnivade :

func (p *pgStore) DoWork() (err error) {
    tx := tryf(p.handle.Begin(), "begin transaction")
        defer func() {
        if err != nil {
            tx.Rollback()
        }
    }()
    var res int64
    tryf(tx.QueryRow(`INSERT INTO table (...) RETURNING c1`, ...).Scan(&res), "insert table")
    _, = tryf(tx.Exec(`INSERT INTO table2 (...) VALUES ($1)`, res), "insert table2")
    return tryf(tx.Commit(), "commit transaction")
}

No entanto, @josharian levantou um bom ponto que me faz hesitar nesta solução:

Conforme escrito, porém, ele move a formatação no estilo fmt de um pacote para a própria linguagem, o que abre uma lata de worms.

Estou totalmente de acordo com esta proposta e posso ver seus benefícios em vários exemplos.

Minha única preocupação com a proposta é a nomenclatura de try , sinto que suas conotações com outras linguagens, podem distorcer a percepção dos desenvolvedores de qual é o seu propósito ao vir de outras linguagens. Java vem encontrar aqui.

Para mim, eu preferiria que o builtin fosse chamado pass . Eu sinto que isso dá uma melhor representação do que está acontecendo. Afinal, você não está lidando com o erro - em vez disso, devolvendo-o para ser tratado pelo chamador. try dá a impressão de que o erro foi tratado.

É um polegar para baixo de mim, principalmente porque o problema que ele visa resolver ("as instruções if clichê normalmente associadas ao tratamento de erros") simplesmente não é um problema para mim. Se todas as verificações de erro fossem simplesmente if err != nil { return err } , então eu poderia ver algum valor em adicionar açúcar sintático para isso (embora Go seja uma linguagem relativamente livre de açúcar por inclinação).

Na verdade, o que eu quero fazer no caso de um erro não nulo varia consideravelmente de uma situação para outra. Talvez eu queira t.Fatal(err) . Talvez eu queira adicionar uma mensagem de decoração return fmt.Sprintf("oh no: %v", err) . Talvez eu apenas registre o erro e continue. Talvez eu defina um sinalizador de erro no meu objeto SafeWriter e continue, verificando o sinalizador no final de alguma sequência de operações. Talvez eu precise tomar algumas outras ações. Nenhum deles pode ser automatizado com try . Portanto, se o argumento para try for que ele eliminará todos os blocos if err != nil , esse argumento não se sustenta.

Será que vai eliminar _some_ deles? Certo. Essa é uma proposta atraente para mim? Meh. Eu realmente não estou preocupado. Para mim, if err != nil é apenas parte do Go, como as chaves, ou defer . Eu entendo que parece verboso e repetitivo para as pessoas que são novas em Go, mas as pessoas que são novas em Go não estão em melhor posição para fazer mudanças dramáticas no idioma, por vários motivos.

A barreira para mudanças significativas no Go tem sido tradicionalmente que a mudança proposta deve resolver um problema que é (A) significativo, (B) afeta muitas pessoas e (C) é bem resolvido pela proposta. Não estou convencido de nenhum desses três critérios. Estou muito feliz com o tratamento de erros do Go.

Para repetir @peterbourgon e @deanveloper , uma das minhas coisas favoritas sobre Go é que o fluxo de código é claro e panic() não é tratado como um mecanismo de controle de fluxo padrão da maneira que é em Python.

Em relação ao debate sobre o pânico, panic() quase sempre aparece sozinho em uma linha porque não tem valor. Você não pode fmt.Println(panic("oops")) . Isso aumenta sua visibilidade tremendamente e o torna muito menos comparável a try() do que as pessoas estão imaginando.

Se houver outra construção de controle de fluxo para funções, eu preferiria _muito_ que fosse uma instrução garantida para ser o item mais à esquerda em uma linha.

Um dos exemplos na proposta resolve o problema para mim:

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

O fluxo de controle realmente se torna menos óbvio e muito obscurecido.

Isso também vai contra a intenção inicial de Rob Pike de que todos os erros precisam ser tratados explicitamente.

Embora uma reação a isso possa ser "então não o use", o problema é -- outras bibliotecas o usarão, e depurá-los, lê-los e usá-los se torna mais problemático. Isso motivará minha empresa a nunca adotar o go 2 e começar a usar apenas bibliotecas que não usam try . Se eu não estiver sozinho com isso, pode levar a uma divisão a-la python 2/3.

Além disso, a nomeação de try implicará automaticamente que eventualmente catch aparecerá na sintaxe e voltaremos a ser Java.

Então, por tudo isso, sou _fortemente_ contra essa proposta.

Eu não gosto do nome try . Isso implica uma _tentativa_ de fazer algo com alto risco de falha (posso ter um viés cultural contra _tentar_ já que não sou um falante nativo de inglês), enquanto try seria usado no caso de esperarmos falhas raras (motivação para querer reduzir a verbosidade do tratamento de erros) e são otimistas. Além disso try nesta proposta de fato _captura_ um erro ao devolvê-lo antecipadamente. Gostei da sugestão pass do @HiImJC.

Além do nome, acho estranho ter return -como a declaração agora escondida no meio das expressões. Isso quebra o estilo de fluxo Go. Isso tornará as revisões de código mais difíceis.

Em geral, acho que esta proposta só beneficiará o programador preguiçoso que agora tem uma arma para código mais curto e ainda menos motivos para fazer o esforço de encapsular erros. Como também dificultará as revisões (retorno no meio da expressão), acho que essa proposta vai contra o objetivo de "programação em escala" do Go.

Uma das minhas coisas favoritas sobre Go que geralmente digo ao descrever a linguagem é que só há uma maneira de fazer as coisas, para a maioria das coisas. Esta proposta vai um pouco contra esse princípio ao oferecer várias maneiras de fazer a mesma coisa. Pessoalmente, acho que isso não é necessário e que tiraria, em vez de aumentar a simplicidade e a legibilidade da linguagem.

Eu gosto desta proposta em geral. A interação com defer parece suficiente para fornecer uma maneira ergonômica de retornar um erro ao mesmo tempo em que adiciona contexto adicional. Embora seja bom resolver o problema que @josharian apontou sobre como incluir o erro original na mensagem de erro encapsulada.

O que está faltando é uma forma ergonômica de interagir com a(s) proposta(s) de inspeção de erros na mesa. Acredito que as APIs devem ser muito deliberadas em quais tipos de erros eles retornam, e o padrão provavelmente deve ser "os erros retornados não são inspecionáveis ​​de forma alguma". Deve então ser fácil ir para um estado em que os erros sejam inspecionáveis ​​de maneira precisa, conforme documentado pela assinatura da função ("Relata um erro do tipo X na circunstância A e um erro do tipo Y na circunstância B").

Infelizmente, a partir de agora, esta proposta torna a opção mais ergonômica a mais indesejável (para mim); passando cegamente por tipos de erros arbitrários. Acho que isso é indesejável porque incentiva a não pensar nos tipos de erros que você retorna e como os usuários de sua API os consumirão. A conveniência adicional desta proposta é certamente boa, mas temo que encoraje o mau comportamento porque a conveniência percebida superará o valor percebido de pensar cuidadosamente sobre quais informações de erro você fornece (ou vaza).

Um bandaid seria se os erros retornados por try fossem convertidos em erros que não fossem "desempacotados". Infelizmente, isso também tem desvantagens bastante graves, pois faz com que qualquer defer não possa inspecionar os erros em si. Além disso, evita o uso em que try realmente retornará um erro de um tipo desejável (ou seja, casos de uso em que try é usado com cuidado e não descuidadamente).

Outra solução seria redirecionar a ideia (descartada) de ter um segundo argumento opcional para try para definir/lista branca do(s) tipo(s) de erro que podem ser retornados desse site. Isso é um pouco problemático porque temos duas maneiras diferentes de definir um "tipo de erro", seja por valor ( io.EOF etc) ou por tipo ( *os.PathError , *exec.ExitError ). É fácil especificar tipos de erro que são valores como argumentos para uma função, mas é mais difícil especificar tipos. Não tenho certeza de como lidar com isso, mas jogando a ideia lá fora.

O problema que @josharian apontou pode ser evitado atrasando a avaliação de err:

defer func() { fmt.HandleErrorf(&err, "oops: %v", err) }()

Não parece ótimo, mas deve funcionar. Eu preferiria, no entanto, que isso pudesse ser resolvido adicionando um novo verbo/sinalizador de formatação para ponteiros de erro, ou talvez para ponteiros em geral, que imprima o valor desreferenciado como com %v . Para o propósito do exemplo, vamos chamá-lo de %*v :

defer fmt.HandleErrorf(&err, "oops: %*v", &err)

O problema à parte, acho que esta proposta parece promissora, mas parece crucial manter a ergonomia de adicionar contexto aos erros sob controle.

Editar:

Outra abordagem é envolver o ponteiro de erro em uma estrutura que implementa Stringer :

type wraperr struct{ err *error }
func (w wraperr) String() string { return (*w.err).Error() }

...

defer handleErrorf(&err, "oops: %v", wraperr{&err})

Algumas coisas da minha perspectiva. Por que estamos tão preocupados em salvar algumas linhas de código? Eu considero isso na mesma linha que as pequenas funções consideradas prejudiciais .

Além disso, acho que tal proposta removeria a responsabilidade de lidar corretamente com o erro para alguma "mágica" que eu me preocupo que seja abusada e encorajaria a preguiça, resultando em código de baixa qualidade e bugs.

A proposta, conforme declarado, também tem vários comportamentos pouco claros, então isso já é problemático do que um _explícito_ extra ~ 3 linhas que são mais claras.

Atualmente, usamos o padrão de diferimento com moderação em casa. Há um artigo aqui que teve uma recepção igualmente mista quando o escrevemos - https://bet365techblog.com/better-error-handling-in-go

No entanto, nosso uso foi em antecipação ao progresso da proposta check / handle .

Check/handle era uma abordagem muito mais abrangente para tornar o tratamento de erros mais conciso. Seu bloco handle manteve o mesmo escopo de função daquele em que foi definido, enquanto quaisquer instruções defer são novos contextos com uma quantidade, por mais que seja, de sobrecarga. Isso parecia estar mais de acordo com as expressões idiomáticas de go, pois se você quisesse o comportamento de "apenas retorne o erro quando acontecer", você poderia declarar isso explicitamente como handle { return err } .

Defer obviamente depende da manutenção da referência de erro também, mas vimos problemas surgirem ao sombrear a referência de erro com vars com escopo de bloco. Portanto, não é infalível o suficiente para ser considerado a maneira padrão de lidar com erros em andamento.

try , neste caso, não parece resolver muito e eu compartilho do mesmo medo que outros de que isso simplesmente levaria a implementações preguiçosas, ou aquelas que usam excessivamente o padrão defer.

Se o tratamento de erros baseado em adiar for A Thing, algo assim provavelmente deve ser adicionado ao pacote de erros:

        f := try(os.Create(filename))
        defer errors.Deferred(&err, f.Close)

Ignorar os erros de instruções Close adiadas é um problema bastante comum. Deve haver uma ferramenta padrão para ajudar com isso.

Uma função interna que retorna é mais difícil de vender do que uma palavra-chave que faz o mesmo.
Eu gostaria mais se fosse uma palavra-chave como está em Zig[1].

  1. https://ziglang.org/documentation/master/#try

Funções embutidas, cuja assinatura de tipo não pode ser expressa usando o sistema de tipos da linguagem e cujo comportamento confunde o que uma função normalmente é, parece apenas uma escotilha de escape que pode ser usada repetidamente para evitar a evolução real da linguagem.

Estamos acostumados a reconhecer imediatamente declarações de retorno (e pânico) porque é assim que esse tipo de fluxo de controle é expresso em Go (e muitas outras linguagens). Não parece muito improvável que também reconheçamos try como alterar o fluxo de controle depois de nos acostumarmos a ele, assim como fazemos para return. Não tenho dúvidas de que um bom suporte IDE também ajudará nisso.

Eu acho que é bastante rebuscado. No código gofmt'ed, um retorno sempre corresponde /^\t*return / – é um padrão muito trivial para detectar a olho nu, sem qualquer assistência. try , por outro lado, pode ocorrer em qualquer lugar no código, aninhado arbitrariamente nas chamadas de função. Nenhuma quantidade de treinamento nos tornará capazes de identificar imediatamente todo o fluxo de controle em uma função sem a ajuda da ferramenta.

Além disso, um recurso que depende de "bom suporte a IDE" estará em desvantagem em todos os ambientes onde não houver um bom suporte a IDE. As ferramentas de revisão de código vêm à mente imediatamente – Gerrit destacará todas as tentativas para mim? E as pessoas que optam por não usar IDEs, ou realce de código sofisticado, por vários motivos? A acme começará a destacar try ?

Um recurso de linguagem deve ser fácil de entender por si só, não depender do suporte do editor.

@kungfusheep Eu gosto desse artigo. Cuidar de embrulhar em um defer sozinho já aumenta bastante a legibilidade sem try .

Estou no campo que não acha que erros no Go são realmente um problema. Mesmo assim, if err != nil { return err } pode ser bastante gaguejante em algumas funções. Eu escrevi funções que precisavam de uma verificação de erro após quase todas as instruções e nenhuma precisava de nenhum tratamento especial além de wrap e return. Às vezes, simplesmente não há nenhuma estrutura de buffer inteligente que torne as coisas mais agradáveis. Às vezes, é apenas um passo crítico diferente após o outro e você precisa simplesmente dar um curto-circuito se algo der errado.

Embora try certamente tornaria esse código muito mais fácil de ler, sendo totalmente compatível com versões anteriores, concordo que try não é um recurso essencial obrigatório, portanto, se as pessoas tiverem muito medo de talvez seja melhor não tê-lo.

A semântica é bastante clara embora. Sempre que você vê try ou está seguindo o caminho feliz, ou retorna. Eu realmente não posso ficar mais simples do que isso.

Isso se parece com uma macro em maiúsculas especial.

@dominikh try sempre corresponde /try\(/ então não sei qual é realmente o seu ponto. É igualmente pesquisável e todos os editores de que já ouvi falar têm um recurso de pesquisa.

@qrpnxz Acho que o ponto que ele estava tentando fazer não é que você não possa procurá-lo programaticamente, mas que é mais difícil procurar com os olhos. O regexp era apenas uma analogia, com ênfase no /^\t* , significando que todos os retornos se destacam claramente por estarem no início de uma linha (ignorando os espaços em branco iniciais).

Pensando mais sobre isso, deve haver algumas funções auxiliares comuns. Talvez eles devam estar em um pacote chamado "adiado".

Abordando a proposta de um check com formato para evitar nomear o retorno, você pode fazer isso apenas com uma função que verifica nil, assim

func Format(err error, message string, args ...interface{}) error {
    if err == nil {
        return nil
    }
    return fmt.Errorf(...)
}

Isso pode ser usado sem um retorno nomeado assim:

func foo(s string) (int, error) {
    n, err := strconv.Atoi(s)
    try(deferred.Format(err, "bad string %q", s))
    return n, nil
}

O fmt.HandleError proposto poderia ser colocado no pacote adiado e minha função auxiliar errors.Defer poderia ser chamada deferred.Exec e poderia haver um exec condicional para os procedimentos serem executados somente se o erro não for nulo.

Juntando, você obtém algo como

func CopyFile(src, dst string) (err error) {
    defer deferred.Annotate(&err, "copy %s %s", src, dst)

    r := try(os.Open(src))
    defer deferred.Exec(&err, r.Close)

    w := try(os.Create(dst))
    defer deferred.Exec(&err, r.Close)

    defer deferred.Cond(&err, func(){ os.Remove(dst) })
    try(io.Copy(w, r))

    return nil
}

Outro exemplo:

func (p *pgStore) DoWork() (err error) {
    tx := try(p.handle.Begin())

    defer deferred.Cond(&err, func(){ tx.Rollback() })

    var res int64 
    err = tx.QueryRow(`INSERT INTO table (...) RETURNING c1`, ...).Scan(&res)
    try(deferred.Format(err, "insert table")

    _, err = tx.Exec(`INSERT INTO table2 (...) VALUES ($1)`, res)
    try(deferred.Format(err, "insert table2"))

    return tx.Commit()
}

Esta proposta nos leva de ter if err != nil em todos os lugares, para ter try em todos os lugares. Desloca o problema proposto e não o resolve.

Embora, eu argumentaria que o atual mecanismo de tratamento de erros não é um problema para começar. Só precisamos melhorar as ferramentas e a verificação em torno disso.

Além disso, eu argumentaria que if err != nil é realmente mais legível do que try porque não sobrecarrega a linha da linguagem de lógica de negócios, mas fica logo abaixo dela:

file := try(os.OpenFile("thing")) // less readable than, 

file, err := os.OpenFile("thing")
if err != nil {

}

E se Go era para ser mais mágico em seu tratamento de erros, por que não apenas possuí-lo totalmente. Por exemplo, Go pode chamar implicitamente o try interno se um usuário não atribuir um erro. Por exemplo:

func getString() (string, error) { ... }

func caller() {
  defer func() {
    if err != nil { ... } // whether `err` must be defined or not is not shown in this example. 
  }

  // would call try internally, because a user is not 
  // assigning an error value. Also, it can add a compile error
  // for "defined and not used err value" if the user does not 
  // handle the error. 
  str := getString()
}

Para mim, isso realmente resolveria o problema de redundância ao custo de mágica e legibilidade potencial.

Portanto, proponho que realmente resolvamos o 'problema' como no exemplo acima ou mantemos o tratamento de erros atual, mas em vez de alterar o idioma para resolver redundância e encapsulamento, não alteramos o idioma, mas melhoramos as ferramentas e a verificação de código para tornar a experiência melhor.

Por exemplo, no VSCode há um trecho chamado iferr se você digitá-lo e pressionar enter, ele se expande para uma declaração completa de tratamento de erros ... portanto, escrever nunca parece cansativo para mim e ler mais tarde é melhor .

@josharian

Embora não seja “uma mudança modesta na biblioteca”, poderíamos considerar aceitar o erro func main() também.

O problema é que nem todas as plataformas têm uma semântica clara sobre o que isso significa. Sua reescrita funciona bem em programas Go "tradicionais" executados em um sistema operacional completo - mas assim que você escreve o firmware do microcontrolador ou mesmo apenas o WebAssembly, não fica muito claro o que os.Exit(1) significaria. Atualmente, os.Exit é uma chamada de biblioteca, então as implementações Go são gratuitas apenas para não fornecê-la. A forma de main é uma preocupação de linguagem.


Uma pergunta sobre a proposta que provavelmente é melhor respondida por "não": Como try interage com argumentos variáveis? É o primeiro caso de uma função variadic (ish) que não tem seu variadic-nes no último argumento. Isso é permitido:

var e []error
try(e...)

Deixando de lado por que você faria isso. Suspeito que a resposta seja "não" (caso contrário, o acompanhamento é "e se o comprimento da fatia expandida for 0). Apenas trazendo isso à tona para que possa ser lembrado ao formular a especificação eventualmente.

  • Vários dos maiores recursos em go são que os internos atuais garantem um fluxo de controle claro, o tratamento de erros é explícito e incentivado e os desenvolvedores são fortemente dissuadidos de escrever código "mágico". A proposta try não é consistente com esses princípios básicos, pois promoverá taquigrafia ao custo da legibilidade do fluxo de controle.
  • Se esta proposta for adotada, talvez considere tornar o try embutido em uma instrução em vez de uma função . Então é mais consistente com outras instruções de fluxo de controle como if . Além disso, a remoção dos parênteses aninhados melhora marginalmente a legibilidade.
  • Novamente, se a proposta for adotada, talvez a implemente sem usar defer ou similar. Ele já não pode ser implementado em puro go (como apontado por outros), então também pode usar uma implementação mais eficiente sob o capô.

Vejo dois problemas nisso:

  1. Ele coloca MUITO código aninhado dentro de funções. Isso adiciona muita carga cognitiva extra, tentando analisar o código em sua cabeça.
  1. Ele nos dá lugares onde o código pode sair do meio de uma instrução.

Número 2 eu acho que é muito pior. Todos os exemplos aqui são chamadas simples que retornam um erro, mas o que é muito mais insidioso é isso:

func doit(abc string) error {
    a := fmt.Sprintf("value of something: %s\n", try(getValue(abc)))
    log.Println(a)
    return nil
}

Esse código pode sair no meio desse sprintf, e será SUPER fácil perder esse fato.

Meu voto é não. Isso não tornará o código Go melhor. Não vai facilitar a leitura. Não vai torná-lo mais robusto.

Eu já disse isso antes, e esta proposta exemplifica isso - eu sinto que 90% das reclamações sobre Go são "Eu não quero escrever uma instrução if ou um loop" . Isso remove algumas instruções if muito simples, mas adiciona carga cognitiva e facilita a perda de pontos de saída para uma função.

Eu só quero salientar que você não pode usar isso no main e pode ser confuso para novos usuários ou ao ensinar. Obviamente isso se aplica a qualquer função que não retorne um erro, mas acho que main é especial, pois aparece em muitos exemplos.

func main() {
    f := try(os.Open("foo.txt"))
    defer f.Close()
}

Não tenho certeza se tentar pânico no main também seria aceitável.

Além disso, não seria particularmente útil em testes ( func TestFoo(t* testing.T) ) o que é lamentável :(

O problema que tenho com isso é que você sempre deseja retornar o erro quando isso acontecer. Quando talvez você queira adicionar contexto ao erro e devolvê-lo ou talvez você apenas queira se comportar de maneira diferente quando um erro acontecer. Talvez isso dependa do tipo de erro retornado.

Eu preferiria algo parecido com um try/catch que pode parecer

Assumindo foo() definido como

func foo() (int, error) {}

Você poderia então fazer

n := try(foo()) {
    case FirstError:
        // do something based on FirstError
    case OtherError:
        // do something based on OtherError
    default:
        // default behavior for any other error
}

O que se traduz em

n, err := foo()
if errors.Is(err, FirstError) {
    // do something based on FirstError
if errors.Is(err, OtherError) {
    // do something based on OtherError
} else {
    // default behavior for any other error
}

Para mim, o tratamento de erros é uma das partes mais importantes de uma base de código.
Já muito código go é if err != nil { return err } , retornando um erro do fundo da pilha sem adicionar contexto extra, ou até (possivelmente) pior adicionando contexto mascarando o erro subjacente com quebra fmt.Errorf .

Fornecer uma nova palavra-chave que é uma espécie de mágica que não faz nada além de substituir if err != nil { return err } parece um caminho perigoso a seguir.
Agora todo o código será apenas envolvido em uma chamada para tentar. Isso é um pouco bom (embora a legibilidade seja uma droga) para código que está lidando apenas com erros no pacote, como:

func foo() error {
  /// stuff
  try(bar())
  // more stuff
}

Mas eu diria que o exemplo dado é realmente horrível e basicamente deixa o chamador tentando entender um erro que está muito profundo na pilha, bem como o tratamento de exceções.
Claro, tudo isso depende do desenvolvedor fazer a coisa certa aqui, mas dá ao desenvolvedor uma ótima maneira de não se preocupar com seus erros com talvez um "vamos corrigir isso mais tarde" (e todos nós sabemos como isso acontece ).

Eu gostaria que olhássemos para o problema de uma perspectiva diferente de *"como podemos reduzir a repetição" e mais sobre "como podemos tornar o tratamento de erros (adequado) mais simples e os desenvolvedores mais produtivos".
Devemos estar pensando em como isso afetará a execução do código de produção.

*Nota: Na verdade, isso não reduz a repetição, apenas altera o que está sendo repetido, ao mesmo tempo em que torna o código menos legível porque tudo está dentro de um try() .

Um último ponto: Ler a proposta no começo parece legal, depois você começa a se meter em todas as pegadinhas (pelo menos as listadas) e é tipo “ok sim, isso é demais”.


Percebo que muito disso é subjetivo, mas é algo com o qual me importo. Essas semânticas são incrivelmente importantes.
O que eu quero ver é uma maneira de tornar a escrita e a manutenção do código de nível de produção mais simples, de modo que você também possa fazer erros "certos" mesmo para código de nível POC/demo.

Como o contexto de erro parece ser um tema recorrente...

Hipótese: a maioria das funções Go retorna (T, error) em oposição a (T1, T2, T3, error)

E se, ao invés de definir try como try(T1, T2, T3, error) (T1, T2, T3) nós o definissemos como
try(func (args) (T1, T2, T3, error))(T1, T2, T3) ? (isso é uma aproximação)

o que quer dizer que a estrutura sintática de uma chamada try é sempre um primeiro argumento que é uma expressão que retorna vários valores, sendo o último um erro.

Então, assim como make , isso abre a porta para uma forma de 2 argumentos da chamada, onde o segundo argumento é o contexto da tentativa (por exemplo, uma string fixa, uma string com %v , uma função que recebe um argumento de erro e retorna outro erro etc.)

Isso ainda permite o encadeamento para o caso (T, error) , mas você não pode mais encadear vários retornos que o IMO normalmente não é necessário.

@ cpuguy83 Se você ler a proposta, verá que não há nada impedindo que você envolva o erro. Na verdade, existem várias maneiras de fazer isso enquanto ainda estiver usando try . Muitas pessoas parecem supor que por algum motivo.

if err != nil { return err } é tão "vamos corrigir isso mais tarde" quanto try exceto mais irritante ao prototipar.

Eu não sei como as coisas dentro de um par de parênteses são menos legíveis do que as etapas de função a cada quatro linhas de clichê.

Seria bom se você apontasse algumas dessas "pegadinhas" em particular que o incomodaram, já que esse é o tópico.

A legibilidade parece ser um problema, mas que tal ir fmt apresentando try() para que se destaque, algo como:

f := try(
    os.Open("file.txt")
)

@MrTravisB

O problema que tenho com isso é que você sempre deseja retornar o erro quando isso acontecer.

Discordo. Ele pressupõe que você deseja fazer isso com frequência suficiente para justificar uma abreviação para isso. Se você não fizer isso, isso não atrapalha o tratamento de erros claramente.

Quando talvez você queira adicionar contexto ao erro e devolvê-lo ou talvez você apenas queira se comportar de maneira diferente quando um erro acontecer.

A proposta descreve um padrão para adicionar contexto de todo o bloco aos erros. @josharian apontou que há um erro nos exemplos, porém, e não está claro qual é a melhor maneira de evitá-lo. Eu escrevi alguns exemplos de maneiras de lidar com isso.

Para um contexto de erro mais específico, novamente, try faz uma coisa, e se você não quiser essa coisa, não use try .

@boomlinde Exatamente meu ponto. Esta proposta está tentando resolver um caso de uso singular em vez de fornecer uma ferramenta para resolver o problema maior de tratamento de erros. Eu acho que a questão fundamental é exatamente o que você apontou.

Ele pressupõe que você deseja fazer isso com frequência suficiente para justificar uma abreviação para isso.

Na minha opinião e experiência, este caso de uso é uma pequena minoria e não garante sintaxe abreviada.

Além disso, a abordagem de usar defer para lidar com erros tem problemas, pois pressupõe que você deseja lidar com todos os erros possíveis da mesma forma. As declarações defer não podem ser canceladas.

defer fmt.HandleErrorf(&err, “foobar”)

n := try(foo())

x : try(foo2())

E se eu quiser um tratamento de erros diferente para erros que podem ser retornados de foo() vs foo2() ?

@MrTravisB

E se eu quiser um tratamento de erro diferente para erros que podem ser retornados de foo() vs foo2()?

Então você usa outra coisa. Esse é o ponto que @boomlinde estava fazendo.

Talvez você pessoalmente não veja esse caso de uso com frequência, mas muitas pessoas o fazem, e adicionar try realmente não afeta você. Na verdade, quanto mais raro o caso de uso for para você, menos afetará você que try seja adicionado.

@qrpnxz

f := try(os.Open("/foo"))
data := try(ioutil.ReadAll(f))
try(send(data))

(sim, eu entendo que existe ReadFile e que este exemplo em particular não é a melhor maneira de copiar dados em algum lugar, não é o ponto)

Isso exige mais esforço para ler porque você precisa analisar o inline do try. A lógica do aplicativo é encerrada em outra chamada.
Eu também argumentaria que um manipulador de erro defer aqui não seria bom, exceto para apenas envolver o erro com uma nova mensagem ... o que é bom, mas há mais para lidar com erros do que facilitar para o humano para ler o que aconteceu.

Na ferrugem, pelo menos, o operador é um postfix ( ? adicionado ao final de uma chamada) que não coloca um fardo extra para desenterrar a lógica real.

Controle de fluxo baseado em expressão

panic pode ser outra função de controle de fluxo, mas não retorna um valor, tornando-se efetivamente uma declaração. Compare isso com try , que é uma expressão e pode ocorrer em qualquer lugar.

recover tem um valor e afeta o controle de fluxo, mas deve ocorrer em uma instrução defer . Esses defer s normalmente são literais de função, recover é chamado apenas uma vez e, portanto, recover também ocorre efetivamente como uma instrução. Novamente, compare isso com try que pode ocorrer em qualquer lugar.

Acho que esses pontos significam que try torna significativamente mais difícil seguir o fluxo de controle de uma maneira que não tínhamos antes, como foi apontado antes, mas não vi a distinção entre instruções e expressões apontou.


Outra proposta

Permitir declarações como

if err != nil {
    return nil, 0, err
}

para ser formatado em uma linha por gofmt quando o bloco contém apenas uma instrução return e essa instrução não contém novas linhas. Por exemplo:

if err != nil { return nil, 0, err }

Justificativa

  • Não requer alterações de idioma
  • A regra de formatação é simples e clara
  • A regra pode ser projetada para ser opt-in onde gofmt mantém novas linhas se elas já existirem (como literais de estrutura). Optar também permite que o escritor faça com que algum tratamento de erros seja enfatizado
  • Se não for opt-in, o código pode ser portado automaticamente para o novo estilo com uma chamada para gofmt
  • É apenas para declarações de return , para que não seja usado desnecessariamente o código de golfe
  • Interage bem com comentários que descrevem por que alguns erros podem ocorrer e por que eles estão sendo retornados. Usar muitas expressões try aninhadas lida com isso mal
  • Reduz o espaço vertical de tratamento de erros em 66%
  • Sem fluxo de controle baseado em expressão
  • O código é lido com muito mais frequência do que escrito, por isso deve ser otimizado para o leitor. Código repetitivo ocupando menos espaço é útil para o leitor, onde try se inclina mais para o escritor
  • As pessoas já propuseram try existentes em várias linhas. Por exemplo, este comentário ou este comentário que introduz um estilo como
f, err := os.Open(file)
try(maybeWrap(err))
  • O estilo "experimentar em sua própria linha" remove qualquer ambiguidade sobre qual err está sendo retornado. Portanto, suspeito que este formulário será comumente usado. Permitir um bloco se for alinhado é quase a mesma coisa, exceto que também é explícito sobre quais são os valores de retorno
  • Ele não promove o uso de retornos nomeados ou empacotamento baseado defer pouco claro. Ambos aumentam a barreira para erros de encapsulamento e o primeiro pode exigir alterações godoc
  • Não há necessidade de discussão sobre quando usar try versus usar o tratamento de erros tradicional
  • Não exclui fazer try ou qualquer outra coisa no futuro. A mudança pode ser positiva mesmo que try seja aceito
  • Nenhuma interação negativa com a biblioteca $# testing 7$#$ ou funções main . Na verdade, se a proposta permitir qualquer instrução de linha única em vez de apenas retornos, ela poderá reduzir o uso de bibliotecas baseadas em asserção. Considerar
value, err := something()
if err != nil { t.Fatal(err) }
  • Nenhuma interação negativa com a verificação de erros específicos. Considerar
n, err := src.Read(buf)
if err == io.EOF { return nil }
else if err != nil { return err }

Em resumo, esta proposta tem um custo pequeno, pode ser projetada para ser opt-in, não impede nenhuma alteração adicional, pois é apenas estilística e reduz a dor de ler o código de tratamento de erros detalhado, mantendo tudo explícito. Acho que deve ser pelo menos considerado como um primeiro passo antes de ir all-in try .


Alguns exemplos portados

De https://github.com/golang/go/issues/32437#issuecomment -498941435

Com tentativa

func NewThing(thingy *foo.Thingy, db *sql.DB, client pb.Client) (*Thing, error) {
        try(dbfile.RunMigrations(db, dbMigrations))
        t := &Thing{
                thingy:  thingy,
                scanner: try(newScanner(thingy, db, client)),
        }
        t.initOtherThing()
        return t, nil
}

Com isso

func NewThing(thingy *foo.Thingy, db *sql.DB, client pb.Client) (*Thing, error) {
        err := dbfile.RunMigrations(db, dbMigrations))
        if err != nil { return nil, fmt.Errorf("running migrations: %v", err) }

        t := &Thing{thingy: thingy}
        t.scanner, err = newScanner(thingy, db, client)
        if err != nil { return nil, fmt.Errorf("creating scanner: %v", err) }

        t.initOtherThing()
        return t, nil
}

É competitivo no uso do espaço e ainda permite adicionar contexto aos erros.

De https://github.com/golang/go/issues/32437#issuecomment -499007288

Com tentativa

func (c *Config) Build() error {
    pkgPath := try(c.load())
    b := bytes.NewBuffer(nil)
    try(emplates.ExecuteTemplate(b, "main", c))
    buf := try(format.Source(b.Bytes()))
    target := fmt.Sprintf("%s.go", filename(pkgPath))
    try(ioutil.WriteFile(target, buf, 0644))
    // ...
}

Com isso

func (c *Config) Build() error {
    pkgPath, err := c.load()
    if err != nil { return nil, errors.WithMessage(err, "load config dir") }

    b := bytes.NewBuffer(nil)
    err = templates.ExecuteTemplate(b, "main", c)
    if err != nil { return nil, errors.WithMessage(err, "execute main template") }

    buf, err := format.Source(b.Bytes())
    if err != nil { return nil, errors.WithMessage(err, "format main template") }

    target := fmt.Sprintf("%s.go", filename(pkgPath))
    err = ioutil.WriteFile(target, buf, 0644)
    if err != nil { return nil, errors.WithMessagef(err, "write file %s", target) }
    // ...
}

O comentário original usou um hipotético tryf para anexar a formatação, que foi removida. Não está claro a melhor maneira de adicionar todos os contextos distintos e talvez try nem seja aplicável.

@cpuguy83
Para mim, é mais legível com try . Neste exemplo eu li "abra um arquivo, leia todos os bytes, envie dados". Com o tratamento de erros regular, eu leria "abra um arquivo, verifique se houve um erro, o tratamento de erros faz isso, depois leia todos os bytes, agora verifique se algo aconteceu ..." Eu sei que você pode verificar o err != nil s, mas para mim try é apenas mais fácil porque quando o vejo, reconheço o comportamento imediatamente: retorna se err != nil. Se você tem um ramo eu tenho que ver o que ele faz. Poderia fazer qualquer coisa.

Eu também argumentaria que um manipulador de erro de adiamento aqui não seria bom, exceto para apenas envolver o erro com uma nova mensagem

Tenho certeza de que há outras coisas que você pode fazer no adiamento, mas independentemente disso, try é para o caso geral simples de qualquer maneira. Sempre que você quiser fazer algo mais, sempre há um bom e velho tratamento de erros do Go. Isso não vai embora.

@zeebo Sim, eu gosto disso.
O artigo do @kungfusheep usou uma verificação de erro de uma linha como essa e eu saí para experimentá-lo. Então, assim que salvei, gofmt o expandiu em três linhas, o que foi triste. Muitas funções no stdlib são definidas em uma linha assim, então me surpreendeu que o gofmt expandisse isso.

@qrpnxz

Acontece que eu leio muito código go. Uma das melhores coisas sobre a linguagem é a facilidade que vem da maioria dos códigos seguindo um estilo particular (obrigado gofmt).
Eu não quero ler um monte de código envolto em try(f()) .
Isso significa que haverá uma divergência no estilo/prática de código, ou linters como "oh, você deveria ter usado try() aqui" (o que novamente eu nem gosto, que é o ponto de eu e outros comentarem nesta proposta).

Não é objetivamente melhor que if err != nil { return err } , apenas menos para digitar.


Uma última coisa:

Se você ler a proposta, verá que não há nada que o impeça de

Podemos, por favor, abster-se de tal linguagem? Claro que li a proposta. Acontece que eu li ontem à noite e então comentei esta manhã depois de pensar sobre isso e não expliquei a minúcia do que eu pretendia.
Este é um tom incrivelmente adversário.

@cpuguy83
Meu cara ruim de CPU. Eu não quis dizer isso dessa forma.

E acho que você precisa apontar que o código que usa try será bem diferente do código que não usa, então posso imaginar que isso afetaria a experiência de analisar esse código, mas não posso concordar totalmente que diferentes significa pior neste caso, embora eu entenda que você pessoalmente não gosta, assim como eu pessoalmente gosto. Muitas coisas em Go são assim. Quanto ao que os linters dizem para você fazer é outra questão, eu acho.

Claro que não é objetivamente melhor. Eu estava expressando que era mais legível assim para mim . Eu cuidadosamente escrevi isso.

Mais uma vez, desculpe por soar assim. Embora este seja um argumento, eu não quis antagonizar você.

https://github.com/golang/go/issues/32437#issuecomment -498908380

Ninguém vai fazer você usar tentar.

Ignorando a ligeireza, acho que é uma maneira bastante ondulada de descartar uma crítica de design.

Claro, eu não tenho que usá-lo. Mas qualquer pessoa com quem eu escrevo código pode usá-lo e me forçar a tentar decifrar try(try(try(to()).parse().this)).easily()) . É como dizer

Ninguém vai fazer você usar a interface vazia{}.

De qualquer forma, Go é bastante rigoroso com a simplicidade: gofmt faz com que todo o código tenha a mesma aparência. O caminho feliz continua à esquerda e tudo o que pode ser caro ou surpreendente fica explícito . try como é proposto é um giro de 180 graus a partir disso. Simplicidade != conciso.

No mínimo try deve ser uma palavra-chave com lvalues.

Não é _objetivamente_ melhor que if err != nil { return err } , apenas menos para digitar.

Há uma diferença objetiva entre os dois: try(Foo()) é uma expressão. Para alguns, essa diferença é uma desvantagem (a crítica try(strconv.Atoi(x))+try(strconv.Atoi(y)) ). Para outros, essa diferença é uma vantagem pelo mesmo motivo. Ainda não objetivamente melhor ou pior - mas também não acho que a diferença deva ser varrida para debaixo do tapete e alegar que é "apenas menos digitar" não faz justiça à proposta.

@elagergren-spideroak difícil dizer que try é chato de se ver em uma respiração e depois dizer que não é explícito na próxima. Você tem que escolher um.

é comum ver argumentos de função sendo colocados primeiro em variáveis ​​temporárias. Tenho certeza que seria mais comum ver

this := try(to()).parse().this
that := try(this.easily())

do que o seu exemplo.

try não fazer nada é o caminho feliz, então isso parece o esperado. No caminho infeliz tudo o que faz é retornar. Vendo que há um try é suficiente para reunir essa informação. Também não há nada caro em retornar de uma função, então a partir dessa descrição eu não acho que try esteja fazendo um 180

@josharian Em relação ao seu comentário em https://github.com/golang/go/issues/32437#issuecomment -498941854 , não acho que haja um erro de avaliação antecipada aqui.

defer fmt.HandleErrorf(&err, “foobar: %v”, err)

O valor não modificado de err é passado para HandleErrorf e um ponteiro para err é passado. Verificamos se err é nil (usando o ponteiro). Caso contrário, formatamos a string, usando o valor não modificado de err . Em seguida, definimos err para o valor de erro formatado, usando o ponteiro.

@Merovius A proposta realmente é apenas uma macro de açúcar de sintaxe, então acabará sendo sobre o que as pessoas acham que parece mais legal ou causará menos problemas. Se você acha que não, por favor me explique. É por isso que sou a favor, pessoalmente. É uma boa adição sem adicionar palavras-chave da minha perspectiva.

@ianlancetaylor , acho que @josharian está correto: o valor “não modificado” de err é o valor no momento em que o defer é colocado na pilha, não o valor (presumivelmente pretendido) de err definido por try antes de retornar.

O outro problema que tenho com try é que torna muito mais fácil para as pessoas despejarem mais lógica em uma única linha. Este é o meu maior problema com a maioria das outras linguagens, é que elas tornam muito fácil colocar 5 expressões em uma única linha, e eu não quero isso de lado.

this := try(to()).parse().this
that := try(this.easily())

^^ mesmo isso é absolutamente horrível. A primeira linha, eu tenho que pular para frente e para trás fazendo a correspondência de parênteses na minha cabeça. Mesmo a segunda linha, que na verdade é bem simples... é muito difícil de ler.
Funções aninhadas são difíceis de ler.

parser, err := to()
if err != nil {
    return err
}
this := parser.parse().this
that, err := this.easily()
if err != nil {
    return err
}

^^ Isso é muito mais fácil e melhor IMO. É super simples e claro. sim, são muito mais linhas de código, não me importo. É muito óbvio.

@bcmills @josharian Ah, claro, obrigado. Então teria que ser

defer func() { fmt.HandleErrorf(&err, “foobar: %v”, err) }()

Não tão legal. Talvez fmt.HandleErrorf deva passar implicitamente o valor do erro como o último argumento, afinal.

Esta questão recebeu muitos comentários muito rapidamente, e muitos deles me parecem estar repetindo comentários que já foram feitos. Claro, sinta-se à vontade para comentar, mas gostaria de sugerir gentilmente que, se você quiser reafirmar um ponto que já foi feito, faça isso usando os emojis do GitHub, em vez de repetir o ponto. Obrigado.

@ianlancetaylor se fmt.HandleErrorf enviar err como o primeiro argumento após o formato, a implementação será melhor e o usuário poderá referenciá-lo por %[1]v sempre.

@natefinch Concordo absolutamente.

Gostaria de saber se uma abordagem de estilo ferrugem seria mais palatável?
Observe que esta não é uma proposta apenas pensando nisso ...

this := to()?.parse().this
that := this.easily()?

No final eu acho que isso é mais legal, mas (poderia usar um ! ou qualquer outra coisa também...), mas ainda não corrige bem o problema de manipulação de erros.


é claro que ferrugem também tem try() mais ou menos assim, mas... o outro estilo ferrugem.

Não é _objetivamente_ melhor que if err != nil { return err } , apenas menos para digitar.

Há uma diferença objetiva entre os dois: try(Foo()) é uma expressão. Para alguns, essa diferença é uma desvantagem (a crítica try(strconv.Atoi(x))+try(strconv.Atoi(y)) ). Para outros, essa diferença é uma vantagem pelo mesmo motivo. Ainda não objetivamente melhor ou pior - mas também não acho que a diferença deva ser varrida para debaixo do tapete e alegar que é "apenas menos digitar" não faz justiça à proposta.

Esta é uma das maiores razões pelas quais eu gosto desta sintaxe; ele me permite usar uma função de retorno de erro como parte de uma expressão maior sem precisar nomear todos os resultados intermediários. Em algumas situações, nomeá-los é fácil, mas em outras não há um nome particularmente significativo ou não redundante para dar a eles, caso em que eu prefiro não dar um nome a eles.

@MrTravisB

Exatamente meu ponto. Esta proposta está tentando resolver um caso de uso singular em vez de fornecer uma ferramenta para resolver o problema maior de tratamento de erros. Eu acho que a questão fundamental é exatamente o que você apontou.

O que especificamente eu disse que é exatamente o seu ponto? Parece-me que você entendeu mal meu ponto de vista se acha que concordamos.

Na minha opinião e experiência, este caso de uso é uma pequena minoria e não garante sintaxe abreviada.

Na fonte Go, existem milhares de casos que podem ser tratados por try imediatamente, mesmo que não haja como adicionar contexto aos erros. Se menor, ainda é uma causa comum de reclamação.

Além disso, a abordagem de usar defer para lidar com erros tem problemas, pois pressupõe que você deseja lidar com todos os erros possíveis da mesma forma. instruções defer não podem ser canceladas.

Da mesma forma, a abordagem de usar + para lidar com aritmética pressupõe que você não quer subtrair, então você não quer se não quiser. A questão interessante é se o contexto de erro em todo o bloco representa pelo menos um padrão comum.

E se eu quiser um tratamento de erros diferente para erros que podem ser retornados de foo() vs foo2()

Novamente, então você não usa try . Então você não ganha nada com try , mas também não perde nada.

@cpuguy83

Gostaria de saber se uma abordagem de estilo ferrugem seria mais palatável?

A proposta apresenta um argumento contra isso.

Neste ponto, acho que ter try{}catch{} é mais legível :upside_down_face:

  1. Usar importações nomeadas para contornar casos de canto defer não é apenas horrível para coisas como godoc, mas o mais importante é muito propenso a erros. Eu não me importo que eu possa embrulhar a coisa toda com mais func() para contornar o problema, é apenas mais coisas que preciso ter em mente, acho que incentiva uma "má prática".
  2. Ninguém vai fazer você usar tentar.

    Isso não significa que seja uma boa solução, estou afirmando que a ideia atual tem uma falha no design e estou pedindo que ela seja abordada de uma maneira menos propensa a erros.

  3. Acho que exemplos como try(try(try(to()).parse().this)).easily()) não são realistas, isso já poderia ser feito com outras funções e acho que seria justo para quem está revisando o código pedir para que ele seja dividido.
  4. E se eu tiver 3 lugares que podem apresentar erros e eu quiser envolver cada lugar separadamente? try() torna isso muito difícil, na verdade try() já está desencorajando erros de empacotamento dada a dificuldade disso, mas aqui está um exemplo do que quero dizer:

    func before() error {
      x, err := foo()
      if err != nil {
        wrap(err, "error on foo")
      }
      y, err := bar(x)
      if err != nil {
        wrapf(err, "error on bar with x=%v", x)
      }
      fmt.Println(y)
      return nil
    }
    
    func after() (err error) {
      defer fmt.HandleErrorf(&err, "something failed but I don't know where: %v", err)
      x := try(foo())
      y := try(bar(x))
      fmt.Println(y)
      return nil
    }
    
  5. Novamente, então você não usa try . Então você não ganha nada com try , mas também não perde nada.

    Digamos que seja uma boa prática envolver erros com contexto útil, try() seria considerado uma prática ruim porque não está adicionando nenhum contexto. Isso significa que try() é um recurso que ninguém quer usar e se tornou um recurso que é usado tão raramente que pode não ter existido.

    Em vez de apenas dizer "bem, se você não gosta, não use e cale a boca" (é assim que se lê), acho que seria melhor tentar abordar o que muitos de uso estão considerando uma falha no projeto. Podemos discutir, em vez disso, o que poderia ser modificado a partir do design proposto para que nossa preocupação seja tratada de uma maneira melhor?

@boomlinde O ponto em que concordamos é que esta proposta está tentando resolver um caso de uso menor e o fato de que "se você não precisa, não use" é o principal argumento para avançar nesse ponto. Como @elagergren-spideroak afirmou, esse argumento não funciona porque, mesmo que eu não queira usá-lo, outros o farão, o que me obriga a usá-lo. Pela lógica do seu argumento, Go também deve ter uma declaração ternária. E se você não gosta de declarações ternárias, não as use.

Isenção de responsabilidade - eu acho que Go deveria ter uma declaração ternária, mas dado que a abordagem de Go para recursos de linguagem é não introduzir recursos que possam tornar o código mais difícil de ler do que não deveria.

Outra coisa me ocorre: estou vendo muitas críticas baseadas na ideia de que ter try pode encorajar os desenvolvedores a lidar com erros de forma descuidada. Mas, na minha opinião, isso é mais verdadeiro para a linguagem atual; o clichê de tratamento de erros é irritante o suficiente para encorajar a pessoa a engolir ou ignorar alguns erros para evitá-lo. Por exemplo, eu escrevi coisas assim algumas vezes:

func exists(filename string) bool {
  _, err := os.Stat(filename)
  return err == nil
}

para poder escrever if exists(...) { ... } , mesmo que este código ignore silenciosamente alguns possíveis erros. Se eu tivesse try , provavelmente não me incomodaria em fazer isso e apenas devolveria (bool, error) .

Sendo caótico aqui, vou lançar a ideia de adicionar uma segunda função interna chamada catch que receberá uma função que recebe um erro e retorna um erro sobrescrito, se um catch subsequente é chamado, ele substituirá o manipulador. por exemplo:

func catch(handler func(err error) error) {
  // .. impl ..
}

Agora, essa função interna também será uma função semelhante a uma macro que lidaria com o próximo erro a ser retornado por try assim:

func wrapf(format string, ...values interface{}) func(err error) error {
  // user defined
  return func(err error) error {
    return fmt.Errorf(format + ": %v", ...append(values, err))
  }
}
func sample() {
  catch(wrapf("something failed in foo"))
  try(foo()) // "something failed in foo: <error>"
  x := try(foo2()) // "something failed in foo: <error>"
  // Subsequent calls for catch overwrite the handler
  catch(wrapf("something failed in bar with x=%v", x))
  try(bar(x)) // "something failed in bar with x=-1: <error>"
}

Isso é bom porque eu posso encapsular erros sem defer que podem ser propensos a erros, a menos que usemos valores de retorno nomeados ou envolva com outra função, também é bom porque defer adicionaria o mesmo manipulador de erros para todos os erros, mesmo que eu queira lidar com 2 deles de maneira diferente. Você também pode usá-lo como achar melhor, por exemplo:

func foo(a, b string) (int64, error) {
  return try(strconv.Atoi(a)) + try(strconv.Atoi(b))
}
func withContext(a, b string) (int64, error) {
  catch(func (err error) error {
    return fmt.Errorf("can't parse a: %s, b: %s, err: %v", a, b, err)
  })
  return try(strconv.Atoi(a)) + try(strconv.Atoi(b))
}
func moreExplicitContext(a, b string) (int64, error) {
  catch(func (err error) error {
    return fmt.Errorf("can't parse a: %s, err: %v", a, err)
  })
  x := try(strconv.Atoi(a))
  catch(func (err error) error {
    return fmt.Errorf("can't parse b: %s, err: %v", b, err)
  })
  y := try(strconv.Atoi(b))
  return x + y
}
func withHelperWrapf(a, b string) (int64, error) {
  catch(wrapf("can't parse a: %s", a))
  x := try(strconv.Atoi(a))
  catch(wrapf("can't parse b: %s", b))
  y := try(strconv.Atoi(b))
  return x + y
}
func before(a, b string) (int64, error) {
  x, err := strconv.Atoi(a)
  if err != nil {
    return 0,  fmt.Errorf("can't parse a: %s, err: %v", a, err)
  }
  y, err := strconv.Atoi(b)
  if err != nil {
    return 0,  fmt.Errorf("can't parse b: %s, err: %v", b, err)
  }
  return x + y
}

E ainda no clima caótico (para te ajudar a ter empatia) Se você não gosta catch , não precisa usar.

Agora... eu realmente não quero dizer a última frase, mas parece que não é útil para a discussão, IMO muito agressivo.
Ainda assim, se formos por esse caminho, acho que podemos ter try{}catch(error err){} em vez disso :stuck_out_tongue:

Veja também #27519 - o modelo de erro #id/catch

Ninguém vai fazer você usar tentar.

Ignorando a ligeireza, acho que é uma maneira bastante ondulada de descartar uma crítica de design.

Desculpe, glib não era minha intenção.

O que estou tentando dizer é que try não se destina a ser uma solução 100%. Existem vários paradigmas de tratamento de erros que não são bem tratados por try . Por exemplo, se você precisar adicionar contexto dependente de callsite ao erro. Você sempre pode voltar a usar if err != nil { para lidar com esses casos mais complicados.

É certamente um argumento válido que try não pode lidar com X, para várias instâncias de X. Mas muitas vezes lidar com o caso X significa tornar o mecanismo mais complicado. Há uma compensação aqui, lidar com X por um lado, mas complicando o mecanismo para todo o resto. O que fazemos depende de quão comum é o X e quanta complicação seria necessária para lidar com o X.

Então, por "Ninguém vai fazer você usar tentar", quero dizer que acho que o exemplo em questão está nos 10%, não nos 90%. Essa afirmação certamente está em debate, e fico feliz em ouvir contra-argumentos. Mas eventualmente teremos que traçar a linha em algum lugar e dizer "sim, try não vai lidar com esse caso. Você terá que usar o tratamento de erros de estilo antigo. Desculpe.".

Não é que "try não pode lidar com este caso específico de tratamento de erros" que é o problema, é "try incentiva você a não encapsular seus erros". A idéia check-handle forçou você a escrever uma declaração de retorno, então escrever um empacotamento de erro era bem trivial.

Sob esta proposta, você precisa usar um retorno nomeado com defer , o que não é intuitivo e parece muito hacky.

A idéia check-handle forçou você a escrever uma declaração de retorno, então escrever um empacotamento de erro era bem trivial.

Isso não é verdade - no rascunho do design, toda função que retorna um erro tem um manipulador padrão que apenas retorna o erro.

Com base no ponto travesso do @Goodwine , você realmente não precisa de funções separadas como HandleErrorf se tiver uma única função de ponte como

func handler(err *error, handle func(error) error) {
  // nil handle is treated as the identity
  if *err != nil && handle != nil {
    *err = handle(*err)
  }
}

que você usaria como

defer handler(&err, func(err error) error {
  if errors.Is(err, io.EOF) {
    return nil
  }
  return fmt.Errorf("oops: %w", err)
})

Você poderia fazer handler si mesmo uma semi-mágica como try .

Se for mágico, pode aceitar seu primeiro argumento implicitamente - permitindo que ele seja usado mesmo em funções que não nomeiam seu retorno error , eliminando um dos aspectos menos afortunados da proposta atual e tornando-a menos exigente e propenso a erros de decoração. Claro, isso não reduz muito o exemplo anterior:

defer handler(func(err error) error {
  if errors.Is(err, io.EOF) {
    return nil
  }
  return fmt.Errorf("oops: %w", err)
})

Se fosse mágico dessa maneira, teria que ser um erro de tempo de compilação se fosse usado em qualquer lugar, exceto como argumento para defer . Você poderia dar um passo adiante e fazê-lo adiar implicitamente, mas defer handler lê muito bem.

Como ele usa defer , ele pode chamar sua função handle sempre que houver um erro não nulo sendo retornado, tornando-o útil mesmo sem try pois você pode adicionar um

defer handler(wrapErrWithPackageName)

no topo para fmt.Errorf("mypkg: %w", err) tudo.

Isso lhe dá muito da proposta check / handle mais antiga, mas funciona com adiar naturalmente (e explicitamente) enquanto se livra da necessidade, na maioria dos casos, de nomear explicitamente um err retorno. Como try é uma macro relativamente simples que (eu imagino) poderia ser implementada inteiramente no front-end.

Isso não é verdade - no rascunho do design, toda função que retorna um erro tem um manipulador padrão que apenas retorna o erro.

Meu mal, você está certo.

Quero dizer que acho que o exemplo em questão está nos 10%, não nos 90%. Essa afirmação certamente está em debate, e fico feliz em ouvir contra-argumentos. Mas, eventualmente, teremos que traçar a linha em algum lugar e dizer "sim, o try não resolverá esse caso. Você terá que usar o tratamento de erros de estilo antigo. Desculpe.".

Concordo, minha opinião é que esta linha deve ser traçada ao verificar EOF ou similar, não no empacotamento. Mas talvez se os erros tivessem mais contexto, isso não seria mais um problema.

Poderia try() auto-empacotar erros com contexto útil para depuração? Por exemplo, se xerrors se tornar errors , os erros devem ter algo parecido com um rastreamento de pilha que try() poderia adicionar, não? Se sim, talvez seja o suficiente 🤔

Se os objetivos forem (lendo https://github.com/golang/proposal/blob/master/design/32437-try-builtin.md):

  • eliminar o clichê
  • alterações mínimas de idioma
  • cobrindo "cenários mais comuns"
  • adicionando muito pouca complexidade à linguagem

Eu aceitaria a sugestão de dar um ângulo e permitir a migração de código de "pequenos passos" para todos os bilhões de linhas de código por aí.

em vez do sugerido:

func printSum(a, b string) error {
        defer fmt.HandleErrorf(&err, "sum %s %s: %v", a,b, err) 
        x := try(strconv.Atoi(a))
        y := try(strconv.Atoi(b))
        fmt.Println("result:", x + y)
        return nil
}

Nós podemos:

func printSum(a, b string) error {
        var err ErrHandler{HandleFunc : twoStringsErr("printSum",a,b)} 
        x, err.Error := strconv.Atoi(a)
        y,err.Error := strconv.Atoi(b)
        fmt.Println("result:", x + y)
        return nil
}

O que ganharíamos?
twoStringsErr pode ser embutido em printSum, ou um manipulador geral que sabe como capturar erros (neste caso com 2 parâmetros de string) - então, se eu tiver as mesmas assinaturas de função repetidas usadas em muitas das minhas funções, não preciso reescrever o manipulador cada Tempo
da mesma maneira, posso ter o tipo ErrHandler estendido da maneira:

type ioErrHandler ErrHandler
func (i ErrHandler) Handle() ...{

}

ou

type parseErrHandler ErrHandler
func (p parseErrHandler) Handle() ...{

}

ou

type str2IntErrHandler ErrHandler
func (s str2IntErrHandler) Handle() ...{

}

e use isso em todo o meu código:

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

Portanto, a necessidade real seria desenvolver um gatilho quando err.Error for definido como não nulo
Usando este método, podemos também:

func (s str2IntErrHandler) Handle() bool{
   **return false**
}

O que diria à função de chamada para continuar em vez de retornar

E use diferentes manipuladores de erros na mesma função:

func printSum(a, b string) error {
        var pErr str2IntErrHandler 
        var oErr overflowError 
        x, err.Error := strconv.Atoi(a)
        y,err.Error := strconv.Atoi(b)
        fmt.Println("result:", x + y)
        totalAsByte,oErr := sumBytes(x,y)
        sunAsByte,oErr := subtractBytes(x,y)
        return nil
}

etc.

Repassando as metas novamente

  • eliminar o clichê - feito
  • alterações mínimas de idioma - feito
  • cobrindo "cenários mais comuns" - mais do que o IMO sugerido
  • adicionando muito pouca complexidade à linguagem - sone
    Além disso - migração de código mais fácil de
x, err := strconv.Atoi(a)

para

x, err.Error := strconv.Atoi(a)

e na verdade - melhor legibilidade (IMO, novamente)

@guybrand você é o mais recente adepto desse tema recorrente (que eu gosto).

Veja https://github.com/golang/go/wiki/Go2ErrorHandlingFeedback#recurring -themes

@guybrand Essa parece ser uma proposta totalmente diferente; Acho que você deveria arquivá-lo como seu próprio problema para que este possa se concentrar em discutir a proposta de @griesemer .

@natefinch concordo. Acho que isso é mais voltado para melhorar a experiência ao escrever Go em vez de otimizar para leitura. Gostaria de saber se macros ou trechos de IDE poderiam resolver o problema sem que isso se tornasse um recurso da linguagem.

@Bom vinho

Digamos que seja uma boa prática envolver erros com contexto útil, try() seria considerado uma prática ruim porque não está adicionando nenhum contexto. Isso significa que try() é um recurso que ninguém quer usar e se torna um recurso que é usado tão raramente que pode não ter existido.

Conforme observado na proposta (e mostrado por exemplo), try não impede fundamentalmente que você adicione contexto. Eu diria que do jeito que é proposto, adicionar contexto aos erros é totalmente ortogonal a ele. Isso é abordado especificamente no FAQ da proposta.

Eu reconheço que try não será útil se dentro de uma única função houver uma infinidade de contextos diferentes que você deseja adicionar a erros diferentes de chamadas de função. No entanto, também acredito que algo na veia geral de HandleErrorf cobre uma grande área de uso porque apenas adicionar contexto de toda a função aos erros não é incomum.

Em vez de apenas dizer "bem, se você não gosta, não use e cale a boca" (é assim que se lê), acho que seria melhor tentar abordar o que muitos de uso estão considerando uma falha no projeto.

Se é assim que se lê, peço desculpas. Meu ponto não é que você deve fingir que não existe se você não gosta. É que é óbvio que há casos em que try seria inútil e que você não deveria usá-lo em tais casos, o que para esta proposta eu acredito que atinge um bom equilíbrio entre KISS e utilidade geral. Eu não acho que eu estava claro sobre esse ponto.

Obrigado a todos pelo feedback prolífico até agora; isso é muito informativo.
Aqui está minha tentativa de um resumo inicial, para ter uma ideia melhor do feedback. Peço desculpas antecipadamente por qualquer pessoa que eu tenha perdido ou deturpado; Espero que eu tenha entendido a essência geral da coisa certa.

0) Do lado positivo, @rasky , @adg , @eandre , @dpinela e outros expressaram explicitamente felicidade pela simplificação de código que try fornece.

1) A preocupação mais importante parece ser que try não encoraja um bom estilo de tratamento de erros, mas sim promove a "saída rápida". ( @agnivade , @peterbourgon , @politician , @a8m , @eandre , @prologic , @kungfusheep , @cpuguy e outros expressaram sua preocupação com isso.)

2) Muitas pessoas não gostam da ideia de um built-in, ou da sintaxe de função que vem com ele porque esconde um return . Seria melhor usar uma palavra-chave. ( @sheerun , @Redundancy , @dolmen , @komuw , @RobertGrantEllis , @elagergren-spideroak). try também pode ser facilmente ignorado (@peterbourgon), especialmente porque pode aparecer em expressões que podem ser aninhadas arbitrariamente. @natefinch está preocupado que try torna "muito fácil despejar muito em uma linha", algo que geralmente tentamos evitar em Go. Além disso, o suporte do IDE para enfatizar try pode não ser suficiente (@dominikh); try precisa "ficar sozinho".

3) Para alguns, o status quo de declarações explícitas if não é um problema, eles estão felizes com isso ( @bitfield , @marwan-at-work, @natefinch). É melhor ter apenas uma maneira de fazer as coisas (@gbbr); e as instruções if explícitas são melhores que as return implícitas ( @DavexPro , @hmage , @prologic , @natefinch).
Na mesma linha, @mattn está preocupado com a "vinculação implícita" do resultado do erro para try - a conexão não é explicitamente visível no código.

4) Usar try dificultará a depuração do código; por exemplo, pode ser necessário reescrever uma expressão try volta em uma instrução if apenas para que as instruções de depuração possam ser inseridas ( @deanveloper , @typeless , @networkimprov , outros).

5) Há alguma preocupação com o uso de retornos nomeados ( @buchanae , @adg).

Várias pessoas deram sugestões para melhorar ou modificar a proposta:

6) Alguns adotaram a ideia de um manipulador de erros opcional (@beoran) ou string de formato fornecida a try ( @unexge , @a8m , @eandre , @gotwarlost) para incentivar um bom tratamento de erros.

7) @pierrec sugeriu que gofmt poderia formatar expressões try adequadamente para torná-las mais visíveis.
Alternativamente, pode-se tornar o código existente mais compacto, permitindo que gofmt formate instruções if verificando erros em uma linha (@zeebo).

8) @marwan-at-work argumenta que try simplesmente muda o tratamento de erros de instruções $# if 9$#$ para expressões try . Em vez disso, se quisermos realmente resolver o problema, Go deve "possuir" o tratamento de erros, tornando-o verdadeiramente implícito. O objetivo deve ser tornar o tratamento de erros (adequado) mais simples e os desenvolvedores mais produtivos (@cpuguy).

9) Finalmente, algumas pessoas não gostam do nome try ( @beoran , @HiImJC , @dolmen) ou preferem um símbolo como ? ( @twisted1919 , @leaxoy , outros) .

Alguns comentários sobre este feedback (numerados de acordo):

0) Obrigado pelo feedback positivo! :-)

1) Seria bom aprender mais sobre essa preocupação. O estilo de codificação atual usando instruções if para testar erros é o mais explícito possível. É muito fácil adicionar informações adicionais a um erro, individualmente (para cada if ). Muitas vezes faz sentido tratar todos os erros detectados em uma função de forma uniforme, o que pode ser feito com um defer - isso já é possível agora. É o fato de já termos todas as ferramentas para um bom tratamento de erros na linguagem, e o problema de uma construção de manipulador não ser ortogonal a defer , que nos levou a deixar de lado um novo mecanismo apenas para aumentar erros .

2) É claro que existe a possibilidade de usar uma palavra-chave ou sintaxe especial em vez de uma built-in. Uma nova palavra-chave não será compatível com versões anteriores. Um novo operador pode, mas parece ainda menos visível. A proposta detalhada discute detalhadamente os vários prós e contras. Mas talvez estejamos julgando mal isso.

3) A razão para esta proposta é que o tratamento de erros (especificamente o código padrão associado) foi mencionado como um problema significativo em Go (junto à falta de genéricos) pela comunidade Go. Esta proposta aborda diretamente a preocupação do clichê. Ele não faz mais do que resolver o caso mais básico porque qualquer caso mais complexo é melhor tratado com o que já temos. Portanto, embora um bom número de pessoas esteja satisfeito com o status quo, há um contingente (provavelmente) igualmente grande de pessoas que adoraria uma abordagem mais simplificada, como try , sabendo que isso é "apenas" açúcar sintático.

4) O ponto de depuração é uma preocupação válida. Se houver necessidade de adicionar código entre detectar um erro e um return , ter que reescrever uma expressão try em uma instrução if pode ser irritante.

5) Valores de retorno nomeados: O documento detalhado discute isso detalhadamente. Se esta é a principal preocupação sobre esta proposta, então estamos em um bom lugar, eu acho.

6) Argumento de manipulador opcional para try : O documento detalhado também discute isso. Consulte a seção sobre iterações de design.

7) Usar gofmt para formatar expressões try forma que fiquem mais visíveis certamente seria uma opção. Mas isso tiraria alguns dos benefícios de try quando usado em uma expressão.

8) Consideramos olhar para o problema do ponto de vista do tratamento de erros ( handle ) em vez do ponto de vista do teste de erros ( try ). Especificamente, consideramos brevemente apenas introduzir a noção de um manipulador de erros (semelhante ao rascunho do projeto original apresentado no Gophercon do ano passado). O pensamento era que se (e somente se) um manipulador for declarado, em atribuições de vários valores onde o último valor é do tipo error , esse valor pode simplesmente ser deixado de lado em uma atribuição. O compilador verificaria implicitamente se não é nulo e, em caso afirmativo, ramificaria para o manipulador. Isso faria o tratamento de erros explícito desaparecer completamente e encorajaria todos a escreverem um handler. Isso parecia uma abordagem extrema porque seria completamente implícito - o fato de que uma verificação acontecesse seria invisível.

9) Posso sugerir que não troquemos de bicicleta o nome neste momento. Uma vez que todas as outras preocupações são resolvidas, é um momento melhor para ajustar o nome.

Isso não quer dizer que as preocupações não sejam válidas - as respostas acima estão simplesmente afirmando nosso pensamento atual. No futuro, seria bom comentar sobre novas preocupações (ou novas evidências em apoio a essas preocupações) - apenas reafirmar o que já foi dito não nos fornece mais informações.

E, finalmente, parece que nem todos que comentam sobre o assunto leram o documento detalhado. Por favor, faça isso antes de comentar para evitar repetir o que já foi dito. Obrigado.

Este não é um comentário sobre a proposta, mas um relatório de erros de digitação. Não foi corrigido desde que a proposta completa foi publicada, então pensei em mencioná-la:

func try(t1 T1, t1 T2, … tn Tn, te error) (T1, T2, … Tn)

deveria estar:

func try(t1 T1, t2 T2, … tn Tn, te error) (T1, T2, … Tn)

Valeria a pena analisar o código Go abertamente disponível para instruções de verificação de erros para tentar descobrir se a maioria das verificações de erros é realmente repetitiva ou se, na maioria dos casos, várias verificações dentro da mesma função adicionam informações contextuais diferentes? A proposta faria muito sentido para o primeiro caso, mas não ajudaria o segundo. No último caso, as pessoas continuarão usando if err != nil ou desistirão de adicionar contexto extra, usarão try() e recorrerão à adição de contexto de erro comum por função que IMO seria prejudicial. Com os próximos recursos de valores de erro, acho que esperamos que as pessoas envolvam erros com mais informações com mais frequência. Provavelmente eu entendi mal a proposta, mas AFAIU, isso ajuda a reduzir o clichê apenas quando todos os erros de uma única função devem ser encapsulados exatamente de uma maneira e não ajuda se uma função lidar com cinco erros que podem precisar ser encapsulados de maneira diferente. Não tenho certeza de quão comuns são esses casos na natureza (bastante comuns na maioria dos meus projetos), mas estou preocupado que try() possa incentivar as pessoas a usar wrappers comuns por função, mesmo quando faria sentido agrupar erros diferentes diferente.

Apenas um comentário rápido com dados de um pequeno conjunto de amostras:

Propomos uma nova função interna chamada try, projetada especificamente para eliminar o clichê se as instruções normalmente associadas ao tratamento de erros em Go

Se este for o problema central que está sendo resolvido por esta proposta, acho que esse "clichê" representa apenas ~ 1,4% do meu código em dezenas de projetos de código aberto disponíveis publicamente, totalizando ~ 60k SLOC.

Curioso se alguém tem estatísticas semelhantes?

Em uma base de código muito maior como o próprio Go, totalizando cerca de 1,6 milhão de SLOC, isso equivale a cerca de 0,5% da base de código com linhas como if err != nil .

Esse é realmente o problema mais impactante para resolver com o Go 2?

Muito obrigado @griesemer por dedicar um tempo para analisar as ideias de todos e fornecer pensamentos explicitamente. Acho que ajuda muito na percepção de que a comunidade está sendo ouvida no processo.

  1. @pierrec sugeriu que o gofmt pudesse formatar as expressões try adequadamente para torná-las mais visíveis.
    Alternativamente, pode-se tornar o código existente mais compacto, permitindo que o gofmt formate instruções if verificando erros em uma linha (@zeebo).
  1. Usar gofmt para formatar expressões try de forma que fiquem mais visíveis certamente seria uma opção. Mas tiraria alguns dos benefícios de try quando usado em uma expressão.

Estes são pensamentos valiosos sobre exigir gofmt para formatar try , mas estou interessado se houver algum pensamento em particular sobre gofmt permitir a verificação de declaração if o erro seja uma linha. A proposta foi agrupada com formatação de try , mas acho que é uma coisa completamente ortogonal. Obrigado.

@griesemer obrigado pelo trabalho incrível passando por todos os comentários e respondendo a maioria, senão todos os comentários 🎉

Uma coisa que não foi abordada em seu feedback foi a ideia de usar a parte de ferramentas/verificação da linguagem Go para melhorar a experiência de tratamento de erros, em vez de atualizar a sintaxe Go.

Por exemplo, com a chegada do novo LSP ( gopls ), parece um lugar perfeito para analisar a assinatura de uma função e cuidar do clichê de tratamento de erros para o desenvolvedor, com envolvimento e verificação adequados também.

@griesemer Tenho certeza de que isso não foi bem pensado, mas tentei modificar sua sugestão mais perto de algo com o qual me sentiria confortável aqui: https://www.reddit.com/r/golang/comments/bwvyhe /proposal_a_builtin_go_error_check_function_try/eq22bqa?utm_source=share&utm_medium=web2x

@zeebo Seria fácil fazer gofmt formatar if err != nil { return ...., err } em uma única linha. Presumivelmente, seria apenas para esse tipo específico de padrão if , não para todas as instruções if "curtas"?

Na mesma linha, havia preocupações sobre try ser invisível porque está na mesma linha que a lógica de negócios. Temos todas essas opções:

Estilo atual:

a, b, c, ... err := BusinessLogic(...)
if err != nil {
   return ..., err
}

Uma linha if :

a, b, c, ... err := BusinessLogic(...)
if err != nil { return ..., err }

try em uma linha separada (!):

a, b, c, ... err := BusinessLogic(...)
try(err)

try como proposto:

a, b, c := try(BusinessLogic(...))

A primeira e a última linha parecem as mais claras (para mim), especialmente quando se usa para reconhecer try como o que é. Com a última linha, um erro é explicitamente verificado, mas como (geralmente) não é a ação principal, fica um pouco mais em segundo plano.

@marwan-at-work Não tenho certeza do que você está propondo que as ferramentas fazem por você. Você sugere que eles ocultem o tratamento de erros de alguma forma?

@dpinela

@guybrand Essa parece ser uma proposta totalmente diferente; Acho que você deveria arquivá-lo como seu próprio problema para que este possa se concentrar em discutir a proposta de @griesemer .

IMO minha proposta difere apenas na sintaxe, ou seja:

  • As metas são semelhantes em conteúdo e prioridade.
  • A ideia de capturar cada erro dentro de sua própria linha e, consequentemente (se não for nil), sair da função enquanto passa por uma função de manipulador é semelhante (pseudo asm - é um "jnz" e "call").
  • Isso significa que o número de linhas em um corpo de função (sem o defer) e o fluxo seriam exatamente iguais (e, portanto, o AST provavelmente também seria o mesmo)

então a principal diferença é se estamos envolvendo a chamada de função original com try(func()) que sempre analisaria a última var para jnz a chamada ou usaria o valor de retorno real para fazer isso.

Eu sei que parece diferente, mas na verdade muito semelhante em conceito.
Por outro lado - se você fizer a tentativa usual .... pegar em muitas linguagens do tipo c - isso seria uma implementação muito diferente, legibilidade diferente etc.

No entanto, penso seriamente em escrever uma proposta, obrigado pela ideia.

@griesemer

Não tenho certeza do que você está propondo que as ferramentas são para você. Você sugere que eles ocultem o tratamento de erros de alguma forma?

Muito pelo contrário: estou sugerindo que gopls pode opcionalmente escrever o clichê de tratamento de erros para você.

Como você mencionou em seu último comentário:

A razão para esta proposta é que o tratamento de erros (especificamente o código padrão associado) foi mencionado como um problema significativo em Go (junto à falta de genéricos) pela comunidade Go

Portanto, o cerne do problema é que o programador acaba escrevendo muito código clichê. Então a questão é escrever, não ler. Portanto, minha sugestão é: deixe o computador (tooling/gopls) fazer a escrita para o programador analisando a assinatura da função e colocando cláusulas de tratamento de erros apropriadas.

Por exemplo:

// user begins to write this function: 
func openFile(path string) ([]byte, error) {
  file, err := os.Open(path)
  defer file.Close()
  bts, err := ioutil.ReadAll(file)
  return bts, nil
}

Em seguida, o usuário aciona a ferramenta, talvez apenas salvando o arquivo (semelhante a como gofmt/goimports normalmente funcionam) e gopls iria olhar para esta função, analisar sua assinatura de retorno e aumentar o código para ser este:

// user has triggered the tool (by saving the file, or code action)
func openFile(path string) ([]byte, error) {
  file, err := os.Open(path)
  if err != nil {
    return nil, fmt.Errorf("openFile: %w", err)
  }
  defer file.Close()
  bts, err := ioutil.ReadAll(file)
  if err != nil {
    return nil, fmt.Errorf("openFile: %w", err)
  }
  return bts, nil
}

Dessa forma, obtemos o melhor dos dois mundos: obtemos a legibilidade/explicidade do sistema de tratamento de erros atual, e o programador não escreveu nenhum clichê de tratamento de erros. Melhor ainda, o usuário pode ir em frente e modificar os blocos de tratamento de erros mais tarde para ter um comportamento diferente: gopls pode entender que o bloco existe e não o modificaria.

Como a ferramenta saberia que eu pretendia lidar com err mais tarde na função em vez de retornar mais cedo? Embora raro, mas o código que eu escrevi mesmo assim.

Peço desculpas se isso já foi mencionado antes, mas não encontrei nenhuma menção a isso.

try(DoSomething()) lê bem para mim e faz sentido: o código está tentando fazer alguma coisa. try(err) , OTOH, parece um pouco estranho, semanticamente falando: como se tenta um erro? Na minha opinião, pode-se _test_ ou _check_ um erro, mas _tentar_ não parece certo.

Eu percebo que permitir try(err) é importante por razões de consistência: suponho que seria estranho se try(DoSomething()) funcionasse, mas err := DoSomething(); try(err) não. Ainda assim, parece que try(err) parece um pouco estranho na página. Não consigo pensar em nenhuma outra função interna que possa parecer tão estranha tão facilmente.

Não tenho sugestões concretas sobre o assunto, mas mesmo assim quis fazer esta observação.

@griesemer Obrigado. Na verdade, a proposta era apenas para return , mas suspeito que permitir que qualquer instrução seja uma única linha seria bom. Por exemplo, em um teste, pode-se, sem alterações na biblioteca de testes, ter

if err != nil { t.Fatal(err) }

A primeira e a última linha parecem as mais claras (para mim), especialmente quando se está acostumado a reconhecer try como o que é. Com a última linha, um erro é explicitamente verificado, mas como (geralmente) não é a ação principal, fica um pouco mais em segundo plano.

Com a última linha, parte do custo fica oculto. Se você quiser anotar o erro, que acredito que a comunidade disse vocalmente ser a melhor prática desejada e deve ser incentivada, seria necessário alterar a assinatura da função para nomear os argumentos e esperar que um único defer fosse aplicado a cada saída no corpo da função, caso contrário try não tem valor; talvez até negativo devido à sua facilidade.

Não tenho mais nada a acrescentar que acredito que já não tenha sido dito.


Eu não vi como responder a esta pergunta do documento de design. O que faz este código:

func foo() (err error) {
    src := try(getReader())
    if src != nil {
        n, err := src.Read(nil)
        if err == io.EOF {
            return nil
        }
        try(err)
        println(n)
    }
    return nil
}

Meu entendimento é que seria desaçúcar em

func foo() (err error) {
    tsrc, te := getReader()
    if err != nil {
        err = te
        return
    }
    src := tsrc

    if src != nil {
        n, err := src.Read(nil)
        if err == io.EOF {
            return nil
        }

        terr := err
        if terr != nil {
            err = terr
            return
        }

        println(n)
    }
    return nil
}

que falha ao compilar porque err é sombreado durante um retorno nu. Isso não compilaria? Se assim for, isso é uma falha muito sutil e não parece muito improvável de acontecer. Se não, então está acontecendo mais do que um pouco de açúcar.

@marwan-at-work

Como você mencionou em seu último comentário:

A razão para esta proposta é que o tratamento de erros (especificamente o código padrão associado) foi mencionado como um problema significativo em Go (junto à falta de genéricos) pela comunidade Go

Portanto, o cerne do problema é que o programador acaba escrevendo muito código clichê. Então a questão é escrever, não ler.

Acho que na verdade é o contrário - para mim, o maior aborrecimento com o clichê atual de tratamento de erros não é tanto ter que digitá-lo, mas como ele espalha o caminho feliz da função verticalmente pela tela, dificultando a compreensão em uma olhadela. O efeito é particularmente pronunciado em código pesado de E/S, onde geralmente há um bloco de clichê entre cada duas operações. Mesmo uma versão simplista de CopyFile leva ~ 20 linhas, embora realmente execute apenas cinco etapas: código aberto, adiar código fechado, destino aberto, fonte de cópia -> destino, destino fechado.

Outro problema com a sintaxe atual é que, como observei anteriormente, se você tiver uma cadeia de operações, cada uma das quais pode retornar um erro, a sintaxe atual o forçará a dar nomes a todos os resultados intermediários, mesmo que você prefira deixe algum anônimo. Quando isso acontece, também prejudica a legibilidade porque você tem que gastar ciclos cerebrais analisando esses nomes, mesmo que eles não sejam muito informativos.

Eu gosto de try em uma linha separada.
E espero que possa especificar handler func independentemente.

func try(error, optional func(error)error)
func (p *pgStore) DoWork() error {
    tx, err := p.handle.Begin()
    try(err)

    handle := func(err error) error {
        tx.Rollback()
        return err
    }

    var res int64
    _, err = tx.QueryRow(`INSERT INTO table (...) RETURNING c1`, ...).Scan(&res)
    try(err, handle)

    _, err = tx.Exec(`INSERT INTO table2 (...) VALUES ($1)`, res)
    try(err, handle)

    return tx.Commit()
}

@zeebo : Os exemplos que dei são traduções 1:1. O primeiro (tradicional if ) não lidou com o erro, assim como os outros. Se o primeiro tratou do erro, e se este fosse o único lugar em que um erro é verificado em uma função, o primeiro exemplo (usando um if ) pode ser a escolha apropriada de escrever o código. Se houver várias verificações de erros, todas usando o mesmo tratamento de erros (agrupamento), digamos, porque todas adicionam informações sobre a função atual, pode-se usar uma instrução defer para lidar com os erros em um só lugar. Opcionalmente, pode-se reescrever os if 's em try 's (ou deixá-los em paz). Se houver vários erros para verificar, e todos eles tratarem os erros de forma diferente (o que pode ser um sinal de que a preocupação da função é muito ampla e que pode precisar ser dividida), usar if 's é o caminho a percorrer. Sim, há mais de uma maneira de fazer a mesma coisa, e a escolha certa depende do código e também do gosto pessoal. Embora nós nos esforcemos em Go por "uma maneira de fazer uma coisa", é claro que isso já não é o caso, especialmente para construções comuns. Por exemplo, quando uma sequência if - else - if se torna muito longa, às vezes uma sequência switch pode ser mais apropriada. Às vezes, uma declaração de variável var x int expressa a intenção melhor do que x := 0 e assim por diante (embora nem todos estejam felizes com isso).

Em relação à sua pergunta sobre a "reescrita": Não, não haveria erro de compilação. Observe que a reescrita acontece internamente (e pode ser mais eficiente do que o padrão de código sugere), e não há necessidade de o compilador reclamar de um retorno oculto. Em seu exemplo, você declarou uma variável local err em um escopo aninhado. try ainda teria acesso direto à variável de resultado err , é claro. A reescrita pode se parecer mais com isso nos bastidores.

[editado] PS: Uma resposta melhor seria: try não é um retorno nu (mesmo que a reescrita pareça). Afinal, dá-se explicitamente try um argumento que contém (ou é) o erro que é retornado se não for nil . O erro de sombra para retornos nus é um erro na fonte (não na tradução subjacente da fonte. O compilador não precisa do erro.

Se o tipo de retorno final da função abrangente não for do tipo erro, podemos entrar em pânico?

Isso tornará o built-in mais versátil (como satisfazer minha preocupação em # 32219)

@pjebs Isso foi considerado e decidido contra. Por favor, leia o documento de design detalhado (que se refere explicitamente ao seu problema sobre este assunto).

Eu também quero apontar que try() é tratado como expressão, embora funcione como instrução de retorno. Sim, eu sei que try é uma macro integrada, mas a maioria dos usuários usará isso como programação funcional, eu acho.

func doSomething() (error, error, error, error, error) {
   ...
}
try(try(try(try(try(doSomething)))))

O design diz que você explorou usando panic em vez de retornar com o erro.

Destaco uma sutil diferença:

Faça exatamente o que sua proposta atual afirma, exceto remover a restrição de que a função abrangente deve ter um tipo de retorno final do tipo error .

Se não tiver um tipo de retorno final error => panic
Se estiver usando try para declarações de variáveis ​​de nível de pacote => panic (remove a necessidade da convenção MustXXX( ) )

Para testes de unidade, uma modesta mudança de idioma.

@mattn , duvido muito que qualquer número significativo de pessoas escreva código assim.

@pjebs , essa semântica - pânico se não houver resultado de erro na função atual - é exatamente o que o documento de design está discutindo em https://github.com/golang/proposal/blob/master/design/32437-try-builtin. md#discussão.

Além disso, em uma tentativa de tornar try útil não apenas dentro de funções com um resultado de erro, a semântica de try dependia do contexto: se try fosse usado no nível do pacote, ou se fosse chamado dentro de uma função sem um resultado de erro, try entraria em pânico ao encontrar um erro. (Como um aparte, por causa dessa propriedade o built-in foi chamado de deve ao invés de tentar nessa proposta.) Tentar (ou deve) se comportar dessa maneira sensível ao contexto parecia natural e também bastante útil: permitiria a eliminação de muitas funções auxiliares must definidas pelo usuário atualmente usadas em expressões de inicialização de variável de nível de pacote. Também abriria a possibilidade de usar try em testes unitários através do pacote testing.

No entanto, a sensibilidade ao contexto de try foi considerada complicada: por exemplo, o comportamento de uma função contendo chamadas try poderia mudar silenciosamente (de possivelmente entrar em pânico para não entrar em pânico e vice-versa) se um resultado de erro fosse adicionado ou removido da assinatura. Esta parecia uma propriedade muito perigosa. A solução óbvia teria sido dividir a funcionalidade de try em duas funções separadas, must e try (muito semelhante ao que é sugerido pelo problema #31442). Mas isso exigiria duas novas funções internas, com apenas tentar diretamente conectado à necessidade imediata de um melhor suporte ao tratamento de erros.

@pjebs Isso é _exatamente_ o que consideramos em uma proposta anterior (consulte o documento detalhado, seção sobre iterações de design, 4º parágrafo):

Além disso, em uma tentativa de tornar try útil não apenas dentro de funções com um resultado de erro, a semântica de try dependia do contexto: se try fosse usado no nível do pacote, ou se fosse chamado dentro de uma função sem um resultado de erro, try entraria em pânico ao encontrar um erro. (Como um aparte, por causa dessa propriedade o built-in foi chamado de must em vez de tentar nessa proposta.)

O consenso (interno do Go Team) era que seria confuso para try depender do contexto e agir de forma tão diferente. Por exemplo, adicionar um resultado de erro a uma função (ou removê-lo) pode alterar silenciosamente o comportamento da função de entrar em pânico para não entrar em pânico (ou vice-versa).

@griesemer Obrigado pelo esclarecimento sobre a reescrita. Estou feliz que ele irá compilar.

Eu entendo que os exemplos foram traduções que não anotaram os erros. Tentei argumentar que try torna mais difícil fazer uma boa anotação de erros em situações comuns, e que a anotação de erros é muito importante para a comunidade. Uma grande parte dos comentários até agora tem explorado maneiras de adicionar melhor suporte a anotações para try .

Sobre ter que lidar com os erros de forma diferente, discordo que seja um sinal de que a preocupação da função é muito ampla. Estou traduzindo alguns exemplos de código real reivindicado dos comentários e colocando-os em uma lista suspensa na parte inferior do meu comentário original e o exemplo em https://github.com/golang/go/issues/32437#issuecomment - 499007288 acho que demonstra bem um caso comum:

func (c *Config) Build() error {
    pkgPath, err := c.load()
    if err != nil { return nil, errors.WithMessage(err, "load config dir") }

    b := bytes.NewBuffer(nil)
    err = templates.ExecuteTemplate(b, "main", c)
    if err != nil { return nil, errors.WithMessage(err, "execute main template") }

    buf, err := format.Source(b.Bytes())
    if err != nil { return nil, errors.WithMessage(err, "format main template") }

    target := fmt.Sprintf("%s.go", filename(pkgPath))
    err = ioutil.WriteFile(target, buf, 0644)
    if err != nil { return nil, errors.WithMessagef(err, "write file %s", target) }
    // ...
}

O objetivo dessa função é executar um modelo em alguns dados em um arquivo. Não acredito que precise ser dividido, e seria lamentável se todos esses erros ganhassem a linha em que foram criados a partir de um adiamento. Isso pode ser bom para os desenvolvedores, mas é muito menos útil para os usuários.

Eu acho que também é um sinal de quão sutis os bugs defer wrap(&err, "message: %v", err) eram e como eles tropeçavam até mesmo programadores Go experientes.


Para resumir meu argumento : acho que a anotação de erro é mais importante do que a verificação de erros baseada em expressões, e podemos obter um pouco de redução de ruído permitindo que a verificação de erros baseada em instruções seja uma linha em vez de três. Obrigado.

@griesemer desculpe, li uma seção diferente que discutia o pânico e não via a discussão dos perigos.

@zeebo Obrigado por este exemplo. Parece que usar uma instrução if é exatamente a escolha certa neste caso. Mas ponto, formatar os if's em one-liners pode simplificar um pouco isso.

Eu gostaria de trazer mais uma vez a idéia de um manipulador como um segundo argumento para try , mas com a adição de que o argumento do manipulador seja _required_, mas nil-able. Isso torna o tratamento do erro o padrão, em vez da exceção. Nos casos em que você realmente deseja passar o erro inalterado, basta fornecer um valor nil ao manipulador e try se comportará exatamente como na proposta original, mas o argumento nil atuará como uma indicação visual de que o erro não está sendo tratado. Será mais fácil detectar durante a revisão do código.

file := try(os.Open("my_file.txt"), nil)

O que deve acontecer se o manipulador for fornecido, mas for nulo? Deve tentar entrar em pânico ou tratá-lo como um manipulador de erros ausente?

Conforme mencionado acima, try se comportará de acordo com a proposta original. Não haveria um manipulador de erros ausente, apenas um nulo.

E se o manipulador for invocado com um erro não nulo e, em seguida, retornar um resultado nulo? Isso significa que o erro foi “cancelado”? Ou a função envolvente deve retornar com um erro nil?

Acredito que a função envolvente retornaria com um erro nil. Seria potencialmente muito confuso se try às vezes pudesse continuar a execução mesmo depois de receber um valor de erro não nulo. Isso permitiria que os manipuladores "cuidassem" do erro em algumas circunstâncias. Esse comportamento pode ser útil em uma função de estilo "obter ou criar", por exemplo.

func getOrCreateObject(obj *object) error {
    defaultObjectHandler := func(err error) error {
        if err == ObjectDoesNotExistErr {
            *obj = object{}
            return nil
        }
        return fmt.Errorf("getting or creating object: %v", err)
    }

    *obj = try(db.getObject(), defaultObjectHandler)
}

Também não ficou claro se permitir um manipulador de erros opcional levaria os programadores a ignorar completamente o tratamento de erros adequado. Também seria fácil fazer o tratamento adequado de erros em todos os lugares, mas perder uma única ocorrência de uma tentativa. E assim por diante.

Acredito que essas duas preocupações são aliviadas ao tornar o manipulador um argumento obrigatório e nulo. Requer que os programadores tomem uma decisão consciente e explícita de que não irão lidar com seu erro.

Como bônus, acho que exigir o manipulador de erros também desencoraja try s profundamente aninhados porque eles são menos breves. Alguns podem ver isso como uma desvantagem, mas acho que é um benefício.

@velovix Eu amo a ideia, mas por que o manipulador de erros precisa ser necessário? Não pode ser nil por padrão? Por que precisamos de uma "pista visual"?

@griesemer E se a ideia @velovix fosse adotada, mas com builtin contendo uma função predefinida que converte err em panic E removemos o requisito de que a função over-arching tenha um valor de retorno de erro?

A idéia é, se a função abrangente não retornar erro, usar try sem manipulador de erros é um erro de tempo de compilação.

O manipulador de erros também pode ser usado para agrupar o erro que será retornado em breve usando várias bibliotecas etc no local do erro, em vez de um defer no topo que modifica um erro retornado nomeado.

@pjebs

por que o manipulador de erros precisa ser necessário? Não pode ser nulo por padrão? Por que precisamos de uma "pista visual"?

Isto é para responder às preocupações que

  1. A proposta try como está agora pode desencorajar as pessoas de fornecer contexto para seus erros porque isso não é tão simples.

Ter um manipulador em primeiro lugar facilita o fornecimento de contexto, e ter o manipulador como um argumento obrigatório envia uma mensagem: O caso comum e recomendado é manipular ou contextualizar o erro de alguma forma, não simplesmente passá-lo pela pilha. Está de acordo com a recomendação geral da comunidade Go.

  1. Uma preocupação do documento da proposta original. Citei no meu primeiro comentário:

Também não ficou claro se permitir um manipulador de erros opcional levaria os programadores a ignorar completamente o tratamento de erros adequado. Também seria fácil fazer o tratamento adequado de erros em todos os lugares, mas perder uma única ocorrência de uma tentativa. E assim por diante.

Ter que passar um nil explícito torna mais difícil esquecer de lidar com um erro corretamente. Você precisa decidir explicitamente não manipular o erro em vez de fazê-lo implicitamente, deixando de fora um argumento.

Pensando mais sobre o retorno condicional mencionado brevemente em https://github.com/golang/go/issues/32437#issuecomment -498947603.
Parece
return if f, err := os.Open("/my/file/path"); err != nil
seria mais compatível com a aparência do if existente em Go.

Se adicionarmos uma regra para a instrução return if que
quando a última expressão de condição (como err != nil ) não está presente,e a última variável da declaração na instrução return if é do tipo error ,então o valor da última variável será automaticamente comparado com nil como condição implícita.

Então a instrução return if pode ser abreviada em:
return if f, err := os.Open("my/file/path")

O que é muito próximo da relação sinal-ruído que o try fornece.
Se mudarmos return if para try , fica
try f, err := os.Open("my/file/path")
Novamente, torna-se semelhante a outras variações propostas do try neste tópico, pelo menos sintaticamente.
Pessoalmente, ainda prefiro return if sobre try neste caso porque torna os pontos de saída de uma função muito explícitos. Por exemplo, ao depurar, geralmente destaco a palavra-chave return no editor para identificar todos os pontos de saída de uma função grande.

Infelizmente, também não parece ajudar o suficiente com a inconveniência de inserir o log de depuração.
A menos que também permitamos um bloco body para return if , como
Original:

        return if f, err := os.Open("my/path") 

Ao depurar:

-       return if f, err := os.Open("my/path") 
+       return if f, err := os.Open("my/path") {
+               fmt.Printf("DEBUG: os.Open: %s\n", err)
+       }

O significado do bloco de corpo de return if é óbvio, suponho. Ele será executado antes defer e retornará.

Dito isso, não tenho reclamações com a abordagem de tratamento de erros existente no Go.
Estou mais preocupado em como a adição do novo tratamento de erros afetaria a qualidade atual do Go.

@velovix Gostamos bastante da ideia de um try com uma função de manipulador explícita como segundo argumento. Mas havia muitas perguntas que não tinham respostas óbvias, como afirma o documento de design. Você respondeu algumas delas de uma forma que lhe parece razoável. É bem provável (e essa foi a nossa experiência dentro da Go Team) que alguém ache que a resposta correta é bem diferente. Por exemplo, você está afirmando que o argumento do manipulador sempre deve ser fornecido, mas que pode ser nil , para torná-lo explícito, não nos importamos em lidar com o erro. Agora, o que acontece se alguém fornecer um valor de função (não um literal nil ), e esse valor de função (armazenado em uma variável) for nulo? Por analogia com o valor explícito nil , nenhum manuseio é necessário. Mas outros podem argumentar que isso é um bug no código. Ou, alternativamente, pode-se permitir argumentos de manipulador de valor nulo, mas uma função pode manipular erros inconsistentemente em alguns casos e não em outros, e não é necessariamente óbvio no código o que se faz, porque parece que um manipulador está sempre presente . Outro argumento era que é melhor ter uma declaração de nível superior de um manipulador de erros porque isso deixa muito claro que a função trata os erros. Daí o defer . Provavelmente há mais.

Seria bom saber mais sobre essa preocupação. O estilo de codificação atual usando instruções if para testar erros é o mais explícito possível. É muito fácil adicionar informações adicionais a um erro, individualmente (para cada if). Muitas vezes faz sentido tratar todos os erros detectados em uma função de forma uniforme, o que pode ser feito com um adiamento - isso já é possível agora. É o fato de já termos todas as ferramentas para um bom tratamento de erros na linguagem, e o problema de uma construção de manipulador não ser ortogonal para diferir, que nos levou a deixar de lado um novo mecanismo exclusivamente para aumentar erros.

@griesemer - IIUC, você está dizendo que, para contextos de erro dependentes de callsite, a instrução if atual está bem. Considerando que, esta nova função try é útil para os casos em que o tratamento de vários erros em um único local é útil.

Acredito que a preocupação era que, enquanto simplesmente fazer um if err != nil { return err} pode ser bom para alguns casos, geralmente é recomendado decorar o erro antes de retornar. E esta proposta parece abordar o anterior e não faz muito para o último. O que essencialmente significa que as pessoas serão incentivadas a usar o padrão de retorno fácil.

@agnivade Você está correto, esta proposta não faz exatamente nada para ajudar na decoração de erros (mas para recomendar o uso de defer ). Uma razão é que já existem mecanismos de linguagem para isso. Assim que a decoração de erro é necessária, especialmente em uma base de erro individual, a quantidade adicional de texto fonte para o código de decoração torna o if menos oneroso em comparação. São os casos em que nenhuma decoração é necessária, ou onde a decoração é sempre a mesma, onde o clichê se torna um incômodo visível e depois diminui o código importante.

As pessoas já são encorajadas a usar um padrão de retorno fácil, try ou não try , há apenas menos para escrever. Pensando bem, _a única maneira de incentivar a decoração de erros é torná-la obrigatória_, porque não importa qual suporte de idioma esteja disponível, erros de decoração exigirão mais trabalho.

Uma maneira de adoçar o negócio seria permitir apenas algo como try (ou qualquer notação de atalho análoga) _se_ um manipulador explícito (possivelmente vazio) for fornecido em algum lugar (observe que o projeto original do rascunho não tinha tal exigência, também).

Não tenho certeza se queremos ir tão longe. Deixe-me reafirmar que muitos códigos perfeitamente finos, digamos, internos de uma biblioteca, não precisam decorar erros em todos os lugares. Não há problema em propagar erros e decorá-los antes que eles saiam dos pontos de entrada da API, por exemplo. (Na verdade, decorá-los em todos os lugares só levará a erros superdecorados que, com os verdadeiros culpados ocultos, dificultam a localização dos erros importantes; assim como o log excessivamente detalhado pode dificultar a visualização do que realmente está acontecendo).

Acho que também podemos adicionar uma função catch , que seria um bom par, então:

func a() int {
  x := randInt()
  // let's assume that this is what recruiters should "fix" for us
  // or this happens in 3rd-party package.
  if x % 1337 != 0 {
    panic("not l33t enough")
  }
  return x
}

func b() error {
  // if a() panics, then x = 0, err = error{"not l33t enough"}
  x, err := catch(a())
  if err != nil {
    return err
  }
  sendSomewhereElse(x)
  return nil
}

// which could be simplified even further

func c() error {
  x := try(catch(a()))
  sendSomewhereElse(x)
  return nil
}

neste exemplo, catch() seria recover() um pânico e return ..., panicValue .
é claro, temos um caso de canto óbvio no qual temos um func, que também retorna um erro. neste caso, acho que seria conveniente apenas passar o valor do erro.

então, basicamente, você pode usar catch() para realmente recuperar() panics e transformá-los em erros.
isso parece muito engraçado para mim, porque Go na verdade não tem exceções, mas neste caso temos um padrão try()-catch() bem legal, que também não deve explodir toda a sua base de código com algo como Java ( catch(Throwable) em Principal + throws LiterallyAnything ). você pode facilmente processar o pânico de alguém como se fossem erros comuns. Atualmente, tenho cerca de 6 milhões de LoC em Go no meu projeto atual, e acho que isso simplificaria as coisas, pelo menos para mim.

@griesemer Obrigado por sua recapitulação da discussão.

Percebo que está faltando um ponto: algumas pessoas argumentaram que deveríamos esperar com esse recurso até termos genéricos, o que esperamos nos permitir resolver esse problema de uma maneira mais elegante.

Além disso, também gosto da sugestão do @velovix e, embora aprecie que isso levante algumas questões, conforme descrito nas especificações, acho que elas podem ser facilmente respondidas de maneira razoável, como o @velovix já fez.

Por exemplo:

  • O que acontece se alguém fornecer um valor de função (não um literal nil) e esse valor de função (armazenado em uma variável) for nil? => Não trate o erro, ponto final. Isso é útil caso o tratamento de erros dependa do contexto e a variável do manipulador seja definida dependendo se o tratamento de erros é necessário ou não. Não é um bug, é um recurso. :)

  • Outro argumento era que é melhor ter uma declaração de nível superior de um manipulador de erros porque isso deixa muito claro que a função trata os erros. => Então defina o manipulador de erro no topo da função como uma função de fechamento nomeada e use isso, então também fica muito claro que o erro deve ser tratado. Este não é um problema sério, é mais um requisito de estilo.

Que outras preocupações existiam? Tenho certeza de que todas elas podem ser respondidas da mesma forma de maneira razoável.

Finalmente, como você diz, "uma maneira de adoçar o negócio seria apenas permitir algo como try (ou qualquer notação de atalho análoga) se um manipulador explícito (possivelmente vazio) for fornecido em algum lugar". Eu acho que se vamos prosseguir com esta proposta, devemos realmente levá-la "até aqui", para encorajar o tratamento adequado de erros "explícito é melhor que implícito".

@griesemer

Agora, o que acontece se alguém fornecer um valor de função (não um literal nil) e esse valor de função (armazenado em uma variável) for nil? Por analogia com o valor nil explícito, nenhum manuseio é necessário. Mas outros podem argumentar que isso é um bug no código.

Em teoria, isso parece uma pegadinha em potencial, embora eu esteja tendo dificuldade em conceituar uma situação razoável em que um manipulador acabaria sendo nulo por acidente. Imagino que os manipuladores geralmente venham de uma função de utilidade definida em outro lugar ou como um fechamento definido na própria função. Nenhum destes provavelmente se tornará nulo inesperadamente. Você poderia teoricamente ter um cenário em que as funções do manipulador estão sendo passadas como argumentos para outras funções, mas aos meus olhos parece um pouco exagerado. Talvez haja um padrão como este que eu não conheço.

Outro argumento era que é melhor ter uma declaração de nível superior de um manipulador de erros porque isso deixa muito claro que a função trata os erros. Daí o defer .

Como @beoran mencionou, definir o manipulador como um encerramento próximo ao topo da função seria muito semelhante em estilo, e é assim que eu pessoalmente espero que as pessoas usem manipuladores com mais frequência. Embora eu aprecie a clareza conquistada pelo fato de que todas as funções que lidam com erros usarão defer , pode ficar menos claro quando uma função precisa dinamizar sua estratégia de tratamento de erros na metade da função. Então, haverá dois defer s para olhar e o leitor terá que raciocinar sobre como eles irão interagir uns com os outros. Esta é uma situação em que acredito que um argumento de manipulador seria mais claro e ergonômico, e acho que esse será um cenário _relativamente_ comum.

É possível fazê-lo funcionar sem colchetes?

Ou seja, algo como:
a := try func(some)

@Cyberax - Como já mencionado acima, é muito essencial que você leia o documento de design cuidadosamente antes de postar. Uma vez que este é um problema de alto tráfego, com muitas pessoas inscritas.

O documento discute operadores versus funções em detalhes.

Gosto muito mais desta versão do que gostei da versão de agosto.

Acho que muito do feedback negativo, que não é totalmente contrário aos retornos sem a palavra-chave return , pode ser resumido em dois pontos:

  1. as pessoas não gostam de parâmetros de resultado nomeados, que se tornariam obrigatórios na maioria dos casos
  2. desencoraja a adição de contexto detalhado aos erros

Veja por exemplo:

A refutação para essas duas objeções é, respectivamente:

  1. "decidimos que [parâmetros de resultado nomeados] estavam ok"
  2. "Ninguém vai fazer você usar try " / não será apropriado para 100% dos casos

Eu realmente não tenho nada a dizer sobre 1 (eu não sinto muito sobre isso). Mas em relação ao 2 eu notaria que a proposta de agosto não teve esse problema, a maioria das contrapropostas também não tem esse problema.

Em particular, nem a contraproposta tryf (que foi postada independentemente duas vezes neste tópico) nem a contraproposta try(X, handlefn) (que fazia parte das iterações de design) tiveram esse problema.

Eu acho que é difícil argumentar que try , como está, afastará as pessoas da decoração de erros com contexto relevante e em direção a uma única decoração de erro genérica por função.

Por esses motivos, acho que vale a pena tentar resolver esse problema e quero propor uma possível solução:

  1. Atualmente o parâmetro defer só pode ser uma função ou chamada de método. Permitir que defer também tenha um nome de função ou um literal de função, ou seja
defer func(...) {...}
defer packageName.functionName
  1. Quando panic ou deferreturn encontrarem este tipo de defer eles irão chamar a função passando o valor zero para todos os seus parâmetros

  2. Permitir que try tenha mais de um parâmetro

  3. Quando try encontrar o novo tipo de defer, ele chamará a função passando um ponteiro para o valor do erro como o primeiro parâmetro seguido por todos os próprios parâmetros de try , exceto o primeiro.

Por exemplo, dado:

func errorfn() error {
    return errors.New("an error")
}


func f(fail bool) {
    defer func(err *error, a, b, c int) {
        fmt.Printf("a=%d b=%d c=%d\n", a, b, c)
    }
    if fail {
        try(errorfn, 1, 2, 3)
    }
}

acontecerá o seguinte:

f(false)        // prints "a=0 b=0 c=0"
f(true)         // prints "a=1 b=2 c=3"

O código em https://github.com/golang/go/issues/32437#issuecomment -499309304 por @zeebo poderia ser reescrito como:

func (c *Config) Build() error {
    defer func(err *error, msg string, args ...interface{}) {
        if *err == nil || msg == "" {
            return
        }
        *err = errors.WithMessagef(err, msg, args...)
    }
    pkgPath := try(c.load(), "load config dir")

    b := bytes.NewBuffer(nil)
    try(templates.ExecuteTemplate(b, "main", c), "execute main template")

    buf := try(format.Source(b.Bytes()), "format main template")

    target := fmt.Sprintf("%s.go", filename(pkgPath))
    try(ioutil.WriteFile(target, buf, 0644), "write file %s", target)
    // ...
}

E definindo ErrorHandlef como:

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

daria a todos os tão procurados tryf de graça, sem puxar as strings de formato fmt -style para o idioma principal.

Este recurso é compatível com versões anteriores porque defer não permite expressões de função como seu argumento. Não introduz novas palavras-chave.
As alterações que precisam ser feitas para implementá-lo, além das descritas em https://github.com/golang/proposal/blob/master/design/32437-try-builtin.md , são:

  1. ensinar o analisador sobre o novo tipo de adiamento
  2. altere o verificador de tipos para verificar se dentro de uma função todos os defers que possuem uma função como parâmetro (ao invés de uma chamada) também possuem a mesma assinatura
  3. altere o verificador de tipos para verificar se os parâmetros passados ​​para try correspondem à assinatura das funções passadas para defer
  4. altere o back-end (?) para gerar a chamada deferproc apropriada
  5. altere a implementação de try para copiar seus argumentos nos argumentos da chamada adiada quando encontrar uma chamada adiada pelo novo tipo de adiamento.

Após as complexidades do projeto check/handle , fiquei agradavelmente surpreso ao ver essa proposta muito mais simples e pragmática chegar, embora esteja desapontado por ter havido tanto retrocesso contra ela.

Reconhecidamente, muito do contra-ataque vem de pessoas que estão bastante satisfeitas com a verbosidade atual (uma posição perfeitamente razoável a ser tomada) e que, presumivelmente, não gostariam de receber qualquer proposta para aliviá-la. Para o resto de nós, acho que esta proposta atinge o ponto ideal de ser simples e semelhante ao Go, não tentar fazer muito e se encaixar bem com as técnicas de tratamento de erros existentes nas quais você sempre pode recorrer se try não fez exatamente o que você queria.

Sobre alguns pontos específicos:

  1. A única coisa que não gosto na proposta é a necessidade de ter um parâmetro de retorno de erro nomeado quando defer é usado, mas, dito isso, não consigo pensar em nenhuma outra solução que não esteja em desacordo com como o resto da linguagem funciona. Então, acho que teremos que aceitar isso se a proposta for aprovada.

  2. É uma pena que try não funcione bem com o pacote de teste para funções que não retornam um valor de erro. Minha própria solução preferida para isso seria ter uma segunda função interna (talvez ptry ou must ) que sempre entrasse em pânico em vez de retornar ao encontrar um erro não nulo e que poderia, portanto, ser usado com as funções acima mencionadas (incluindo main ). Embora essa ideia tenha sido rejeitada na presente iteração da proposta, tive a impressão de que era uma 'chamada de perto' e, portanto, pode ser elegível para reconsideração.

  3. Eu acho que seria difícil para as pessoas entenderem o que go try(f) ou defer try(f) estavam fazendo e que é melhor, portanto, proibi-los completamente.

  4. Eu concordo com aqueles que pensam que as técnicas de tratamento de erros existentes pareceriam menos detalhadas se go fmt não reescrevesse instruções if uma única linha. Pessoalmente, eu preferiria uma regra simples de que isso seria permitido para _qualquer_ instrução única if independentemente de se preocupar com o tratamento de erros ou não. Na verdade, nunca consegui entender por que isso não é permitido atualmente ao escrever funções de linha única em que o corpo é colocado na mesma linha em que a declaração é permitida.

No caso de erros de decoração

func myfunc()( err error){
try(thing())
defer func(){
err = errors.Wrap(err,"more context")
}()
}

Isso parece consideravelmente mais detalhado e doloroso do que os paradigmas existentes, e não tão conciso quanto check/handle. A variante try() sem encapsulamento é mais concisa, mas parece que as pessoas acabarão usando uma mistura de try e retornos de erro simples. Não tenho certeza se gosto da ideia de misturar tentativas e retornos de erros simples, mas estou totalmente convencido de erros de decoração (e ansioso por Is/As). Faça-me pensar que, embora isso seja sintaticamente legal, não tenho certeza se realmente gostaria de usá-lo. check/handle senti algo que eu abraçaria mais profundamente.

Eu realmente gosto da simplicidade disso e da abordagem "faça uma coisa bem". No meu interpretador GoAWK , seria muito útil - eu tenho cerca de 100 if err != nil { return nil } construções que simplificariam e organizariam, e isso está em uma base de código bastante pequena.

Eu li a justificativa da proposta para torná-la um built-in em vez de uma palavra-chave, e tudo se resume a não ter que ajustar o analisador. Mas isso não é uma quantidade relativamente pequena de dor para compiladores e criadores de ferramentas, enquanto ter os parênteses extras e os problemas de legibilidade de isso parece uma função, mas não é algo que todos os codificadores e códigos Go leitores têm que suportar. Na minha opinião, o argumento (desculpa? :-) de que "mas panic() controla o fluxo" não o corta, porque pânico e recuperação são por sua própria natureza, excepcionais , enquanto try() ser tratamento de erro normal e fluxo de controle.

Eu definitivamente apreciaria mesmo que isso acontecesse como está, mas minha forte preferência seria que o fluxo de controle normal fosse claro, ou seja, feito por meio de uma palavra-chave.

Sou a favor desta proposta. Evita minha maior ressalva sobre a proposta anterior: a não ortogonalidade de handle em relação a defer .

Gostaria de mencionar dois aspectos que acho que não foram destacados acima.

Em primeiro lugar, embora esta proposta não facilite a adição de texto de erro específico do contexto a um erro, ela _facilita_ a adição de informações de rastreamento de erro de quadro de pilha a um erro: https://play.golang.org/p /YL1MoqR08E6

Em segundo lugar, try é sem dúvida uma solução justa para a maioria dos problemas subjacentes https://github.com/golang/go/issues/19642. Para dar um exemplo desse problema, você pode usar try para evitar escrever todos os valores de retorno a cada vez. Isso também é potencialmente útil ao retornar tipos de estrutura por valor com nomes longos.

func (f *Font) viewGlyphData(b *Buffer, x GlyphIndex) (buf []byte, offset, length uint32, err error) {
    xx := int(x)
    if f.NumGlyphs() <= xx {
        try(ErrNotFound)
    }
    i := f.cached.locations[xx+0]
    j := f.cached.locations[xx+1]
    if j < i {
        try(errInvalidGlyphDataLength)
    }
    if j-i > maxGlyphDataLength {
        try(errUnsupportedGlyphDataLength)
    }
    buf, err = b.view(&f.src, int(i), int(j-i))
    return buf, i, j - i, err
}

Também gosto desta proposta.

E eu tenho um pedido.

Como make , podemos permitir que try receba um número variável de parâmetros

  • tente(f):
    como acima.
    um valor de erro de retorno é obrigatório (como o último parâmetro de retorno).
    MODELO DE USO MAIS COMUM
  • try(f, doPanic bool):
    como acima, mas se doPanic, então panic (err) em vez de retornar.
    Neste modo, um valor de erro de retorno não é necessário.
  • tente(f, f):
    como acima, mas chame fn(err) antes de retornar.
    Neste modo, um valor de erro de retorno não é necessário.

Dessa forma, é um builtin que pode lidar com todos os casos de uso, enquanto ainda é explícito. Suas vantagens:

  • sempre explícito - não há necessidade de inferir se entrar em pânico ou definir erro e retornar
  • suporta manipulador específico de contexto (mas nenhuma cadeia de manipulador)
  • suporta casos de uso onde não há variável de retorno de erro
  • suporta a semântica must(...)

Enquanto if err !=nil { return ... err } repetitivo é certamente uma gagueira feia, eu estou com aqueles
que pensam que a proposta try() é muito baixa em legibilidade e um tanto inexplícita.
O uso de retornos nomeados também é problemático.

Se esse tipo de arrumação é necessário, por que não try(err) como açúcar sintático para
if err !=nil { return err } :

file, err := os.Open("file.go")
try(err)

para

file, err := os.Open("file.go")
if err != nil {
   return err
}

E se houver mais de um valor de retorno, try(err) poderia return t1, ... tn, err
onde t1, ... tn são os valores zero dos outros valores de retorno.

Essa sugestão pode evitar a necessidade de valores de retorno nomeados e ser,
a meu ver, mais fácil de entender e mais legível.

Melhor ainda, acho que seria:

file, try(err) := os.Open("file.go")

Ou mesmo

file, err? := os.Open("file.go")

Este último é compatível com versões anteriores (? atualmente não é permitido em identificadores).

(Esta sugestão está relacionada a https://github.com/golang/go/wiki/Go2ErrorHandlingFeedback#recurring-themes. Mas os exemplos de temas recorrentes parecem diferentes porque isso estava em um estágio em que um identificador explícito ainda estava sendo discutido em vez de sair isso para um adiamento.)

Obrigado à equipe go por esta proposta cuidadosa e interessante.

@rogpeppe comenta se try adiciona automaticamente o quadro de pilha, não eu, estou bem com isso desencorajando a adição de contexto.

@aarzilli - Então, de acordo com sua proposta, uma cláusula de defer é obrigatória toda vez que damos parâmetros extras para tryf ?

O que acontece se eu fizer

try(ioutil.WriteFile(target, buf, 0644), "write file %s", target)

e não escreva uma função defer?

@agnivade

O que acontece se eu fizer (...) e não escrever uma função defer?

erro de verificação de tipo.

Na minha opinião, usar try para evitar escrever todos os valores de retorno é na verdade apenas mais um golpe contra ele.

func (f *Font) viewGlyphData(b *Buffer, x GlyphIndex) (buf []byte, offset, length uint32, err error) {
    xx := int(x)
    if f.NumGlyphs() <= xx {
        try(ErrNotFound)
    }
    //...

Eu entendo completamente o desejo de evitar ter que escrever return nil, 0, 0, ErrNotFound , mas prefiro resolver isso de outra maneira.

A palavra try não significa "retornar". E é assim que está sendo usado aqui. Na verdade, eu preferiria que a proposta mudasse para que try não pudesse receber um error diretamente, porque eu nunca quero que ninguém escreva código assim ^^ . Ele lê errado . Se você mostrasse esse código para um novato, eles não teriam ideia do que essa tentativa estava fazendo.

Se quisermos uma maneira de retornar facilmente os padrões e um valor de erro, vamos resolver isso separadamente. Talvez outro builtin como

return default(ErrNotFound)

Pelo menos isso se lê com algum tipo de lógica.

Mas não vamos abusar de try para resolver algum outro problema.

@natefinch se o try embutido for nomeado check como na proposta original, seria check(err) que é consideravelmente melhor, imo.

Deixando isso de lado, não sei se é realmente um abuso escrever try(err) . Ele cai fora da definição de forma limpa. Mas, por outro lado, isso também significa que isso é legal:

a, b := try(1, f(), err)

Acho que meu principal problema com try é que é realmente apenas um panic que só sobe um nível... exceto que, diferentemente do pânico, é uma expressão, não uma declaração, então você pode esconder no meio de uma declaração em algum lugar. Isso quase o torna pior do que o pânico.

@natefinch Se você conceitua isso como um pânico que sobe um nível e depois faz outras coisas, parece muito confuso. No entanto, eu conceituo de forma diferente. Funções que retornam erros em Go estão efetivamente retornando um Result, para emprestar vagamente da terminologia de Rust. try é um utilitário que descompacta o resultado e retorna um "resultado de erro" se error != nil ou descompacta a parte T do resultado se error == nil .

Claro, em Go não temos objetos de resultado, mas é efetivamente o mesmo padrão e try parece uma codificação natural desse padrão. Acredito que qualquer solução para este problema terá que codificar algum aspecto do tratamento de erros, e try s assumir isso me parece razoável. Eu e outros estamos sugerindo estender um pouco a capacidade de try para melhor se adequar aos padrões de tratamento de erros Go existentes, mas o conceito subjacente permanece o mesmo.

@ugorji A variante try(f, bool) que você propõe soa como a must de #32219.

@ugorji A variante try(f, bool) que você propõe soa como a must de #32219.

Sim, ele é. Eu apenas senti que todos os 3 casos poderiam ser tratados com uma função interna singular e satisfazer todos os casos de uso elegantemente.

Como try() já é mágico, e ciente do valor de retorno do erro, poderia ser aumentado para também retornar um ponteiro para esse valor quando chamado na forma nula (argumento zero)? Isso eliminaria a necessidade de retornos nomeados e, acredito, ajudaria a correlacionar visualmente de onde se espera que o erro venha nas instruções de defer. Por exemplo:

func foo() error {
  defer fmt.HandleErrorf(try(), "important foo context info")
  try(bar())
  try(baz())
  try(etc())
}

@ugorji
Eu acho que o booleano em try(f, bool) tornaria difícil de ler e fácil de perder. Eu gosto da sua proposta, mas para o caso de pânico, acho que isso poderia ser deixado de fora para os usuários escreverem isso dentro do manipulador do seu terceiro marcador, por exemplo, try(f(), func(err error) { panic('at the disco'); }) , isso o torna mais explícito para os usuários do que um try(f(), true) oculto

@ugorji
Eu acho que o booleano em try(f, bool) tornaria difícil de ler e fácil de perder. Eu gosto da sua proposta, mas para o caso de pânico, acho que isso poderia ser deixado de fora para os usuários escreverem isso dentro do manipulador do seu terceiro marcador, por exemplo, try(f(), func(err error) { panic('at the disco'); }) , isso o torna mais explícito para os usuários do que um try(f(), true) oculto

Pensando melhor, tendo a concordar com sua posição e seu raciocínio, e ainda parece elegante como uma frase de efeito.

@patrick-nyt ainda é outro proponente da _sintaxe de atribuição_ para acionar um teste nil, em https://github.com/golang/go/issues/32437#issuecomment -499533464

Este conceito aparece em 13 respostas separadas à proposta de cheque/manuseio
https://github.com/golang/go/wiki/Go2ErrorHandlingFeedback#recurring -themes

f, ?return := os.Open(...)
f, ?panic  := os.Open(...)

Por quê? Porque se lê como Go 1, enquanto try() e check não.

Uma objeção a try parece ser que é uma expressão. Suponha, em vez disso, que haja uma instrução postfix unária ? que significa retorno se não for nulo. Aqui está o exemplo de código padrão (supondo que meu pacote adiado proposto seja adicionado):

func CopyFile(src, dst string) error {
    var err error // Don't need a named return because err is explicitly named
    defer deferred.Annotate(&err, "copy %s %s", src, dst)

    r, err := os.Open(src)
    err?
    defer deferred.AnnotatedExec(&err, r.Close)

    w, err := os.Create(dst)
    err?
    defer deferred.AnnotatedExec(&err, r.Close)

    defer deferred.Cond(&err, func(){ os.Remove(dst) })
    _, err = io.Copy(w, r)

    return err
}

O exemplo pgStore:

func (p *pgStore) DoWork() error {
    tx, err := p.handle.Begin()
    err?

    defer deferred.Cond(&err, func(){ tx.Rollback() })

    var res int64 
    err = tx.QueryRow(`INSERT INTO table (...) RETURNING c1`, ...).Scan(&res)
    // tricky bit: this would not change the value of err 
    // but the deferred.Cond would still be triggered by err being set before
    deferred.Format(err, "insert table")?

    _, err = tx.Exec(`INSERT INTO table2 (...) VALUES ($1)`, res)
    deferred.Format(err, "insert table2")?

    return tx.Commit()
}

Eu gosto disso do @jargv :

Como o try() já é mágico e está ciente do valor de retorno do erro, ele poderia ser aumentado para também retornar um ponteiro para esse valor quando chamado na forma nula (argumento zero)? Isso eliminaria a necessidade de retornos nomeados

Mas em vez de sobrecarregar o nome try com base no número de argumentos, acho que poderia haver outra mágica incorporada, digamos reterr ou algo assim.

Eu resumi alguns pacotes muito usados, procurando por código go que "sofre" de manipulação de erros, mas deve ter sido bem pensado antes de ser escrito, tentando descobrir que "mágica" o try() proposto faria.
Atualmente, a menos que eu tenha entendido mal a proposta, muitos deles (por exemplo, tratamento de erros não super básico) não ganhariam muito, ou teriam que ficar com o estilo "antigo" de tratamento de erros.
Exemplo de net/http/request.go:

func (r *Request) write(w io.Writer, usingProxy bool, extraHeaders Header, waitForContinue func() bool) (err error) {
`

trace := httptrace.ContextClientTrace(r.Context())
if trace != nil && trace.WroteRequest != nil {
    defer func() {
        trace.WroteRequest(httptrace.WroteRequestInfo{
            Err: err,
        })
    }()
}

// Find the target host. Prefer the Host: header, but if that
// is not given, use the host from the request URL.
//
// Clean the host, in case it arrives with unexpected stuff in it.
host := cleanHost(r.Host)
if host == "" {
    if r.URL == nil {
        return errMissingHost
    }
    host = cleanHost(r.URL.Host)
}

// According to RFC 6874, an HTTP client, proxy, or other
// intermediary must remove any IPv6 zone identifier attached
// to an outgoing URI.
host = removeZone(host)

ruri := r.URL.RequestURI()
if usingProxy && r.URL.Scheme != "" && r.URL.Opaque == "" {
    ruri = r.URL.Scheme + "://" + host + ruri
} else if r.Method == "CONNECT" && r.URL.Path == "" {
    // CONNECT requests normally give just the host and port, not a full URL.
    ruri = host
    if r.URL.Opaque != "" {
        ruri = r.URL.Opaque
    }
}
if stringContainsCTLByte(ruri) {
    return errors.New("net/http: can't write control character in Request.URL")
}
// TODO: validate r.Method too? At least it's less likely to
// come from an attacker (more likely to be a constant in
// code).

// Wrap the writer in a bufio Writer if it's not already buffered.
// Don't always call NewWriter, as that forces a bytes.Buffer
// and other small bufio Writers to have a minimum 4k buffer
// size.
var bw *bufio.Writer
if _, ok := w.(io.ByteWriter); !ok {
    bw = bufio.NewWriter(w)
    w = bw
}

_, err = fmt.Fprintf(w, "%s %s HTTP/1.1\r\n", valueOrDefault(r.Method, "GET"), ruri)
if err != nil {
    return err
}

// Header lines
_, err = fmt.Fprintf(w, "Host: %s\r\n", host)
if err != nil {
    return err
}
if trace != nil && trace.WroteHeaderField != nil {
    trace.WroteHeaderField("Host", []string{host})
}

// Use the defaultUserAgent unless the Header contains one, which
// may be blank to not send the header.
userAgent := defaultUserAgent
if r.Header.has("User-Agent") {
    userAgent = r.Header.Get("User-Agent")
}
if userAgent != "" {
    _, err = fmt.Fprintf(w, "User-Agent: %s\r\n", userAgent)
    if err != nil {
        return err
    }
    if trace != nil && trace.WroteHeaderField != nil {
        trace.WroteHeaderField("User-Agent", []string{userAgent})
    }
}

// Process Body,ContentLength,Close,Trailer
tw, err := newTransferWriter(r)
if err != nil {
    return err
}
err = tw.writeHeader(w, trace)
if err != nil {
    return err
}

err = r.Header.writeSubset(w, reqWriteExcludeHeader, trace)
if err != nil {
    return err
}

if extraHeaders != nil {
    err = extraHeaders.write(w, trace)
    if err != nil {
        return err
    }
}

_, err = io.WriteString(w, "\r\n")
if err != nil {
    return err
}

if trace != nil && trace.WroteHeaders != nil {
    trace.WroteHeaders()
}

// Flush and wait for 100-continue if expected.
if waitForContinue != nil {
    if bw, ok := w.(*bufio.Writer); ok {
        err = bw.Flush()
        if err != nil {
            return err
        }
    }
    if trace != nil && trace.Wait100Continue != nil {
        trace.Wait100Continue()
    }
    if !waitForContinue() {
        r.closeBody()
        return nil
    }
}

if bw, ok := w.(*bufio.Writer); ok && tw.FlushHeaders {
    if err := bw.Flush(); err != nil {
        return err
    }
}

// Write body and trailer
err = tw.writeBody(w)
if err != nil {
    if tw.bodyReadError == err {
        err = requestBodyReadError{err}
    }
    return err
}

if bw != nil {
    return bw.Flush()
}
return nil

}
`

ou conforme usado em um teste completo como pprof/profile/profile_test.go:
`
func checkAggregation(prof *Profile, a *aggTest) error {
// Verifique se o número total de amostras para as linhas foi preservado.
total := int64(0)

samples := make(map[string]bool)
for _, sample := range prof.Sample {
    tb := locationHash(sample)
    samples[tb] = true
    total += sample.Value[0]
}

if total != totalSamples {
    return fmt.Errorf("sample total %d, want %d", total, totalSamples)
}

// Check the number of unique sample locations
if a.rows != len(samples) {
    return fmt.Errorf("number of samples %d, want %d", len(samples), a.rows)
}

// Check that all mappings have the right detail flags.
for _, m := range prof.Mapping {
    if m.HasFunctions != a.function {
        return fmt.Errorf("unexpected mapping.HasFunctions %v, want %v", m.HasFunctions, a.function)
    }
    if m.HasFilenames != a.fileline {
        return fmt.Errorf("unexpected mapping.HasFilenames %v, want %v", m.HasFilenames, a.fileline)
    }
    if m.HasLineNumbers != a.fileline {
        return fmt.Errorf("unexpected mapping.HasLineNumbers %v, want %v", m.HasLineNumbers, a.fileline)
    }
    if m.HasInlineFrames != a.inlineFrame {
        return fmt.Errorf("unexpected mapping.HasInlineFrames %v, want %v", m.HasInlineFrames, a.inlineFrame)
    }
}

// Check that aggregation has removed finer resolution data.
for _, l := range prof.Location {
    if !a.inlineFrame && len(l.Line) > 1 {
        return fmt.Errorf("found %d lines on location %d, want 1", len(l.Line), l.ID)
    }

    for _, ln := range l.Line {
        if !a.fileline && (ln.Function.Filename != "" || ln.Line != 0) {
            return fmt.Errorf("found line %s:%d on location %d, want :0",
                ln.Function.Filename, ln.Line, l.ID)
        }
        if !a.function && (ln.Function.Name != "") {
            return fmt.Errorf(`found file %s location %d, want ""`,
                ln.Function.Name, l.ID)
        }
    }
}

return nil

}
`
Estes são dois exemplos em que posso pensar em que se diria: "Gostaria de uma melhor opção de tratamento de erros"

Alguém pode demonstrar como isso melhoraria usando try() ?

Sou principalmente a favor desta proposta.

Minha principal preocupação, compartilhada com muitos comentaristas, é sobre parâmetros de resultado nomeados. A proposta atual certamente incentiva muito mais o uso de parâmetros de resultado nomeados e acho que isso seria um erro. Não acredito que isso seja simplesmente uma questão de estilo, como afirma a proposta: resultados nomeados são uma característica sutil da linguagem que, em muitos casos, torna o código mais propenso a bugs ou menos claro. Após ~8 anos lendo e escrevendo código Go, eu realmente só uso parâmetros de resultado nomeados para dois propósitos:

  • Documentação dos parâmetros de resultado
  • Manipulando um valor de resultado (geralmente um error ) dentro de um defer

Para atacar este problema de uma nova direção, aqui está uma ideia que eu não acho que se alinha com nada que tenha sido discutido no documento de design ou neste tópico de comentários sobre o problema. Vamos chamá-lo de "erro-defer":

Permitir que o defer seja usado para chamar funções com um parâmetro de erro implícito.

Então, se você tem uma função

func f(err error, t1 T1, t2 T2, ..., tn Tn) error

Então, em uma função g onde o último parâmetro de resultado tem o tipo error (ou seja, qualquer função onde try pode ser usado), uma chamada para f pode ser adiado da seguinte forma:

func g() (R0, R0, ..., error) {
    defer f(t0, t1, ..., tn) // err is implicit
}

A semântica de error-defer é:

  1. A chamada adiada para f é chamada com o último parâmetro de resultado de g como o primeiro parâmetro de entrada de f
  2. f só é chamado se esse erro não for nulo
  3. O resultado de f é atribuído ao último parâmetro de resultado de g

Então, para usar um exemplo do antigo documento de design de tratamento de erros, usando error-defer e try, poderíamos fazer

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

Veja como HandleErrorf funcionaria:

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

func handleErrorf(err error, format string, args ...interface{}) error {
    return fmt.Errorf(format+": %v", append(args, err)...)
}

Um caso de canto que precisaria ser resolvido é como lidar com casos em que é ambíguo qual forma de adiamento estamos usando. Acho que isso só acontece com funções (muito incomuns) com assinaturas assim:

func(error, ...error) error

Parece razoável dizer que este caso é tratado da maneira sem adiamento de erros (e isso preserva a compatibilidade com versões anteriores).


Pensando nessa ideia nos últimos dias, é um pouco mágico, mas evitar parâmetros de resultado nomeados é uma grande vantagem a seu favor. Como try encoraja mais o uso de defer para manipulação de erros, faz algum sentido que defer possa ser estendido para melhor se adequar a esse propósito. Além disso, há uma certa simetria entre try e error-defer.

Finalmente, os adiamentos de erro são úteis hoje mesmo sem tentativa, pois suplantam o uso de parâmetros de resultado nomeados para manipular retornos de erro. Por exemplo, aqui está uma versão editada de algum código real:

// GetMulti retrieves multiple files through the cache at once and returns its
// results as a slice parallel to the input.
func (c *FileCache) GetMulti(keys []string) (_ []*File, err error) {
    files := make([]*file, len(keys))

    defer func() {
        if err != nil {
            // Return any successfully retrieved files.
            for _, f := range files {
                if f != nil {
                    c.put(f)
                }
            }
        }
    }()

    // ...
}

Com error-defer, isso se torna:

// GetMulti retrieves multiple files through the cache at once and returns its
// results as a slice parallel to the input.
func (c *FileCache) GetMulti(keys []string) ([]*File, error) {
    files := make([]*file, len(keys))

    defer func(err error) error {
        // Return any successfully retrieved files.
        for _, f := range files {
            if f != nil {
                c.put(f)
            }
        }
        return err
    }()

    // ...
}

@beoran Em relação ao seu comentário de que devemos esperar pelos genéricos. Os genéricos não ajudarão aqui - por favor, leia o FAQ .

Em relação às suas sugestões sobre o comportamento padrão de 2 argumentos try do @velovix : Como eu disse antes , sua ideia do que é a escolha obviamente razoável é o pesadelo de outra pessoa.

Posso sugerir que continuemos esta discussão uma vez que um amplo consenso evolui de que try com um manipulador de erro explícito é uma ideia melhor do que o mínimo atual try . Nesse ponto, faz sentido discutir os detalhes de tal design.

(Gosto de ter um manipulador, aliás. É uma de nossas propostas anteriores. E se adotarmos try como está, ainda podemos avançar para um try com um manipulador em um -compatível - pelo menos se o manipulador for opcional. Mas vamos dar um passo de cada vez.)

@aarzilli Obrigado pela sua sugestão .

Enquanto os erros de decoração forem opcionais, as pessoas tenderão a não fazê-lo (afinal, é um trabalho extra). Veja também meu comentário aqui .

Então, eu não acho que os try _desanimam_ as pessoas de decorar erros (eles já estão desencorajados mesmo com o if pelo motivo acima); é que try não _incentiva_ isso.

(Uma maneira de incentivá-lo é vinculá-lo a try : só se pode usar try se também decorar o erro ou optar por não participar explicitamente.)

Mas voltando às suas sugestões: acho que você está introduzindo muito mais maquinário aqui. Alterar a semântica de defer apenas para fazê-la funcionar melhor para try não é algo que gostaríamos de considerar, a menos que essas alterações defer sejam benéficas de uma maneira mais geral. Além disso, sua sugestão une defer com try e assim torna ambos os mecanismos menos ortogonais; algo que gostaríamos de evitar.

Mas, mais importante, duvido que você queira forçar todos a escrever um defer apenas para que possam usar try . Mas sem fazer isso, voltamos à estaca zero: as pessoas tenderão a não decorar erros.

(Eu gosto de ter um manipulador, por falar nisso. É uma de nossas propostas anteriores. E se adotarmos try como está, ainda podemos avançar para uma tentativa com um manipulador de maneira compatível com o futuro - pelo menos se o manipulador for opcional. Mas vamos dar um passo de cada vez.)

Claro, talvez uma abordagem de várias etapas seja o caminho a seguir. Se adicionarmos um argumento handler opcional no futuro, ferramentas podem ser criadas para avisar o escritor de um try não tratado no mesmo espírito que a ferramenta errcheck . De qualquer forma, agradeço seu feedback!

@alanfo Obrigado pelo seu feedback positivo.

Sobre os pontos que você levantou:

1) Se o único problema com try é o fato de que será necessário nomear um retorno de erro para que possamos decorar um erro via defer , acho que estamos bem. Se nomear o resultado for um problema real, podemos resolvê-lo. Um mecanismo simples em que posso pensar seria uma variável pré-declarada que é um alias para um resultado de erro (pense nisso como mantendo o erro que acionou o try mais recente). Pode haver ideias melhores. Não propusemos isso porque já existe um mecanismo na linguagem, que é dar nome ao resultado.
2) try e testes: Isso pode ser resolvido e feito para funcionar. Veja o documento detalhado.
3) Isso é explicitamente abordado no documento detalhado.
4) Reconhecido.

@benhoyt Obrigado pelo seu feedback positivo.

Se o principal argumento contra esta proposta é o fato de que try é um built-in, estamos em um ótimo lugar. Usar um built-in é simplesmente uma solução pragmática para o problema de compatibilidade com versões anteriores (isso não causa trabalho extra para o analisador e ferramentas etc. - mas isso é apenas um bom benefício colateral, não o principal motivo). Há também alguns benefícios em ter que escrever parênteses, isso é discutido em detalhes no documento de design (seção sobre Propriedades do design proposto).

Dito tudo isso, se usar um built-in é o showtopper, devemos considerar a palavra-chave try . Não será compatível com o código existente, pois a palavra-chave pode entrar em conflito com os identificadores existentes.

(Para ser completo, há também a opção de um operador como ? , que seria compatível com versões anteriores. Mas não me parece a melhor escolha para uma linguagem como Go. Mas, novamente, se isso é tudo o que é preciso para tornar try palatável, talvez devêssemos considerá-lo.)

@ugorji Obrigado pelo seu feedback positivo.

try pode ser estendido para receber um argumento adicional. Nossa preferência seria pegar apenas uma função com assinatura func (error) error . Se você quiser entrar em pânico, é fácil fornecer uma função auxiliar de uma linha:

func doPanic(err error) error { panic(err) }

Melhor manter o design de try simples.

@patrick-nyt O que você está sugerindo :

file, err := os.Open("file.go")
try(err)

será possível com a proposta atual.

@dpinela , @ugorji Leia também o documento de design sobre must vs try . É melhor manter try o mais simples possível. must é um "padrão" comum em expressões de inicialização, mas não há necessidade urgente de "corrigir" isso.

@jargv Obrigado pela sua sugestão . Esta é uma ideia interessante (veja também o meu comentário aqui sobre este assunto). Para resumir:

  • try(x) funciona como proposto
  • try() retorna um *error apontando para o resultado do erro

Esta seria, de fato, outra maneira de obter o resultado sem precisar nomeá-lo.

@cespare A sugestão de @jargv me parece muito mais simples do que você está propondo . Ele resolve o mesmo problema de acesso ao erro de resultado. O que você acha?

Conforme https://github.com/golang/go/issues/32437#issuecomment -499320588:

func doPanic(err error) error { panic(err) }

Prevejo que esta função seria bastante comum. Isso poderia ser predefinido em "builtin" (ou em outro lugar em um pacote padrão, por exemplo errors )?

Pena que você não prevê genéricos poderosos o suficiente para implementar
tentar, eu realmente esperava que fosse possível fazê-lo.

Sim, esta proposta pode ser um primeiro passo, embora eu não veja muita utilidade em
eu mesmo como está agora.

Concedido, esta questão talvez tenha muito foco em alternativas detalhadas,
mas isso mostra que muitos participantes não estão completamente satisfeitos com
isto. O que parece faltar é um amplo consenso sobre essa proposta...

Op vr 7 jun. 2019 01:04 schreef pj [email protected] :

Asper #32437 (comentário)
https://github.com/golang/go/issues/32437#issuecomment-499320588 :

func doPanic(err error) error { panic(err) }

Prevejo que esta função seria bastante comum. Isso poderia ser predefinido
em "embutido"?


Você está recebendo isso porque foi mencionado.
Responda a este e-mail diretamente, visualize-o no GitHub
https://github.com/golang/go/issues/32437?email_source=notifications&email_token=AAARM6OOOLLYO5ZCE6VVL2TPZGJWRA5CNFSM4HTGCZ72YY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGODXEMYZYissue#comentário-499
ou silenciar o thread
https://github.com/notifications/unsubscribe-auth/AAARM6K5AOR2DES4QDTNLSTPZGJWRANCNFSM4HTGCZ7Q
.

@pjebs , escrevi a função equivalente dezenas de vezes. Eu costumo chamar de “orDie” ou “check”. É tão simples que não há necessidade real de torná-lo parte da biblioteca padrão. Além disso, pessoas diferentes podem querer fazer login ou qualquer outra coisa antes do término.

@beoran Talvez você possa expandir a conexão entre genéricos e tratamento de erros. Quando penso neles, parecem duas coisas diferentes. Generics não é um catch all que pode resolver todos os problemas com o idioma. É a capacidade de escrever uma única função que pode operar em vários tipos.

Essa proposta específica de tratamento de erros tenta reduzir o clichê introduzindo uma função pré-declarada try que altera o controle de fluxo em algumas circunstâncias. Os genéricos nunca mudarão o fluxo de controle. Então eu realmente não vejo a relação.

Minha reação inicial a isso foi um 👎, pois imaginei que lidar com várias chamadas propensas a erros em uma função tornaria o identificador de erro defer confuso. Depois de ler toda a proposta, mudei minha reação para um ❤️ e 👍, pois aprendi que isso ainda pode ser alcançado com complexidade relativamente baixa.

@carlmjohnson Sim, é simples, mas...

Eu escrevi a função equivalente dezenas de vezes.

As vantagens de uma função pré-declarada são:

  1. Podemos uni-lo
  2. Não precisamos redeclarar a função err => panic em cada pacote que usamos ou manter um local comum para ela. Como é comum provavelmente a todos na comunidade Go, o "pacote padrão" é _ o _ local comum para ele.

@griesemer Com a variante do manipulador de erros da proposta try original, o requisito da função abrangente para retornar o erro agora não é mais necessário.

Quando perguntei pela primeira vez sobre o err => panic, fui apontado que a proposta considerava, mas considerava muito perigoso (por boas razões). Mas se fizermos o uso de try() sem um manipulador de erros em um cenário em que a função abrangente não retorna erro, transformá-lo em um erro de tempo de compilação alivia a preocupação discutida na proposta

@pjebs O requisito da função abrangente para retornar um erro não era necessário no design original _se_ um manipulador de erro foi fornecido. Mas é apenas mais uma complicação de try . É _muito_ melhor mantê-lo simples. Em vez disso, seria mais claro ter uma função must separada, que sempre entra em pânico em caso de erro (mas, caso contrário, é como try ). Então é óbvio o que acontece no código e não é preciso olhar para o contexto.

A principal atração de ter um must seria que ele poderia ser usado com testes de unidade; especialmente se o pacote testing foi ajustado adequadamente para se recuperar dos pânicos causados ​​por must e reportá-los como falhas de teste de uma maneira agradável. Mas por que adicionar mais um novo mecanismo de linguagem quando podemos apenas ajustar o pacote de teste para também aceitar a função de teste da forma TestXxx(t *testing.T) error ? Se eles retornarem um erro, o que parece bastante natural afinal (talvez devêssemos ter feito isso desde o início), então try funcionará bem. Testes locais precisarão de um pouco mais de trabalho, mas provavelmente é factível.

O outro uso relativamente comum para must é em expressões de inicialização global ( must(regexp.Compile... , etc.). Se seria um "bom ter", mas isso não necessariamente o eleva ao nível necessário para um novo recurso de linguagem.

@griesemer Dado que must está vagamente relacionado a try , e dado que o impulso é para try ser implementado, você não acha que é bom considerar must ao mesmo tempo - mesmo que seja apenas um "bom ter".

As chances são de que, se não for discutido nesta rodada, simplesmente não será implementado/considerado seriamente, pelo menos por mais de 3 anos (ou talvez nunca). A sobreposição na discussão também seria boa, em vez de começar do zero e reciclar as discussões.

Muitas pessoas afirmaram que must complementa try muito bem.

@pjebs Certamente não parece que haja algum "momento para try ser implementado" agora... - E também postamos isso há dois dias. Nem nada foi decidido. Vamos dar um tempo.

Não nos escapou que must se encaixa bem com try , mas isso não é o mesmo que torná-lo parte da linguagem. Só começamos a explorar este espaço com um grupo mais amplo de pessoas. Nós realmente não sabemos ainda o que pode surgir a favor ou contra. Obrigado.

Depois de passar horas lendo todos os comentários e o documento de design detalhado, eu queria adicionar minhas opiniões a esta proposta.

Farei o meu melhor para respeitar o pedido de @ianlancetaylor não apenas para reafirmar os pontos anteriores, mas para adicionar novos comentários à discussão. No entanto, acho que não posso fazer os novos comentários sem fazer referência aos comentários anteriores.

Preocupações

Sobrecarga infeliz de adiar

A preferência por sobrecarregar a natureza óbvia e direta de defer como alarmante. Se eu escrever defer closeFile(f) isso é direto e óbvio para mim o que está acontecendo e por quê; no final da função que será chamada. E enquanto usar defer para panic() e recover() é menos óbvio, eu raramente ou nunca o uso e quase nunca o vejo ao ler o código de outra pessoa.

Spoo para sobrecarregar defer para também lidar com erros não é óbvio e confuso. Por que a palavra-chave defer ? defer não significa _"Fazer mais tarde"_ em vez de _"Talvez até mais tarde?"_

Também há a preocupação mencionada pela equipe Go sobre o desempenho defer . Dado isso, parece duplamente lamentável que defer esteja sendo considerado para o fluxo de código _"hot path"_.

Nenhuma estatística verificando um caso de uso significativo

Como o @prologic mencionou, essa proposta try() é baseada em uma grande porcentagem de código que usaria esse caso de uso ou é baseada na tentativa de aplacar aqueles que reclamaram do tratamento de erros do Go?

Eu gostaria de saber como fornecer estatísticas da minha base de código sem revisar exaustivamente todos os arquivos e fazer anotações; Eu não sei como @prologic foi capaz, embora feliz por ele ter feito isso.

Mas, curiosamente, eu ficaria surpreso se try() abordasse 5% dos meus casos de uso e suspeitaria que isso abordaria menos de 1%. Você tem certeza de que outros têm resultados muito diferentes? Você pegou um subconjunto da biblioteca padrão e tentou ver como seria aplicado?

Porque sem estatísticas conhecidas de que isso é apropriado para uma grande quantidade de código em estado selvagem, eu tenho que perguntar se essa nova mudança complicada na linguagem que exigirá que todos aprendam os novos conceitos realmente aborda um número atraente de casos de uso?

Torna mais fácil para os desenvolvedores ignorarem erros

Esta é uma repetição total do que os outros têm comentários, mas o que basicamente fornecer try() é análogo em muitos aspectos a simplesmente abraçar o seguinte como código idomático, e este é um código que nunca encontrará seu caminho em nenhum código -respeitando os navios do desenvolvedor:

f, _ := os.Open(filename)

Eu sei que posso ser melhor em meu próprio código, mas também sei que muitos de nós dependem da generosidade de outros desenvolvedores Go que publicam alguns pacotes tremendamente úteis, mas pelo que vi em _"Other People's Code(tm)"_ as melhores práticas no tratamento de erros são frequentemente ignoradas.

Então, sério, nós realmente queremos tornar mais fácil para os desenvolvedores ignorarem erros e permitir que eles poluam o GitHub com pacotes não robustos?

Pode (principalmente) já implementar try() no userland

A menos que eu entenda mal a proposta - o que provavelmente faço - aqui está try() no Go Playground implementado em userland , embora com apenas um (1) valor de retorno e retornando uma interface em vez do tipo esperado:

package main

import (
    "errors"
    "fmt"
    "strings"
)
func main() {
    defer func() {
        r := recover()
        if r != nil && strings.HasPrefix(r.(string),"TRY:") {
            fmt.Printf("Ouch! %s",strings.TrimPrefix(r.(string),"TRY: "))
        }
    }()
    n := try(badjuju()).(int)
    fmt.Printf("Just chillin %dx!",n)   
}
func badjuju() (int,error) {
    return 10, errors.New("this is a really bad error")
}
func try(args ...interface{}) interface{} {
    err,ok := args[1].(error)
    if ok && err != nil {
        panic(fmt.Sprintf("TRY: %s",err.Error()))
    }
    return args[0]
}

Assim, o usuário poderia adicionar um try2() , try3() e assim por diante, dependendo de quantos valores de retorno eles precisassem retornar.

Mas Go precisaria apenas de um (1) recurso de linguagem simples _ mas universal _ para permitir que os usuários que desejam try() implementem seu próprio suporte, embora um que ainda exija declaração de tipo explícita. Adicione um recurso _(totalmente compatível com versões anteriores)_ para um Go func para retornar um número variável de valores de retorno, por exemplo:

func try(args ...interface{}) ...interface{} {
    err,ok := args[1].(error)
    if ok && err != nil {
        panic(fmt.Sprintf("TRY: %s",err.Error()))
    }
    return args[0:len(args)-2]
}

E se você abordar os genéricos primeiro, as asserções de tipo nem serão necessárias _(embora eu ache que os casos de uso para genéricos devam ser reduzidos adicionando builtins para abordar os casos de uso genéricos em vez de adicionar a semântica confusa e salada de sintaxe de genéricos de Java et. al.)_

Falta de obviedade

Ao estudar o código da proposta, descobri que o comportamento não é óbvio e um pouco difícil de raciocinar.

Quando vejo try() envolvendo uma expressão, o que acontecerá se um erro for retornado?

O erro será simplesmente ignorado? Ou ele pulará para o primeiro ou o mais recente defer , e se assim for, ele definirá automaticamente uma variável chamada err dentro do encerramento que, ou ele passará como um parâmetro _(I não vê um parâmetro?)_. E se não for um nome de erro automático, como nomeio? E isso significa que não posso declarar minha própria variável err na minha função, para evitar conflitos?

E ele vai chamar todos os defer s? Na ordem inversa ou na ordem normal?

Ou ele retornará tanto do fechamento quanto do func onde o erro foi retornado? _(Algo que eu nunca teria considerado se não tivesse lido aqui palavras que implicam isso.)_

Depois de ler a proposta e todos os comentários até agora, honestamente, ainda não sei as respostas para as perguntas acima. É esse o tipo de recurso que queremos adicionar a uma linguagem cujos defensores defendem como sendo _"Capitão Óbvio?"_

Falta de controle

Usando defer , parece que o único controle que os desenvolvedores teriam é ramificar para _(o mais recente?)_ defer . Mas na minha experiência com qualquer método além de um func trivial, geralmente é mais complicado do que isso.

Muitas vezes, acho útil compartilhar aspectos do tratamento de erros em um func — ou mesmo em um package — mas também ter um tratamento mais específico compartilhado em um ou mais outros pacotes.

Por exemplo, posso chamar cinco (5) func chamadas que retornam um error() de dentro de outro func ; vamos rotulá-los A() , B() , C() , D() e E() . Eu posso precisar C() para ter seu próprio tratamento de erros, A() , B() , D() e E() para compartilhar algum tratamento de erros, e B() e E() para ter manuseio específico.

Mas não acredito que seja possível fazer isso com esta proposta. Pelo menos não facilmente.

Ironicamente, no entanto, Go já possui recursos de linguagem que permitem um alto nível de flexibilidade que não precisa ser limitado a um pequeno conjunto de casos de uso; func s e encerramentos. Então minha pergunta retórica é:

_ "Por que não podemos simplesmente adicionar pequenas melhorias à linguagem existente para resolver esses casos de uso e não precisar adicionar novas funções internas ou aceitar semânticas confusas?" _

É uma pergunta retórica porque pretendo apresentar uma proposta como alternativa, que concebi durante o estudo desta proposta e considerando todos os seus inconvenientes.

Mas eu discordo, isso virá mais tarde e este comentário é sobre por que a proposta atual precisa ser reconsiderada.

Falta de suporte declarado para break

Isso pode parecer que sai do campo esquerdo, pois a maioria das pessoas usa retornos antecipados para tratamento de erros, mas descobri que é preferível usar break para tratamento de erros envolvendo a maior parte ou toda uma função antes de return .

Eu usei essa abordagem por um tempo e seus benefícios em facilitar a refatoração por si só a tornam preferível a return , mas ela tem vários outros benefícios, incluindo ponto de saída único e capacidade de encerrar uma seção de uma função antecipadamente, mas ainda ser capaz de executar limpeza _(provavelmente por isso raramente uso defer , que acho mais difícil de raciocinar em termos de fluxo de programa.)_

Para usar break em vez de um retorno antecipado use um loop for range "1" {...} para criar um bloco para o intervalo sair de _(na verdade eu crio um pacote chamado only que contém apenas uma constante chamado Once com um valor de "1" ):_

func (me *Config) WriteFile() (err error) {
    for range only.Once {
        var j []byte
        j, err = json.MarshalIndent(me, "", "    ")
        if err != nil {
            err = fmt.Errorf("unable to marshal config; %s", 
                err.Error(),
            )
            break
        }
        err = me.MaybeMakeDir(me.GetDir(), os.ModePerm)
        if err != nil {
            err = fmt.Errorf("unable to make directory'%s'; %s", 
                me.GetDir(), 
                err.Error(),
            )
            break
        }
        err = ioutil.WriteFile(string(me.GetFilepath()), j, os.ModePerm)
        if err != nil {
            err = fmt.Errorf("unable to write to config file '%s'; %s", 
                me.GetFilepath(), 
                err.Error(),
            )
            break
        }
    }
    return err
}

Eu pretendo escrever sobre o padrão em um futuro próximo e discutir as várias razões pelas quais eu descobri que ele funciona melhor do que os retornos antecipados.

Mas eu discordo. Minha razão de trazê-lo aqui é que eu teria que Go para implementar o tratamento de erros que assume return s inicial e ignora o uso break para tratamento de erros

Minha opinião err == nil é problemática

Como digressão adicional, quero trazer à tona a preocupação que senti sobre o tratamento de erros idiomáticos em Go. Embora eu acredite muito na filosofia do Go de lidar com erros quando eles ocorrem versus usar o tratamento de exceções, sinto que o uso de nil para indicar que nenhum erro é problemático porque muitas vezes acho que gostaria de retornar uma mensagem de sucesso de uma rotina — para uso em respostas de API — e não apenas retornar um valor diferente de zero apenas quando houver um erro.

Então, para o Go 2, eu realmente gostaria de ver o Go considerar a adição de um novo tipo interno de status e três funções internas iserror() , iswarning() , issuccess() . status poderia implementar error — permitindo muita compatibilidade com versões anteriores e um nil passado para issuccess() retornaria true — mas status teria um estado interno adicional para o nível de erro, de modo que o teste do nível de erro sempre fosse feito com uma das funções internas e, idealmente, nunca com uma verificação nil . Isso permitiria algo como a seguinte abordagem:

func (me *Config) WriteFile() (sts status) {
    for range only.Once {
        var j []byte
        j, sts = json.MarshalIndent(me, "", "    ")
        if iserror(sts) {
            sts.AddMessage("unable to marshal config")
            break
        }
        sts = me.MaybeMakeDir(me.GetDir(), os.ModePerm)
        if iserror(sts) {
            sts.AddMessage("unable to make directory'%s'", me.GetDir())
            break
        }
        sts = ioutil.WriteFile(string(me.GetFilepath()), j, os.ModePerm)
        if iserror(sts) {
            sts.AddMessage("unable to write to config file '%s'", 
                me.GetFilepath(), 
            )
            break
        }
        sts = fmt.Status("config file written")
    }
    return sts
}

Eu já estou usando uma abordagem userland em um pacote de uso interno de nível pré-beta que é semelhante ao acima para tratamento de erros. Francamente , gasto muito menos tempo pensando em como estruturar o código ao usar essa abordagem do que quando estava tentando seguir o tratamento de erros idiomáticos do Go.

Se você acha que há alguma chance de evoluir o código idiomático Go para essa abordagem, leve isso em consideração ao implementar o tratamento de erros, inclusive ao considerar esta proposta try() .

_"Não é para todos"_ justificação

Uma das principais respostas da equipe Go foi _"Novamente, esta proposta não tenta resolver todas as situações de tratamento de erros."_
E essa é provavelmente a preocupação mais preocupante, do ponto de vista da governança.

Essa nova mudança complicada na linguagem que exigirá que todos aprendam os novos conceitos realmente aborda um número atraente de casos de uso?

E essa não é a mesma justificativa que os membros da equipe principal negaram inúmeras solicitações de recursos da comunidade? A seguir está uma citação direta de um comentário feito por um membro da equipe Go em uma resposta arquetípica a uma solicitação de recurso enviada há cerca de 2 anos _(não estou nomeando a pessoa ou a solicitação de recurso específica porque esta discussão não deve ser possível as pessoas, mas sim sobre a língua):_

_"Um novo recurso de linguagem precisa de casos de uso atraentes. Todos os recursos de linguagem são úteis, ou ninguém os proporia. A questão é: eles são úteis o suficiente para justificar a complicação da linguagem e exigir que todos aprendam os novos conceitos? Quais são os usos atraentes casos aqui? Como as pessoas usarão isso? Por exemplo, as pessoas esperariam poder... e se sim, como elas fariam isso? Esta proposta faz mais do que deixar você...?"
— Um membro central da equipe Go

Francamente, quando vi essas respostas, senti um de dois sentimentos:

  1. Indignação se for uma característica com a qual concordo, ou
  2. Euforia se for uma característica com a qual não concordo.

Mas em ambos os casos meus sentimentos foram/são irrelevantes; Eu entendo e concordo que parte da razão pela qual o Go é o idioma em que muitos de nós escolhemos desenvolver é por causa dessa guarda zelosa da pureza do idioma.

E é por isso que essa proposta me incomoda tanto, porque a equipe principal do Go parece estar se aprofundando nessa proposta no mesmo nível de alguém que quer dogmaticamente um recurso esotérico que não há como a comunidade Go jamais tolerará.

_(E eu realmente espero que a equipe não atire no mensageiro e tome isso como uma crítica construtiva de alguém que quer ver Go continuar sendo o melhor que pode ser para todos nós, pois eu teria que ser considerado "Persona non grata" por a equipe principal.)_

Se exigir um conjunto atraente de casos de uso do mundo real é o padrão para todas as propostas de recursos geradas pela comunidade, não deveria ser o mesmo padrão para _ todas _ propostas de recursos?

Aninhamento de try()

Isso também foi abordado por alguns, mas quero fazer uma comparação entre try() e a solicitação contínua de operadores ternários. Citando os comentários de outro membro da equipe Go cerca de 18 meses atrás:

_"quando "programando em grande escala" (grandes bases de código com grandes equipes por longos períodos de tempo), o código é lido com MUITO mais frequência do que é escrito, então otimizamos a legibilidade, não a capacidade de escrita."_

Uma das razões _primárias_ declaradas para não adicionar operadores ternários é que eles são difíceis de ler e/ou fáceis de interpretar mal quando aninhados. No entanto, o mesmo pode ser verdade para instruções try() aninhadas como try(try(try(to()).parse().this)).easily()) .

Razões adicionais para argumentar contra os operadores ternários são que eles são _"expressões"_ com o argumento de que expressões aninhadas podem adicionar complexidade. Mas try() também não cria uma expressão aninhável?

Agora, alguém aqui disse _"Acho que exemplos como [aninhados try() s] não são realistas"_ e essa afirmação não foi contestada.

Mas se as pessoas aceitam como postulado que os desenvolvedores não aninham try() , então por que a mesma deferência não é dada aos operadores ternários quando as pessoas dizem _"Eu acho que operadores ternários profundamente aninhados são irreais?"_

Conclusão para este ponto, acho que se o argumento contra os operadores ternários é realmente válido, então eles também devem ser considerados argumentos válidos contra esta proposta try() .

Resumindo

No momento em que este texto foi escrito, os votos negativos de 58% para votos positivos de 42% . Acho que isso por si só deve ser suficiente para indicar que esta é uma proposta divisiva o suficiente para que seja hora de retornar à prancheta sobre essa questão.

fwiw

PS Para ser mais irônico, acho que devemos seguir a sabedoria parafraseada de Yoda:

_"Não há try() . Apenas do() ."_

@ianlancetaylor

@beoran Talvez você possa expandir a conexão entre genéricos e tratamento de erros.

Não falando por @beoran , mas no meu comentário de alguns minutos atrás, você verá que, se tivéssemos genéricos _(mais parâmetros de retorno variadic)_, poderíamos construir nosso próprio try() .

No entanto - e vou repetir o que disse acima sobre genéricos aqui onde será mais fácil de ver:

_" Acho que os casos de uso para genéricos devem ser reduzidos adicionando builtins para abordar os casos de uso genéricos em vez de adicionar a semântica confusa e salada de sintaxe de genéricos de Java et. al.)"_

@ianlancetaylor

Ao tentar formular uma resposta para sua pergunta, tentei implementar a função try no Go como está e, para minha alegria, na verdade já é possível emular algo bem parecido:

func try(v interface{}, err error) interface{} {
   if err != nil { 
     panic(err)
   }
   return v
}

Veja aqui como pode ser usado: https://play.golang.org/p/Kq9Q0hZHlXL

As desvantagens dessa abordagem são:

  1. Um resgate adiado é necessário, mas com try como nesta proposta, um manipulador adiado também é necessário se quisermos fazer o tratamento de erros adequado. Então eu sinto que isso não é uma desvantagem séria. Poderia até ser melhor se Go tivesse algum tipo de super(arg1, ..., argn) embutido que faz com que o chamador do chamador, um nível acima da pilha de chamadas, retorne com os argumentos fornecidos arg1,...argn, uma espécie de super retorno Se você for.
  2. Este try que implementei só pode funcionar com uma função que retorna um único resultado e um erro.
  3. Você precisa digitar assert os resultados da interface vazia retornados.

Genéricos suficientemente poderosos podem resolver os problemas 2 e 3, deixando apenas 1, que pode ser resolvido adicionando um super() . Com esses dois recursos no lugar, poderíamos obter algo como:

func (T ... interface{})try(T, err error) super {
   if err != nil { 
      super(err)
   }
  super(T...)
}

E então o resgate adiado não seria mais necessário. Esse benefício estaria disponível mesmo se nenhum genérico fosse adicionado ao Go.

Na verdade, essa ideia de um super() embutido é tão poderosa e interessante que eu poderia postar uma proposta para ela separadamente.

@beoran É bom ver que chegamos exatamente às mesmas restrições independentemente em relação à implementação de try() no userland, exceto pela super parte que não incluí porque queria falar sobre algo semelhante em uma proposta alternativa. :-)

Eu gosto da proposta, mas o fato de você ter que especificar explicitamente que defer try(...) e go try(...) não são permitidos me fez pensar que algo não estava certo... A ortogonalidade é um bom guia de design. Ao ler mais e ver coisas como
x = try(foo(...)) y = try(bar(...))
Gostaria de saber se pode ser try precisa ser um contexto ! Considerar:
try ( x = foo(...) y = bar(...) )
Aqui foo() e bar() retornam dois valores, sendo o segundo error . A semântica de tentativa só importa para chamadas dentro do bloco try onde o valor de erro retornado é elidido (sem receptor) em vez de ignorado (o receptor é _ ). Você pode até mesmo lidar com alguns erros entre as chamadas foo e bar .

Resumo:
a) o problema de não permitir try para go e defer desaparece em virtude da sintaxe.
b) o tratamento de erros de várias funções pode ser fatorado.
c) sua natureza mágica é melhor expressa como sintaxe especial do que como chamada de função.

Se try for um contexto, acabamos de criar blocos try/catch que estamos tentando evitar especificamente (e por boas razões)

Não há captura. Exatamente o mesmo código seria gerado quando a proposta atual foi
x = try(foo(...)) y = try(bar(...))
Esta é apenas uma sintaxe diferente, não semântica.
````

Acho que fiz algumas suposições sobre isso que não deveria ter feito, embora ainda haja algumas desvantagens.

E se foo ou bar não retornarem um erro, eles também podem ser colocados no contexto try? Caso contrário, parece que seria meio feio alternar entre funções de erro e não-erro, e se puderem, voltamos aos problemas dos blocos try em linguagens mais antigas.

A segunda coisa é que normalmente a sintaxe keyword ( ... ) significa que você prefixa a palavra-chave em cada linha. Então, para import, var, const, etc: cada linha começa com a palavra-chave. Abrir uma exceção a essa regra não parece uma boa decisão

Ao invés de usar uma função, seria apenas mais idiomático usar um identificador especial?

Já temos o identificador em branco _ que ignora valores.
Poderíamos ter algo como # que só pode ser usado em funções que tenham o último valor retornado do tipo erro.

func foo() (error) {
    f, # := os.Open()
    defer f.Close()
    _, # = f.WriteString("foo")
    return nil
}

quando um erro é atribuído a # a função retorna imediatamente com o erro recebido. Já para as outras variáveis ​​seus valores seriam:

  • se eles não forem nomeados valor zero
  • o valor atribuído às variáveis ​​nomeadas caso contrário

@deanveloper , a semântica do bloco try importa apenas para funções que retornam um valor de erro e onde o valor de erro não é atribuído. Assim, o último exemplo da presente proposta também poderia ser escrito como
try(x = foo(...)) try(y = bar(...))
colocar ambas as instruções dentro do mesmo bloco é semelhante ao que fazemos para instruções repetidas import , const e var .

Agora se você tem, por exemplo
try( x = foo(...)) go zee(...) defer fum() y = bar(...) )
Isso é equivalente a escrever
try(x = foo(...)) go zee(...) defer fum() try(y = bar(...))
Fatorar tudo isso em um bloco try o torna menos ocupado.

Considerar
try(x = foo())
Se foo() não retornar um valor de erro, isso é equivalente a
x = foo()

Considerar
try(f, _ := os.open(filename))
Como o valor de erro retornado é ignorado, isso é equivalente a apenas
f, _ := os.open(filename)

Considerar
try(f, err := os.open(filename))
Como o valor de erro retornado não é ignorado, isso é equivalente a
f, err := os.open(filename) if err != nil { return ..., err }
Conforme especificado atualmente na proposta.

E também organiza muito bem as tentativas aninhadas!

Aqui está um link para a proposta alternativa que mencionei acima:

Ele exige a adição de dois (2) recursos de linguagem pequenos, mas de uso geral, para abordar os mesmos casos de uso que try()

  1. Capacidade de chamar um func /closure em uma declaração de atribuição.
  2. Capacidade de break , continue ou return mais de um nível.

Com esses dois recursos, eles não seriam _"mágicos"_ e acredito que seu uso produziria um código Go mais fácil de entender e mais alinhado com o código Go idiomático com o qual todos estamos familiarizados.

Eu li a proposta e realmente gosto de onde tentar está indo.

Dado o quão prevalente a tentativa será, eu me pergunto se torná-lo um comportamento mais padrão tornaria mais fácil de manusear.

Considere mapas. Isso é válido:

v := m[key]

como é este:

v, ok := m[key]

E se tratarmos os erros exatamente da maneira que o try sugere, mas removermos o builtin. Então, se começamos com:

v, err := fn()

Em vez de escrever:

v := try(fn())

Em vez disso, poderíamos escrever:

v := fn()

Quando o valor err não é capturado, ele é tratado exatamente como o try. Levaria um pouco para se acostumar, mas parece muito semelhante a v, ok := m[key] e v, ok := x.(string) . Basicamente, qualquer erro não tratado faz com que a função retorne e o valor de erro seja definido.

Para voltar às conclusões dos documentos de design e requisitos de implementação:

• A sintaxe do idioma é mantida e nenhuma palavra-chave nova é introduzida
• Continua a ser açúcar sintático como tentar e espero que seja fácil de explicar.
• Não requer nova sintaxe
• Deve ser totalmente compatível com versões anteriores.

Imagino que isso teria quase os mesmos requisitos de implementação que try, pois a principal diferença é, em vez do builtin disparar o açúcar sintático, agora é a ausência do campo err.

Então, usando o exemplo CopyFile da proposta junto com defer fmt.HandleErrorf(&err, "copy %s %s", src, dst) , obtemos:

func CopyFile(src, dst string) (err error) {
        defer fmt.HandleErrorf(&err, "copy %s %s", src, dst)

        r := os.Open(src)
        defer r.Close()

        w := os.Create(dst)
        defer func() {
                err := w.Close()
                if err != nil {
                        os.Remove(dst) // only if a “try” fails
                }
        }()

        io.Copy(w, r)
        w.Close()
        return nil
}

@savaki Eu gosto disso e estava pensando no que seria necessário para fazer o Go inverter o tratamento de erros sempre manipulando erros por padrão e deixar o programador especificar quando não fazer isso (capturando o erro em uma variável), mas total falta de qualquer identificador tornaria o código difícil de seguir, pois não seria possível ver todos os pontos de retorno. Pode ser uma convenção para nomear funções que poderiam retornar um erro de forma diferente poderia funcionar (como capitalizar identificadores públicos). Pode ser que se uma função retornou um erro, ela deve sempre terminar com, digamos ? . Então Go pode sempre tratar o erro implicitamente e retorná-lo automaticamente para a função de chamada assim como try. Isso o torna muito semelhante a algumas propostas sugerindo usar um identificador ? em vez de tentar, mas uma diferença importante é que aqui ? seria parte do nome da função e não um identificador adicional. Na verdade, uma função que retornou error como o último valor de retorno nem compilaria se não tivesse o sufixo ? . Claro que ? é arbitrário e pode ser substituído por qualquer outra coisa que torne a intenção mais explícita. operation?() seria equivalente a envolver try(someFunc()) mas ? faria parte do nome da função e seu único propósito seria indicar que a função pode retornar um erro assim como a capitalização a primeira letra de uma variável.

Isso superficialmente acaba sendo muito semelhante a outras propostas que pedem para substituir try por ? mas uma diferença crítica é que torna o tratamento de erros implícito (automático) e, em vez disso, torna explícito ignorar (ou encapsular) erros que uma espécie de melhor prática de qualquer maneira. O problema mais óbvio com isso, é claro, é que não é compatível com versões anteriores e tenho certeza de que há muitos mais.

Dito isso, eu estaria muito interessado em ver como o Go pode fazer o tratamento de erros do caso padrão/implícito automatizando-o e deixando o programador escrever um pouco de código extra para ignorar/substituir o tratamento. O desafio que eu acho é como tornar todos os pontos de retorno óbvios neste caso, porque sem isso os erros se tornarão mais como exceções no sentido de que eles podem vir de qualquer lugar, pois o fluxo do programa não o tornaria óbvio. Pode-se dizer que cometer erros implícitos com o indicador visual é o mesmo que implementar try e tornar errcheck uma falha do compilador.

poderíamos fazer algo como exceções c++ com decoradores para funções antigas?

func some_old_test() (int, error){
    return 0, errors.New("err1")
}
func some_new_test() (int){
        if true {
             return 1
        }
    throw errors.New("err2")
}
func throw_res(int, e error) int {
    if e != nil {
        throw e
    }
    return int
}
func main() {
    fmt.Println("Hello, playground")
    try{
        i := throw_res(some_old_test())
        fmt.Println("i=", i + some_new_test())
    } catch(err io.Error) {
        return err
    } catch(err error) {
        fmt.Println("unknown err", err)
    }
}

@owais Eu estava pensando que a semântica seria exatamente a mesma que tentar, então pelo menos você precisaria declarar o tipo de erro. Então, se começamos com:

func foo() error {
  _, err := fn() 
  if err != nil {
    return err
  }

  return nil
} 

Se eu entendi a proposta try, basta fazer isso:

func foo() error {
  _  := fn() 
  return nil
} 

não compilaria. Uma boa vantagem é que dá à compilação a oportunidade de dizer ao usuário o que está faltando. Algo no sentido de que o uso de tratamento de erros implícito requer que o tipo de retorno de erro seja nomeado, err.

Isso, então, funcionaria:

func foo() (err error) {
  _  := fn() 
  return nil
} 

por que não apenas lidar com o caso de um erro que não é atribuído a uma variável.

  • remova a necessidade de retornos nomeados, o compilador pode fazer isso sozinho.
  • permite adicionar contexto.
  • lida com o caso de uso comum.
  • compatível com versões anteriores
  • não interage estranhamente com defer, loops ou switches.

retorno implícito para o caso if err != nil, o compilador pode gerar o nome da variável local para retornos, se necessário, não pode ser acessado pelo programador.
pessoalmente, não gosto deste caso em particular do ponto de vista da legibilidade do código

f := os.Open("foo.txt")

prefere um retorno explícito, segue o código é mais lido do que o mantra escrito

f := os.Open("foo.txt") else return

Curiosamente, poderíamos aceitar as duas formas e fazer com que o gofmt adicionasse automaticamente o else return.

adicionando contexto, também nomeação local da variável. return se torna explícito porque queremos adicionar contexto.

f := os.Open("foo.txt") else err {
  return errors.Wrap(err, "some context")
}

adicionando contexto com vários valores de retorno

f := os.Open("foo.txt") else err {
  return i, j, errors.Wrap(err, "some context")
}

funções aninhadas requerem que as funções externas tratem todos os resultados na mesma ordem
menos o erro final.

bits := ioutil.ReadAll(os.Open("foo")) else err {
  // either error ends up here.
  return i, j, errors.Wrap(err, "some context")
}

compilador recusa a compilação devido ao valor de retorno de erro ausente na função

func foo(s string) int {
   i := strconv.Atoi(s) // cannot implicitly return error due to missing error return value for foo.
   return i * 2
}

compila alegremente porque o erro é explicitamente ignorado.

func foo(s string) int {
   i, _ := strconv.Atoi(s)
   return i * 2
} 

compilador está feliz. ele ignora o erro como faz atualmente porque não ocorre nenhuma atribuição ou sufixo.

func foo() error {
  return errors.New("whoops")
}

func bar() {
  foo()
}

dentro de um loop você pode usar continue.

for _, s := range []string{"1","2","3","4","5","6"} {
  i := strconv.Atoi(s) else continue
}

edit: substituiu ; por else

@savaki Acho que entendi seu comentário original e gosto da ideia de Go lidar com erros por padrão, mas não acho viável sem adicionar algumas alterações de sintaxe adicionais e, uma vez que fazemos isso, torna-se surpreendentemente semelhante à proposta atual.

A maior desvantagem do que você propõe é que ele não expõe todos os pontos de onde uma função pode retornar ao contrário do if err != nil {return err} atual ou da função try apresentada nesta proposta. Mesmo que funcionasse exatamente da mesma maneira sob o capô, visualmente o código seria muito diferente. Ao ler o código, não haveria como saber quais chamadas de função podem retornar um erro. Isso acabaria sendo uma experiência pior do que exceções IMO.

Pode ser que o tratamento de erros possa ser tornado implícito se o compilador forçar alguma convenção semântica em funções que possam retornar erros. Como eles devem começar ou terminar com uma determinada frase ou personagem. Isso tornaria todos os pontos de retorno muito óbvios e acho que seria melhor do que o tratamento manual de erros, mas não tenho certeza de quão significativamente melhor, considerando que já existem verificações de lint que exigem carga quando detectam um erro sendo ignorado. Seria muito interessante ver se o compilador pode forçar as funções a serem nomeadas de uma certa maneira dependendo se elas podem retornar possíveis erros.

A principal desvantagem dessa abordagem é que o parâmetro de resultado do erro precisa ser nomeado, possivelmente levando a APIs menos bonitas (mas veja as perguntas frequentes sobre esse assunto). Acreditamos que nos acostumaremos com isso uma vez que esse estilo se estabeleça.

Não tenho certeza se algo assim já foi sugerido antes, não consigo encontrar aqui ou na proposta. Você considerou outra função interna que retorna um ponteiro para o valor de retorno de erro da função atual?
por exemplo:

func example() error {
        var err *error = funcerror() // always return a non-nil pointer
        fmt.Print(*err) // always nil if the return parameters are not named and not in a defer

        defer func() {
                err := funcerror()
                fmt.Print(*err) // "x"
        }

        return errors.New("x")
}
func exampleNamed() (err error) {
        funcErr := funcerror()
        fmt.Print(*funcErr) // "nil"

        err = errors.New("x")
        fmt.Print(*funcErr) // "x", named return parameter is reflected even before return is called

        *funcErr = errors.New("y")
        fmt.Print(err) // "y", unfortunate side effect?

        defer func() {
                funcErr := funcerror()
                fmt.Print(*funcErr) // "z"
                fmt.Print(err) // "z"
        }

        return errors.New("z")
}

uso com try:

func CopyFile(src, dst string) (error) {
        defer func() {
                err := funcerror()
                if *err != nil {
                        *err = fmt.Errorf("copy %s %s: %v", src, dst, err)
                }
        }()
        // one liner alternative
        // defer fmt.HandleErrorf(funcerror(), "copy %s %s", src, dst)

        r := try(os.Open(src))
        defer r.Close()

        w := try(os.Create(dst))
        defer func() {
                w.Close()
                err := funcerror()
                if *err != nil {
                        os.Remove(dst) // only if a “try” fails
                }
        }()

        try(io.Copy(w, r))
        try(w.Close())
        return nil
}

Alternativamente funcerror (o nome é um trabalho em andamento :D ) pode retornar nil se não for chamado dentro de defer.

Outra alternativa é que funcerror retorne uma interface "Errorer" para torná-la somente leitura:

type interface Errorer() {
        Error() error
}

@savaki Na verdade, gosto da sua proposta de omitir try() e permitir que seja mais como testar um mapa ou uma declaração de tipo. Isso parece muito mais _"Go-like."_

No entanto, ainda há um problema gritante que vejo, e essa é a sua proposta presume que todos os erros usando essa abordagem acionarão um return e deixarão a função. O que não contempla é emitir um break do atual for ou um continue do atual for .

Os primeiros return s são uma marreta quando muitas vezes um bisturi é a melhor escolha.

Portanto, afirmo que break e continue devem ser estratégias válidas de tratamento de erros e, atualmente, sua proposta pressupõe apenas return enquanto try() presume isso ou chamar um erro handler que por si só pode return , não break ou continue .

Parece que o savaki e eu tivemos ideias semelhantes, apenas adicionei a semântica do bloco para lidar com o erro, se desejado. Por exemplo, adicionando comtexto, para loops onde você deseja curto-circuito, etc.

@mikeschinkel veja minha extensão, ele e eu tivemos ideias semelhantes, apenas a estendi com uma instrução de bloco opcional

@james-lawrence

@mikesckinkel veja minha extensão, ele e eu tivemos ideias semelhantes, apenas a estendi com uma instrução de bloco opcional

Tomando seu exemplo:

f := os.Open("foo.txt"); err {
  return errors.Wrap(err, "some context")
}

O que se compara ao que fazemos hoje:

f,err := os.Open("foo.txt"); 
if err != nil {
  return errors.Wrap(err, "some context")
}

É definitivamente preferível para mim. Exceto que tem alguns problemas:

  1. err parece ser _"magicamente"_ declarado. A magia deve ser minimizada, não? Então vamos declarar:
f, err := os.Open("foo.txt"); err {
  return errors.Wrap(err, "some context")
}
  1. Mas isso ainda não funciona porque Go não interpreta valores nil como false nem valores de ponteiro como true , então precisaria ser:
f, err := os.Open("foo.txt"); err != nil {
  return errors.Wrap(err, "some context")
}

E o que isso funciona, começa a parecer muito trabalho e muita sintaxe em uma linha, então e eu posso continuar a fazer o caminho antigo para maior clareza.

Mas e se o Go adicionasse dois (2) built-ins; iserror() e error() ? Então poderíamos fazer isso, o que não me parece tão ruim:

f := os.Open("foo.txt"); iserror() {
  return errors.Wrap(error(), "some context")
}

Ou melhor _(algo como):_

f := os.Open("foo.txt"); iserror() {
  return error().Extend("some context")
}

O que você e os outros pensam?

Como um aparte, verifique a ortografia do meu nome de usuário. Eu não teria sido notificado de sua menção se eu não estivesse prestando atenção de qualquer maneira ...

@mikeschinkel desculpe pelo nome que eu estava no meu telefone e o github não estava sugerindo automaticamente.

err parece ser declarado "magicamente". A magia deve ser minimizada, não? Então vamos declarar:

meh, toda a ideia de inserir automaticamente um retorno é mágica. esta não é a coisa mais mágica acontecendo em toda esta proposta. Além disso, eu argumentaria que o erro foi declarado; apenas no final, dentro do contexto de um bloco com escopo, evitando que ele polua o escopo pai enquanto ainda mantém todas as coisas boas que obtemos normalmente usando instruções if.

Em geral, estou muito feliz com o tratamento de erros do go com as próximas adições ao pacote de erros. Não vejo nada nesta proposta como super útil. Estou apenas tentando oferecer o ajuste mais natural para o golang se estivermos decididos a fazê-lo.

_"toda a ideia de inserir um retorno automaticamente é mágica."_

Você não terá nenhum argumento meu lá.

_"isso não é a coisa mais mágica acontecendo em toda esta proposta."_

Acho que estava tentando argumentar que _"toda magia é problemática."_

_"Além disso, eu diria que o erro foi declarado; apenas no final dentro do contexto de um bloco com escopo..."_

Então, se eu quisesse chamá-lo de err2 , isso também funcionaria?

f := os.Open("foo.txt"); err2 {
  return errors.Wrap(err, "some context")
}

Portanto, suponho que você também esteja propondo tratamento de caso especial do err / err2 após o ponto e vírgula, ou seja, que seria assumido nil ou não nil em vez de bool como ao verificar um mapa?

if _,ok := m[a]; !ok {
   print("there is no 'a' in 'm'")
}

Em geral, estou muito feliz com o tratamento de erros do go com as próximas adições ao pacote de erros.

Eu também estou feliz com o tratamento de erros, quando combinado com break e continue _(mas não return .)_

Do jeito que está, vejo essa proposta try() como mais prejudicial do que útil, e prefiro não ver nada do que esse implemento conforme proposto. #jmtcw.

@beoran @mikeschinkel Anteriormente, sugeri que não poderíamos implementar esta versão de try usando genéricos, porque isso altera o fluxo de controle. Se estou lendo corretamente, vocês dois estão sugerindo que podemos usar genéricos para implementar try fazendo com que ele chame panic . Mas esta versão de try muito explicitamente não panic . Portanto, não podemos usar genéricos para implementar esta versão de try .

Sim, poderíamos usar genéricos (uma versão de genéricos significativamente mais poderosa do que a do rascunho de design em https://go.googlesource.com/proposal/+/master/design/go2draft-contracts.md) para escrever uma função que entra em pânico no erro. Mas entrar em pânico com o erro não é o tipo de tratamento de erro que os programadores Go escrevem hoje, e não me parece uma boa ideia.

O tratamento especial do @mikeschinkel seria que o bloco executasse apenas quando ocorresse um erro.
```
f := os.Open('foo'); err { return err } // err sempre seria não nulo aqui.

@ianlancetaylor

_"Sim, poderíamos usar genéricos... Mas entrar em pânico com o erro não é o tipo de tratamento de erro que os programadores Go escrevem hoje, e não me parece uma boa ideia."_

Na verdade, concordo fortemente com você sobre isso, portanto, parece que você pode ter interpretado mal a intenção do meu comentário. Eu não estava sugerindo que a equipe Go implementaria o tratamento de erros que usava panic() — claro que não.

Em vez disso, eu estava tentando realmente seguir sua liderança de muitos de seus comentários anteriores sobre outras questões e sugeri que evitássemos fazer quaisquer alterações no Go que não fossem absolutamente necessárias, porque elas são possíveis no userland . Então _se_ os genéricos foram endereçados _então_ as pessoas que gostariam try() poderiam de fato implementá-lo por conta própria, embora alavancando panic() . E esse seria um recurso a menos que a equipe precisaria adicionar e documentar para Go.

O que eu não estava fazendo - e talvez isso não estivesse claro - era defender que as pessoas realmente usassem panic() para implementar try() , apenas que poderiam se realmente quisessem, e tivessem os recursos de genéricos.

Isso esclarece?

Para mim, chamar panic , seja como for, é bem diferente desta proposta para try . Então, embora eu ache que entendo o que você está dizendo, não concordo que sejam equivalentes. Mesmo que tivéssemos genéricos poderosos o suficiente para implementar uma versão de try que entre em pânico, acho que ainda haveria um desejo razoável pela versão de try apresentada nesta proposta.

@ianlancetaylor Reconhecido. Novamente, eu estava procurando um motivo pelo qual try() não precisasse ser adicionado em vez de encontrar uma maneira de adicioná-lo. Como eu disse acima, eu prefiro não ter nada de novo para tratamento de erros do que ter try() como proposto aqui.

Eu pessoalmente gostei mais da proposta check anterior, baseada em aspectos puramente visuais; check tinha o mesmo poder que este try() mas bar(check foo()) é mais legível para mim do que bar(try(foo())) (eu só precisava de um segundo para contar os parênteses!).

Mais importante, minha principal reclamação sobre handle / check era que ele não permitia empacotar cheques individuais de maneiras diferentes - e agora essa proposta try() tem a mesma falha, enquanto invoca recursos complicados e confusos para novatos de adiamentos e retornos nomeados. E com handle pelo menos tivemos a opção de usar escopos para definir blocos handle, com defer nem isso é possível.

No que me diz respeito, esta proposta perde para a proposta anterior handle / check em todos os aspectos.

Aqui está outra preocupação com o uso de adiamentos para tratamento de erros.

try é uma saída controlada/intencional de uma função. adias são executados sempre, incluindo saídas não controladas/não intencionais das funções. Essa incompatibilidade pode causar confusão. Aqui está um cenário imaginário:

func someHTTPHandlerGuts() (err error) {
  defer func() {
    recordMetric("db call failed")
    return fmt.HandleErrorf("db unavailable: %v", err)
  }()
  data := try(makeDBCall)
  // some code that panics due to a bug
  return nil
}

Lembre-se de que net/http se recupera de panics e imagine depurar um problema de produção em torno do panic. Você olharia para sua instrumentação e veria um pico nas falhas de chamadas de banco de dados, das chamadas recordMetric . Isso pode mascarar o verdadeiro problema, que é o pânico na linha subsequente.

Não tenho certeza de quão séria é essa preocupação na prática, mas é (infelizmente) talvez outra razão para pensar que adiar não é um mecanismo ideal para tratamento de erros.

Aqui está uma modificação que pode ajudar com algumas das preocupações levantadas: Trate try como goto ao invés de return . Me ouça. :)

try seria açúcar sintático para:

t1, … tn, te := f()  // t1, … tn, te are local (invisible) temporaries
if te != nil {
        err = te   // assign te to the error result parameter
        goto error // goto "error" label
}
x1, … xn = t1, … tn  // assignment only if there was no error

Benefícios:

  • defer não é necessário para decorar erros. (No entanto, os retornos nomeados ainda são necessários.)
  • A existência do rótulo error: é uma pista visual de que existe um try em algum lugar da função.

Isso também fornece um mecanismo para adicionar manipuladores que evitam os problemas do manipulador como função: Use rótulos como manipuladores. try(fn(), wrap) seria goto wrap em vez de goto error . O compilador pode confirmar que wrap: está presente na função. Observe que ter manipuladores também ajuda na depuração: você pode adicionar/alterar o manipulador para fornecer um caminho de depuração.

Código de amostra:

func CopyFile(src, dst string) (err error) {
    r := try(os.Open(src))
    defer r.Close()

    w := try(os.Create(dst))
    defer func() {
        w.Close()
        if err != nil {
            os.Remove(dst) // only if a “try” fails
        }
    }()

    try(io.Copy(w, r), copyfail)
    try(w.Close())
    return nil

error:
    return fmt.Errorf("copy %s %s: %v", src, dst, err)

copyfail:
    recordMetric("copy failure") // count incidents of this failure
    return fmt.Errorf("copy %s %s: %v", src, dst, err)
}

Outros comentários:

  • Podemos exigir que qualquer rótulo usado como destino de um try seja precedido por uma instrução final. Na prática, isso os forçaria ao fim da função e poderia impedir algum código de espaguete. Por outro lado, pode impedir alguns usos razoáveis ​​e úteis.
  • try pode ser usado para criar um loop. Acho que isso se enquadra na bandeira de "se dói, não faça", mas não tenho certeza.
  • Isso exigiria a correção de https://github.com/golang/go/issues/26058.

Crédito: acredito que uma variante dessa ideia foi sugerida pela primeira vez por @griesemer pessoalmente na GopherCon no ano passado.

@josharian Pensar sobre a interação com panic é importante aqui, e fico feliz que você tenha mencionado isso, mas seu exemplo me parece estranho. No código a seguir, não faz sentido para mim que o defer sempre registre uma métrica "db call failed" . Seria uma métrica falsa se someHTTPHandlerGuts obtiver sucesso e retornar nil . O defer é executado em todos os casos de saída, não apenas em casos de erro ou pânico, então o código parece errado mesmo que não haja pânico.

func someHTTPHandlerGuts() (err error) {
  defer func() {
    recordMetric("db call failed")
    return fmt.HandleErrorf("db unavailable: %v", err)
  }()
  data := try(makeDBCall)
  // some code that panics due to a bug
  return nil
}

@josharian Sim, esta é mais ou menos exatamente a versão que discutimos no ano passado (exceto que usamos check em vez de try ). Eu acho que seria crucial que não se pudesse pular "de volta" para o resto do corpo da função, uma vez que estamos no rótulo error . Isso garantiria que o goto fosse um pouco "estruturado" (sem código de espaguete possível). Uma preocupação que foi levantada foi que o rótulo do manipulador de erros (o error: ) sempre terminaria no final da função (caso contrário, seria necessário pular de alguma forma). Pessoalmente, eu gosto do código de tratamento de erros fora do caminho (no final), mas outros acharam que ele deveria estar visível logo no início.

@mikeshenkel eu vejo o retorno de um loop como um sinal positivo em vez de negativo. Meu palpite é que isso encorajaria os desenvolvedores a usar uma função separada para lidar com o conteúdo de um loop ou usar explicitamente err como fazemos atualmente. Ambos parecem bons resultados para mim.

Do meu ponto de vista, não sinto que essa sintaxe try tenha que lidar com todos os casos de uso, assim como não sinto que preciso usar o

V, ok:= m[chave]

Formulário da leitura de um mapa

Você pode evitar que os rótulos goto forcem os manipuladores para o final da função ressuscitando a proposta handle / check de uma forma simplificada. E se usássemos a sintaxe handle err { ... } mas não deixássemos os manipuladores encadearem, em vez disso, apenas o último fosse usado. Ele simplifica muito essa proposta, e é muito parecido com a ideia goto, exceto que coloca o manuseio mais próximo do ponto de uso.

func CopyFile(src, dst string) (err error) {
    handle err {
        return fmt.Errorf("copy %s %s: %v", src, dst, err)
    }
    r := check os.Open(src)
    defer r.Close()

    w := check os.Create(dst)
    defer func() {
        w.Close()
        if err != nil {
            os.Remove(dst) // only if a “check” fails
        }
    }()

    {
        // handlers are scoped, after this scope the original handle is used again.
        // as an alternative, we could have repeated the first handle after the io.Copy,
        // or come up with a syntax to name the handlers, though that's often not useful.
        handle err {
            recordMetric("copy failure") // count incidents of this failure
            return fmt.Errorf("copy %s %s: %v", src, dst, err)
        }
        check io.Copy(w, r)
    }
    check w.Close()
    return nil
}

Como bônus, isso tem um caminho futuro para permitir que os manipuladores encadeem, pois todos os usos existentes teriam um retorno.

@josharian @griesemer se você introduzir manipuladores nomeados (que muitas respostas para verificar/tratar solicitadas, veja temas recorrentes ), existem opções de sintaxe preferíveis a try(f(), err) :

try.err f()
try?err f()
try#err f()

?err    f() // because 'try' is redundant
?return f() // no handler
?panic  f() // no handler

(?err f()).method()

f?err() // lead with function name, instead of error handling
f?err().method()

file, ?err := os.Open(...) // many check/handle responses also requested this style

Uma das coisas que mais gosto no Go é que sua sintaxe é relativamente livre de pontuação e pode ser lida em voz alta sem grandes problemas. Eu realmente odiaria que Go acabasse como $#@!perl .

Para mim, fazer "experimentar" uma função interna e habilitar cadeias tem 2 problemas:

  • É inconsistente com o restante do fluxo de controle em go (por exemplo, palavras-chave for/if/return/etc).
  • Isso torna o código menos legível.

Eu preferiria fazer uma declaração sem parênteses. Os exemplos na proposta exigiriam várias linhas, mas se tornariam mais legíveis (ou seja, instâncias individuais de "experimentar" seriam mais difíceis de perder). Sim, isso quebraria os analisadores externos, mas prefiro preservar a consistência.

O operador ternário é outro lugar onde ir não tem algo e requer mais teclas, mas ao mesmo tempo melhora a legibilidade/manutenção. Adicionar "tentar" nesta forma mais restrita equilibrará melhor expressividade versus legibilidade, IMO.

FWIW, panic afeta o fluxo de controle e tem parênteses, mas go e defer também afetam o fluxo e não. Eu costumo pensar que try é mais parecido com defer porque é uma operação de fluxo incomum e dificultar a execução de try (try os.Open(file)).Read(buf) é bom porque queremos desencorajar one-liners enfim, mas tanto faz. Qualquer um serve.

Sugestão que todos vão odiar por um nome implícito para uma variável de retorno de erro final: $err . É melhor do que try() IMO. :-)

@griesemer

_"Pessoalmente, eu gosto do código de tratamento de erros fora do caminho (no final)"_

+1 para isso!

Acho que o tratamento de erros implementado _antes_ do erro ocorrer é muito mais difícil de raciocinar do que o tratamento de erros implementado _depois_ do erro ocorrer. Ter que pular mentalmente para trás e forçar a seguir o fluxo lógico parece que estou de volta a 1980 escrevendo Basic com GOTOs.

Deixe-me propor outra maneira potencial de lidar com erros usando CopyFile() como exemplo novamente:

func CopyFile(src, dst string) (err error) {

    r := os.Open(src)
    defer r.Close()

    w := os.Create(dst)
    defer w.Close()

    io.Copy(w, r)
    w.Close()

    for err := error {
        switch err.Source() {
        case w.Close:
            os.Remove(dst) // only if a “try” fails
            fallthrough
        case os.Open, os.Create, io.Copy:
            err = fmt.Errorf("copy %s %s: %v", src, dst, err)
        default:
            err = fmt.Errorf("an unexpected error occurred")
        }
    }

    return err
}

As alterações de idioma necessárias seriam:

  1. Permite uma construção for error{} , semelhante a for range{} mas apenas inserida em caso de erro e executada apenas uma vez.

  2. Permite omitir a captura de valores de retorno que implementam <object>.Error() string mas somente quando existe uma construção for error{} dentro do mesmo func .

  3. Faça com que o fluxo de controle do programa salte para a primeira linha da construção for error{} quando um func retornar um _"error"_ em seu último valor de retorno.

  4. Ao retornar um _"erro"_ Go adicionaria atribuir uma referência à função que retornou o erro que deve ser recuperável por <error>.Source()

O que é um _"erro"_?

Atualmente um _"error"_ é definido como qualquer objeto que implementa Error() string e, claro, não é nil .

No entanto, muitas vezes há a necessidade de estender o erro _mesmo em caso de sucesso_ para permitir o retorno de valores necessários para resultados de sucesso de uma API RESTful. Portanto, peço que a equipe Go não assuma automaticamente que err!=nil significa _"erro"_, mas verifique se um objeto de erro implementa um IsError() e se IsError() retorna true antes de assumir que qualquer valor diferente nil é um _"erro."_

_(Não estou necessariamente falando de código na biblioteca padrão, mas principalmente se você escolher seu fluxo de controle para ramificar em um _"erro"_. Se você olhar apenas para err!=nil , ficaremos muito limitados no que pode fazer em termos de valores de retorno em nossas funções.)_

BTW, permitir que todos testem um _"erro"_ da mesma maneira provavelmente poderia ser feito mais facilmente adicionando uma nova função interna iserror() :

type ErrorIser interface {
    IsError() bool
}
func iserror(err error) bool {
    if err == nil { 
        return false
    }
    if _,ok := err.(ErrorIser); !ok {
        return true
    }
    return err.IsError()
}

Um benefício colateral de permitir a não captura de _"erros"_

Observe que permitir a não captura do último _"error"_ de chamadas func permitiria a refatoração posterior para retornar erros de func s que inicialmente não precisavam retornar erros. E isso permitiria essa refatoração sem quebrar nenhum código existente que usa essa forma de recuperação de erros e chama os ditos func s.

Para mim, essa decisão de _"Devo retornar um erro ou renunciar ao tratamento de erros para chamar a simplicidade?"_ é um dos meus maiores dilemas ao escrever código Go. Permitir a não captura de _"erros"_ acima eliminaria esse dilema.

Na verdade, tentei implementar essa ideia como tradutor Go cerca de meio ano atrás. Não tenho uma opinião forte se esse recurso deve ser adicionado como Go embutido, mas deixe-me compartilhar a experiência (embora não tenha certeza de que seja útil).

https://github.com/rhysd/trygo

Chamei a linguagem estendida de TryGo e implementei o tradutor TryGo to Go.

Com o tradutor, o código

func CreateFileInSubdir(subdir, filename string, content []byte) error {
    cwd := try(os.Getwd())

    try(os.Mkdir(filepath.Join(cwd, subdir)))

    p := filepath.Join(cwd, subdir, filename)
    f := try(os.Create(p))
    defer f.Close()

    try(f.Write(content))

    fmt.Println("Created:", p)
    return nil
}

pode ser traduzido em

func CreateFileInSubdir(subdir, filename string, content []byte) error {
    cwd, _err0 := os.Getwd()
    if _err0 != nil {
        return _err0
    }

    if _err1 := os.Mkdir(filepath.Join(cwd, subdir)); _err1 != nil {
        return _err1
    }

    p := filepath.Join(cwd, subdir, filename)
    f, _err2 := os.Create(p)
    if _err2 != nil {
        return _err2
    }
    defer f.Close()

    if _, _err3 := f.Write(content); _err3 != nil {
        return _err3
    }

    fmt.Println("Created:", p)
    return nil
}

Pela restrição de idioma, não consegui implementar a chamada genérica try() . É restrito a

  • RHS da declaração de definição
  • RHS da declaração de atribuição
  • Extrato de chamada

mas eu poderia tentar isso com meu pequeno projeto. Minha experiência foi

  • basicamente funciona bem e salva várias linhas
  • o valor de retorno nomeado é realmente inutilizável para err pois o valor de retorno de sua função é determinado pela atribuição e pela função especial try() . muito confuso
  • esta função try() não tinha o recurso 'erro de encapsulamento' conforme discutido acima.

_"Ambos parecem bons resultados para mim."_

Teremos que concordar em discordar aqui.

_"esta sintaxe try (não precisa) lidar com todos os casos de uso"_

Esse meme é provavelmente o mais preocupante. Pelo menos considerando a resistência da equipe/comunidade Go a quaisquer mudanças no passado que não sejam amplamente aplicáveis.

Se permitirmos essa justificativa aqui, por que não podemos revisitar propostas anteriores que foram rejeitadas porque não eram amplamente aplicáveis?

E agora estamos abertos para argumentar por mudanças no Go que são úteis apenas para casos extremos selecionados?

Na minha opinião, estabelecer este precedente não produzirá bons resultados a longo prazo...

_"@mikeshenkel"_

PS Eu não vi sua mensagem no início por causa de um erro de ortografia. _(isso não me ofende, só não sou notificado quando meu nome de usuário é digitado incorretamente...)_

Eu aprecio o compromisso com a compatibilidade com versões anteriores que motiva você a tornar try um builtin, em vez de uma palavra-chave, mas depois de lutar com a _estranheza_ total de ter uma função usada com frequência que pode alterar o fluxo de controle ( panic e recover são extremamente raros), fiquei me perguntando: alguém fez alguma análise em larga escala da frequência de try como identificador em bases de código de código aberto? Eu estava curioso e cético, então fiz uma pesquisa preliminar no seguinte:

Nas 11.108.770 linhas significativas de Go que vivem nesses repositórios, havia apenas 63 instâncias de try sendo usadas como identificador. Claro, eu percebo que essas bases de código (embora grandes, amplamente usadas e importantes por si só) representam apenas uma fração do código Go lá fora e, além disso, não temos como analisar diretamente as bases de código privadas, mas certamente é um resultado interessante.

Além disso, como try , como qualquer palavra-chave, é minúscula, você nunca a encontrará na API pública de um pacote. As adições de palavras-chave afetarão apenas as partes internas do pacote.

Isso tudo é o prefácio de algumas ideias que eu queria colocar no mix que se beneficiariam de try como palavra-chave.

Eu proporia as seguintes construções.

1) Sem manipulador

// The existing proposal, but as a keyword rather than builtin.  When an error is 
// "caught", the function returns all zero values plus the error.  Nothing 
// particularly new here.
func doSomething() (int, error) {
    try SomeFunc()
    a, b := try AnotherFunc()

    // ...

    return 123, nil
}

2) Manipulador

Observe que os manipuladores de erros são blocos de código simples, destinados a serem embutidos, em vez de funções. Mais sobre isso abaixo.

func doSomething() (int, error) {
    // Inline error handler
    a, b := try SomeFunc() else err {
        return 0, errors.Wrap(err, "error in doSomething:")
    }

    // Named error handlers
    handler logAndContinue err {
        log.Errorf("non-critical error: %v", err)
    }
    handler annotateAndReturn err {
        return 0, errors.Wrap(err, "error in doSomething:")
    }

    c, d := try SomeFunc() else logAndContinue
    e, f := try OtherFunc() else annotateAndReturn

    // ...

    return 123, nil
}

Restrições propostas:

  • Você só pode try uma chamada de função. Não try err .
  • Se você não especificar um manipulador, poderá apenas try de dentro de uma função que retorne um erro como seu valor de retorno mais à direita. Não há mudança em como try se comporta com base em seu contexto. Ele nunca entra em pânico (como discutido muito anteriormente no tópico).
  • Não existe nenhuma "cadeia de manipuladores" de qualquer tipo. Os manipuladores são apenas blocos de código inlineáveis.

Benefícios:

  • A sintaxe try / else poderia ser trivialmente desaçucarada no "composto if" existente:
    go a, b := try SomeFunc() else err { return 0, errors.Wrap(err, "error in doSomething:") }
    torna-se
    go if a, b, err := SomeFunc(); err != nil { return 0, errors.Wrap(err, "error in doSomething:") }
    A meu ver, ifs compostos sempre pareceram mais confusos do que úteis por uma razão muito simples: condicionais geralmente ocorrem _depois_ de uma operação e têm algo a ver com o processamento de seus resultados. Se a operação estiver inserida dentro da instrução condicional, é simplesmente menos óbvio que está acontecendo. O olho está distraído. Além disso, o escopo das variáveis ​​definidas não é tão óbvio quanto quando elas estão mais à esquerda em uma linha.
  • Os manipuladores de erros não são intencionalmente definidos como funções (nem com nada que se assemelhe à semântica de função). Isso faz várias coisas para nós:

    • O compilador pode simplesmente inline um manipulador nomeado onde quer que seja referido. É muito mais como um modelo simples de macro/codegen do que uma chamada de função. O tempo de execução nem precisa saber que existem manipuladores.

    • Não estamos limitados em relação ao que podemos fazer dentro de um manipulador. Contornamos a crítica de check / handle que "essa estrutura de tratamento de erros é boa apenas para resgates". Também contornamos a crítica da "cadeia de manipuladores". Qualquer código arbitrário pode ser colocado dentro de um desses manipuladores e nenhum outro fluxo de controle está implícito.

    • Não precisamos sequestrar return dentro do manipulador para significar super return . Seqüestrar uma palavra-chave é extremamente confuso. return significa apenas return , e não há necessidade real de super return .

    • defer não precisa trabalhar como um mecanismo de tratamento de erros. Podemos continuar a pensar nisso principalmente como uma forma de limpar recursos, etc.

  • Sobre adicionar contexto aos erros:

    • Adicionar contexto com manipuladores é extremamente simples e se parece muito com os blocos if err != nil existentes

    • Mesmo que a construção "tente sem manipulador" não encoraje diretamente a adição de contexto, é muito simples refatorar no formulário do manipulador. Seu uso pretendido seria principalmente durante o desenvolvimento, e seria extremamente simples escrever uma verificação de go vet para destacar erros não tratados.

Desculpe se essas ideias são muito parecidas com outras propostas — tentei acompanhar todas elas, mas posso ter perdido um bom negócio.

@brynbellomy Obrigado pela análise de palavras-chave - é uma informação muito útil. Parece que try como palavra-chave pode estar ok. (Você diz que as APIs não são afetadas - isso é verdade, mas try ainda pode aparecer como nome de parâmetro ou algo parecido - então a documentação pode ter que mudar. Mas eu concordo que isso não afetaria os clientes desses pacotes.)

Em relação à sua proposta: ficaria muito bem mesmo sem manipuladores nomeados, não é? (Isso simplificaria a proposta sem perda de energia. Pode-se simplesmente chamar uma função local do manipulador embutido.)

Em relação à sua proposta: ficaria muito bem mesmo sem manipuladores nomeados, não é? (Isso simplificaria a proposta sem perda de energia. Pode-se simplesmente chamar uma função local do manipulador embutido.)

@griesemer De fato - eu estava me sentindo bastante morno em incluir isso. Certamente mais Go-ish sem.

Por outro lado, parece que as pessoas querem a capacidade de fazer tratamento de erros de uma linha, incluindo frases que return . Um caso típico seria log, então return . Se desembolsar para uma função local na cláusula else , provavelmente perderemos isso:

a, b := try SomeFunc() else err {
    someLocalFunc(err)
    return 0, err
}

(Eu ainda prefiro isso a ifs compostos, no entanto)

No entanto, você ainda pode obter retornos de uma linha que adicionam contexto de erro implementando um simples ajuste gofmt discutido anteriormente no tópico:

a, b := try SomeFunc() else err { return 0, errors.Wrap(err, "bad stuff!") }

A nova palavra-chave é necessária na proposta acima? Por que não:

SomeFunc() else return
a, b := SomeOtherFunc() else err { return 0, errors.Wrap(err, "bad stuff!") }

@griesemer se os manipuladores estiverem de volta à mesa, sugiro que você crie um novo problema para discussão de try/handle ou try/_label_. Essa proposta omitiu especificamente os manipuladores e existem inúmeras maneiras de defini-los e invocá-los.

Qualquer pessoa que sugira manipuladores deve primeiro ler o wiki de feedback de verificação/manuseio. As chances são boas de que o que você sonha já está descrito lá :-)
https://github.com/golang/go/wiki/Go2ErrorHandlingFeedback

@smonkewitz não, uma nova palavra-chave não é necessária nessa versão, pois está vinculada a instruções de atribuição, que foram mencionadas várias vezes até agora em vários açúcares de sintaxe.

https://github.com/golang/go/issues/32437#issuecomment -499808741
https://github.com/golang/go/issues/32437#issuecomment -499852124
https://github.com/golang/go/issues/32437#issuecomment -500095505

@ianlancetaylor esse tipo específico de tratamento de erros já foi considerado pela equipe go? Não é tão fácil de implementar quanto o try proposto, mas parece mais idiomático. ~declaração desnecessária, desculpe.~

Gostaria de repetir algo que @deanveloper e alguns outros disseram, mas com minha própria ênfase. Em https://github.com/golang/go/issues/32437#issuecomment -498939499 @deanveloper disse:

try é um retorno condicional. O fluxo de controle E os retornos são mantidos em pedestais em Go. Todo o fluxo de controle dentro de uma função é recuado e todos os retornos começam com return . Misturar esses dois conceitos em uma chamada de função fácil de perder parece um pouco estranho.

Além disso, nesta proposta try é uma função que retorna valores, portanto pode ser usada como parte de uma expressão maior.

Alguns argumentaram que panic já estabeleceu o precedente para uma função incorporada que altera o fluxo de controle, mas acho que panic é fundamentalmente diferente por dois motivos:

  1. O pânico não é condicional; ele sempre aborta a função de chamada.
  2. Panic não retorna nenhum valor e, portanto, só pode aparecer como uma declaração independente, o que aumenta sua visibilidade.

Tente por outro lado:

  1. É condicional; ele pode ou não retornar da função de chamada.
  2. Retorna valores e pode aparecer em uma expressão composta, possivelmente várias vezes, em uma única linha, potencialmente além da margem direita da minha janela do editor.

Por essas razões, acho que try parece mais do que um "pouco fora", acho que prejudica fundamentalmente a legibilidade do código.

Hoje, quando encontramos algum código Go pela primeira vez, podemos vasculhá-lo rapidamente para encontrar os possíveis pontos de saída e controlar os pontos de fluxo. Eu acredito que é uma propriedade altamente valiosa do código Go. Usando try fica muito fácil escrever código sem essa propriedade.

Admito que é provável que os desenvolvedores de Go que valorizam a legibilidade do código convergiriam em idiomas de uso para try que evitam essas armadilhas de legibilidade. Espero que isso aconteça, pois a legibilidade do código parece ser um valor fundamental para muitos desenvolvedores de Go. Mas não é óbvio para mim que try agrega valor suficiente sobre os idiomas de código existentes para carregar o peso de adicionar um novo conceito à linguagem para que todos aprendam e isso pode facilmente prejudicar a legibilidade.

````
se != "quebrou" {
não conserte (isso)
}

@ChrisHines Para o seu ponto (que é repetido em outro lugar neste tópico), vamos adicionar outra restrição:

  • qualquer instrução try (mesmo aquelas sem um manipulador) deve ocorrer em sua própria linha.

Você ainda se beneficiaria de uma grande redução no ruído visual. Então, você tem retornos garantidos anotados por return e retornos condicionais anotados por try , e essas palavras-chave sempre ficam no início de uma linha (ou na pior das hipóteses, diretamente após uma atribuição de variável).

Então, nada desse tipo de bobagem:

try EmitEvent(try (try DecodeMsg(m)).SaveToDB())

mas sim isso:

dm := try DecodeMsg(m)
um := try dm.SaveToDB()
try EmitEvent(um)

que ainda parece mais claro do que isso:

dm, err := DecodeMsg(m)
if err != nil {
    return nil, err
}

um, err := dm.SaveToDB()
if err != nil {
    return nil, err
}

err = EmitEvent(um)
if err != nil {
    return nil, err
}

Uma coisa que eu gosto neste design é que é impossível ignorar silenciosamente os erros sem ainda anotar que um pode ocorrer . Considerando que agora, você às vezes vê x, _ := SomeFunc() (qual é o valor de retorno ignorado? um erro? algo mais?), agora você tem que anotar claramente:

x := try SomeFunc() else err {}

Desde meu post anterior em apoio à proposta, vi duas ideias postadas por @jagv (parameterless try retorna *error ) e por @josharian (rotulados manipuladores de erros) que acredito em um forma ligeiramente modificada melhoraria consideravelmente a proposta.

Juntando essas ideias com outra que eu mesmo tive, teríamos quatro versões de try :

  1. experimentar()
  2. tente(parâmetros)
  3. try(params, label)
  4. try(params, panic)

1 simplesmente retornaria um ponteiro para o parâmetro de retorno de erro (ERP) ou nil se não houvesse um (somente #4). Isso forneceria uma alternativa a um ERP nomeado sem a necessidade de adicionar mais um integrado.

2 funcionaria exatamente como atualmente previsto. Um erro não nulo seria retornado imediatamente, mas poderia ser decorado por uma instrução defer .

3 funcionaria como sugerido por @josharian , ou seja, em um erro não nulo, o código seria ramificado para o rótulo. No entanto, não haveria nenhum rótulo de manipulador de erro padrão, pois esse caso agora degeneraria em #2.

Parece-me que essa geralmente será uma maneira melhor de decorar erros (ou manipulá-los localmente e retornar nil) do que defer , pois é mais simples e rápido. Quem não gostou ainda pode usar o número 2.

Seria uma prática recomendada colocar o rótulo/código de tratamento de erros próximo ao final da função e não voltar para o restante do corpo da função. No entanto, não acho que o compilador deva aplicar, pois pode haver ocasiões estranhas em que eles são úteis e a aplicação pode ser difícil em qualquer caso.

Portanto, o rótulo normal e o comportamento goto aplicariam o assunto (como @josharian disse) ao #26058 sendo corrigido primeiro, mas acho que deve ser corrigido de qualquer maneira.

O nome do rótulo não pode ser panic , pois isso entraria em conflito com #4.

4 seria panic imediatamente em vez de retornar ou ramificar. Conseqüentemente, se esta fosse a única versão de try usada em uma função específica, nenhum ERP seria necessário.

Eu adicionei isso para que o pacote de teste possa funcionar como funciona agora, sem a necessidade de outras alterações internas ou outras. No entanto, também pode ser útil em outros cenários _fatal_.

Isso precisa ser uma versão separada de try , pois a alternativa de ramificar para um manipulador de erros e entrar em pânico com isso ainda exigiria um ERP.

Um dos tipos mais fortes de reações à proposta inicial foi a preocupação em perder a visibilidade fácil do fluxo normal de onde uma função retorna.

Por exemplo, @deanveloper expressou essa preocupação muito bem em https://github.com/golang/go/issues/32437#issuecomment -498932961, que eu acho que é o comentário mais votado aqui.

@dominikh escreveu em https://github.com/golang/go/issues/32437#issuecomment -499067357:

No código gofmt'ed, um return sempre corresponde a /^\t*return / – é um padrão muito trivial para detectar a olho nu, sem qualquer assistência. try, por outro lado, pode ocorrer em qualquer lugar no código, aninhado arbitrariamente nas chamadas de função. Nenhuma quantidade de treinamento nos tornará capazes de identificar imediatamente todo o fluxo de controle em uma função sem a ajuda da ferramenta.

Para ajudar com isso, @brynbellomy sugeriu ontem:

qualquer instrução try (mesmo aquelas sem um manipulador) deve ocorrer em sua própria linha.

Levando isso adiante, o try pode ser necessário para ser o início da linha, mesmo para uma atribuição.

Então pode ser:

try dm := DecodeMsg(m)
try um := dm.SaveToDB()
try EmitEvent(um)

em vez do seguinte (do exemplo de @brynbellomy ):

dm, err := DecodeMsg(m)
if err != nil {
    return nil, err
}

um, err := dm.SaveToDB()
if err != nil {
    return nil, err
}

err = EmitEvent(um)
if err != nil {
    return nil, err
}

Isso parece preservar uma boa quantidade de visibilidade, mesmo sem qualquer editor ou assistência de IDE, enquanto ainda reduz o clichê.

Isso pode funcionar com a abordagem baseada em adiamento proposta atualmente que depende de parâmetros de resultado nomeados ou pode funcionar com a especificação de funções de manipulador normais. (Especificar funções de manipulador sem exigir valores de retorno nomeados me parece melhor do que exigir valores de retorno nomeados, mas esse é um ponto separado).

A proposta inclui este exemplo:

info := try(try(os.Open(file)).Stat())    // proposed try built-in

Isso poderia ser em vez disso:

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

Isso ainda é uma redução no clichê comparado ao que alguém pode escrever hoje, mesmo que não seja tão curto quanto a sintaxe proposta. Talvez isso seria suficientemente curto?

@elagergren-spideroak forneceu este exemplo:

try(try(try(to()).parse().this)).easily())

Eu acho que tem parênteses incompatíveis, o que talvez seja um ponto deliberado ou um pouco de humor sutil, então não tenho certeza se esse exemplo pretende ter 2 try ou 3 try . De qualquer forma, talvez seja melhor exigir que isso seja distribuído em 2-3 linhas que comecem com try .

@thepudds , era isso que eu queria chegar no meu comentário anterior. Exceto aquele dado

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

Uma coisa óbvia a fazer é pensar em try como um bloco try onde mais de uma frase pode ser colocada entre parênteses. Assim, o acima pode se tornar

try (
    f := os.Open(file)
    into := f.Stat()
)

Se o compilador souber como lidar com isso, a mesma coisa funciona para o aninhamento também. Então agora o acima pode se tornar

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

A partir de assinaturas de funções o compilador sabe que Open pode retornar um valor de erro e como está em um bloco try, ele precisa gerar tratamento de erros e então chamar Stat() no valor retornado primário e assim por diante.

A próxima coisa é permitir instruções onde não há nenhum valor de erro sendo gerado ou é tratado localmente. Então agora você pode dizer

try (
    f := os.Open(file)
    debug("f: %v\n", f) // debug returns nothing
    into := f.Stat()
)

Isso permite a evolução do código sem reorganizar os blocos try. Mas, por alguma estranha razão, as pessoas parecem pensar que o tratamento de erros deve ser explicado explicitamente! Eles querem

try(try(try(to()).parse()).this)).easily())

Enquanto eu estou perfeitamente bem com

try to().parse().this().easily()

Mesmo que em ambos os casos exatamente o mesmo código de verificação de erros possa ser gerado. Minha opinião é que você sempre pode escrever um código especial para tratamento de erros, se precisar. try (ou como você preferir chamá-lo) simplesmente organiza o tratamento de erros padrão (que é direcioná-lo para o chamador).

Outro benefício é que, se o compilador gerar o tratamento de erros padrão, ele poderá adicionar mais algumas informações de identificação para que você saiba qual das quatro funções acima falhou.

Eu estava um pouco preocupado com a legibilidade de programas onde try aparece dentro de outras expressões. Então eu corri grep "return .*err$" na biblioteca padrão e comecei a ler os blocos aleatoriamente. Existem 7214 resultados, eu li apenas algumas centenas.

A primeira coisa a ser observada é que onde try se aplica, torna quase todos esses blocos um pouco mais legíveis.

A segunda coisa é que muito poucos deles, menos de 1 em 10, colocariam try dentro de outra expressão. O caso típico são declarações da forma x := try(...) ou ^try(...)$ .

Aqui estão alguns exemplos em que try apareceria dentro de outra expressão:

texto/modelo

func gt(arg1, arg2 reflect.Value) (bool, error) {
        // > is the inverse of <=.
        lessOrEqual, err := le(arg1, arg2)
        if err != nil {
                return false, err
        }
        return !lessOrEqual, nil
}

torna-se:

func gt(arg1, arg2 reflect.Value) (bool, error) {
        // > is the inverse of <=.
        return !try(le(arg1, arg2)), nil
}

texto/modelo

func index(item reflect.Value, indices ...reflect.Value) (reflect.Value, error) {
...
        switch v.Kind() {
        case reflect.Map:
                index, err := prepareArg(index, v.Type().Key())
                if err != nil {
                        return reflect.Value{}, err
                }
                if x := v.MapIndex(index); x.IsValid() {
                        v = x
                } else {
                        v = reflect.Zero(v.Type().Elem())
                }
        ...
        }
}

torna-se

func index(item reflect.Value, indices ...reflect.Value) (reflect.Value, error) {
        ...
        switch v.Kind() {
        case reflect.Map:
                if x := v.MapIndex(try(prepareArg(index, v.Type().Key()))); x.IsValid() {
                        v = x
                } else {
                        v = reflect.Zero(v.Type().Elem())
                }
        ...
        }
}

(este é o exemplo mais questionável que vi)

regexp/sintaxe:

regexp/syntax/parse.go

func Parse(s string, flags Flags) (*Regexp, error) {
        ...
        if c, t, err = nextRune(t); err != nil {
                return nil, err
        }
        p.literal(c)
        ...
}

torna-se

func Parse(s string, flags Flags) (*Regexp, error) {
        ...
        c, t = try(nextRune(t))
        p.literal(c)
        ...
}

Este não é um exemplo de try dentro de outra expressão, mas quero chamá-lo porque melhora a legibilidade. É muito mais fácil ver aqui que os valores de c e t estão além do escopo da instrução if.

net/http

net/http/request.go:readRequest

        mimeHeader, err := tp.ReadMIMEHeader()
        if err != nil {
                return nil, err
        }
        req.Header = Header(mimeHeader)

torna-se:

        req.Header = Header(try(tp.ReadMIMEHeader())

banco de dados/sql

        if driverCtx, ok := driveri.(driver.DriverContext); ok {
                connector, err := driverCtx.OpenConnector(dataSourceName)
                if err != nil {
                        return nil, err
                }
                return OpenDB(connector), nil
        }

torna-se

        if driverCtx, ok := driveri.(driver.DriverContext); ok {
                return OpenDB(try(driverCtx.OpenConnector(dataSourceName))), nil
        }

banco de dados/sql

        si, err := ctxDriverPrepare(ctx, dc.ci, query)
        if err != nil {
                return nil, err
        }
        ds := &driverStmt{Locker: dc, si: si}

torna-se

        ds := &driverStmt{
                Locker: dc,
                si: try(ctxDriverPrepare(ctx, dc.ci, query)),
        }

net/http

        if http2isconnectioncloserequest(req) && dialonmiss {
                // it gets its own connection.
                http2tracegetconn(req, addr)
                const singleuse = true
                cc, err := p.t.dialclientconn(addr, singleuse)
                if err != nil {
                        return nil, err
                }
                return cc, nil
        }

torna-se

        if http2isconnectioncloserequest(req) && dialonmiss {
                // it gets its own connection.
                http2tracegetconn(req, addr)
                const singleuse = true
                return try(p.t.dialclientconn(addr, singleuse))
        }

net/http

func (f *http2Framer) endWrite() error {
        ...
        n, err := f.w.Write(f.wbuf)
        if err == nil && n != len(f.wbuf) {
                err = io.ErrShortWrite
        }
        return err
}

torna-se

func (f *http2Framer) endWrite() error {
        ...
        if try(f.w.Write(f.wbuf) != len(f.wbuf) {
                return io.ErrShortWrite
        }
        return nil
}

(Esse eu gosto muito.)

net/http

        if f, err := fr.ReadFrame(); err != nil {
                return nil, err
        } else {
                hc = f.(*http2ContinuationFrame) // guaranteed by checkFrameOrder
        }

torna-se

        hc = try(fr.ReadFrame()).(*http2ContinuationFrame)// guaranteed by checkFrameOrder

}

(Legal também.)

líquido :

        if ctrlFn != nil {
                c, err := newRawConn(fd)
                if err != nil {
                        return err
                }
                if err := ctrlFn(fd.ctrlNetwork(), laddr.String(), c); err != nil {
                        return err
                }
        }

torna-se

        if ctrlFn != nil {
                try(ctrlFn(fd.ctrlNetwork(), laddr.String(), try(newRawConn(fd))))
        }

talvez isso seja demais e, em vez disso, deveria ser:

        if ctrlFn != nil {
                c := try(newRawConn(fd))
                try(ctrlFn(fd.ctrlNetwork(), laddr.String(), c))
        }

No geral, gosto bastante do efeito de try no código de biblioteca padrão que li.

Um ponto final: Ver try aplicado para ler código além dos poucos exemplos da proposta foi esclarecedor. Acho que vale a pena considerar escrever uma ferramenta para converter automaticamente o código para usar try (onde não altera a semântica do programa). Seria interessante ler uma amostra dos diffs produzidos em relação a pacotes populares no github para ver se o que encontrei na biblioteca padrão se mantém. A saída de tal programa poderia fornecer uma visão extra sobre o efeito da proposta.

@crawshaw obrigado por fazer isso, foi ótimo vê-lo em ação. Mas vê-lo em ação me fez levar mais a sério os argumentos contra o tratamento de erros inline que até agora eu vinha descartando.

Como isso estava tão próximo da sugestão interessante do @thepudds de fazer try uma declaração, reescrevi todos os exemplos usando essa sintaxe e achei muito mais claro do que a expressão try ou o status quo, sem exigir muitas linhas extras:

func gt(arg1, arg2 reflect.Value) (bool, error) {
        // > is the inverse of <=.
        try lessOrEqual := le(arg1, arg2)
        return !lessOrEqual, nil
}
func index(item reflect.Value, indices ...reflect.Value) (reflect.Value, error) {
        ...
        switch v.Kind() {
        case reflect.Map:
                try index := prepareArg(index, v.Type().Key())
                if x := v.MapIndex(index); x.IsValid() {
                        v = x
                } else {
                        v = reflect.Zero(v.Type().Elem())
                }
        ...
        }
}
func Parse(s string, flags Flags) (*Regexp, error) {
        ...
        try c, t = nextRune(t)
        p.literal(c)
        ...
}
        try mimeHeader := tp.ReadMIMEHeader()
        req.Header = Header(mimeHeader)
        if driverCtx, ok := driveri.(driver.DriverContext); ok {
                try connector := driverCtx.OpenConnector(dataSourceName)
                return OpenDB(connector), nil
        }

Este seria sem dúvida melhor com uma expressão- try se houvesse vários campos que tivessem que ser try -ed, mas eu ainda prefiro o equilíbrio dessa troca

        try si := ctxDriverPrepare(ctx, dc.ci, query)
        ds := &driverStmt{Locker: dc, si: si}

Este é basicamente o pior caso para isso e parece bom:

        if http2isconnectioncloserequest(req) && dialonmiss {
                // it gets its own connection.
                http2tracegetconn(req, addr)
                const singleuse = true
                try cc := p.t.dialclientconn(addr, singleuse)
                return cc, nil
        }

Eu debati comigo mesmo se if try seria ou deveria ser legal, mas não consegui encontrar uma explicação razoável por que não deveria ser e funciona muito bem aqui:

func (f *http2Framer) endWrite() error {
        ...
        if try n := f.w.Write(f.wbuf); n != len(f.wbuf) {
                return io.ErrShortWrite
        }
        return nil
}
        try f := fr.ReadFrame()
        hc = f.(*http2ContinuationFrame) // guaranteed by checkFrameOrder
        if ctrlFn != nil {
                try c := newRawConn(fd)
                try ctrlFn(fd.ctrlNetwork(), laddr.String(), c)
        }

Analisar os exemplos de @crawshaw só me faz sentir mais seguro de que o fluxo de controle será muitas vezes enigmático o suficiente para ser ainda mais cuidadoso com o design. Relacionar até mesmo uma pequena quantidade de complexidade torna-se difícil de ler e fácil de estragar. Fico feliz em ver as opções consideradas, mas complicar o fluxo de controle em uma linguagem tão protegida parece excepcionalmente fora do personagem.

func (f *http2Framer) endWrite() error {
        ...
        if try(f.w.Write(f.wbuf) != len(f.wbuf) {
                return io.ErrShortWrite
        }
        return nil
}

Além disso, try não está "tentando" nada. É um "relé de proteção". Se a semântica básica da proposta estiver errada, não me surpreende que o código resultante também seja problemático.

func (f *http2Framer) endWrite() error {
        ...
        relay n := f.w.Write(f.wbuf)
        return checkShortWrite(n, len(f.wbuf))
}

Se você fizer try uma declaração, poderá usar um sinalizador para indicar qual valor de retorno e qual ação:

try c, @      := newRawConn(fd) // return
try c, <strong i="6">@panic</strong> := newRawConn(fd) // panic
try c, <strong i="7">@hname</strong> := newRawConn(fd) // invoke named handler
try c, <strong i="8">@_</strong>     := newRawConn(fd) // ignore, or invoke "ignored" handler if defined

Você ainda precisa de uma sintaxe de subexpressão (Russ afirmou que é um requisito), pelo menos para ações de pânico e ignorar.

Em primeiro lugar, aplaudo @crawshaw por dedicar um tempo para analisar cerca de 200 exemplos reais e dedicar um tempo para seu relato atencioso acima.

Segundo, @jimmyfrasche , em relação à sua resposta aqui sobre o exemplo http2Framer :


Eu debati comigo mesmo se if try seria ou deveria ser legal, mas não consegui encontrar uma explicação razoável por que não deveria ser e funciona muito bem aqui:

```
func (f *http2Framer) endWrite() erro {
...
se tentar n := fwWrite(f.wbuf); n != len(f.wbuf) {
return io.ErrShortWrite
}
retorno nulo
}

At least under what I was suggesting above in https://github.com/golang/go/issues/32437#issuecomment-500213884, under that proposal variation I would suggest `if try` is not allowed.

That `http2Framer` example could instead be:

func (f *http2Framer) endWrite() erro {
...
tente n := fwWrite(f.wbuf)
if n != len(f.wbuf) {
return io.ErrShortWrite
}
retorno nulo
}
`` That is one line longer, but hopefully still "light on the page". Personally, I think that (arguably) reads more cleanly, but more importantly it is easier to see the tentar`.

@deanveloper escreveu acima em https://github.com/golang/go/issues/32437#issuecomment -498932961:

Retornar de uma função parece ter sido uma coisa "sagra" a se fazer

Esse exemplo específico http2Framer acaba não sendo tão curto quanto poderia ser. No entanto, ele mantém o retorno de uma função mais "sacra" se try deve ser a primeira coisa em uma linha.

@crawshaw mencionou:

A segunda coisa é que muito poucos deles, menos de 1 em 10, colocariam try dentro de outra expressão. O caso típico são declarações da forma x := try(...) ou ^try(...)$.

Talvez não haja problema em ajudar apenas parcialmente esses 1 em 10 exemplos com uma forma mais restrita de try , especialmente se o caso típico desses exemplos terminar com a mesma contagem de linhas, mesmo que try seja obrigado a ser a primeira coisa em uma linha?

@jimmyfrasche

@crawshaw obrigado por fazer isso, foi ótimo vê-lo em ação. Mas vê-lo em ação me fez levar mais a sério os argumentos contra o tratamento de erros inline que até agora eu vinha descartando.

Como isso estava tão próximo da sugestão interessante do @thepudds de fazer try uma declaração, reescrevi todos os exemplos usando essa sintaxe e achei muito mais claro do que a expressão try ou o status quo, sem exigir muitas linhas extras:

func gt(arg1, arg2 reflect.Value) (bool, error) {
        // > is the inverse of <=.
        try lessOrEqual := le(arg1, arg2)
        return !lessOrEqual, nil
}

Seu primeiro exemplo ilustra bem porque eu prefiro fortemente a expressão- try . Na sua versão, tenho que colocar o resultado da chamada para le em uma variável, mas essa variável não tem significado semântico que o termo le já não implique. Portanto, não há nenhum nome que eu possa dar que não seja sem sentido (como x ) ou redundante (como lessOrEqual ). Com expression- try , nenhuma variável intermediária é necessária, então esse problema nem surge.

Prefiro não ter que gastar esforço mental inventando nomes para coisas que é melhor deixar no anonimato.

Fico feliz em dar meu apoio aos últimos posts em que try (a palavra-chave) foi movida para o início da linha. Ele realmente deve compartilhar o mesmo espaço visual que return .

Re: A sugestão de @jimmyfrasche de permitir try dentro de instruções compostas if , é exatamente o tipo de coisa que acho que muitos aqui estão tentando evitar, por alguns motivos:

  • ele combina dois mecanismos de fluxo de controle muito diferentes em uma única linha
  • a expressão try é realmente avaliada primeiro e pode fazer com que a função retorne, mas ela aparece após o if
  • eles retornam com erros totalmente diferentes, um dos quais não vemos no código e outro que fazemos
  • torna menos óbvio que o try não é manipulado, porque o bloco se parece muito com um bloco manipulador (mesmo que esteja lidando com um problema totalmente diferente)

Pode-se abordar esta situação de um ângulo ligeiramente diferente que favorece empurrar as pessoas para lidar com try s. Que tal permitir que a sintaxe try / else contenha condicionais subsequentes (que é um padrão comum com muitas funções de E/S que retornam err e n , qualquer um dos quais pode indicar um problema):

func (f *http2Framer) endWrite() error {
        // ...
        try n := f.w.Write(f.wbuf) else err {
                return errors.Wrap(err, "error writing:")
        } else if n != len(f.wbuf) {
                return io.ErrShortWrite
        }
        return nil
}

No caso de você não lidar com o erro retornado por .Write , você ainda teria uma anotação clara de que .Write pode errar (como apontado por @thepudds):

func (f *http2Framer) endWrite() error {
        // ...
        try n := f.w.Write(f.wbuf)
        if n != len(f.wbuf) {
                return io.ErrShortWrite
        }
        return nil
}

Eu apoio a resposta de @daved . Na minha opinião, cada exemplo que @crawshaw destacou se tornou menos claro e mais propenso a erros como resultado de try .

Fico feliz em dar meu apoio aos últimos posts em que try (a palavra-chave) foi movida para o início da linha. Ele realmente deve compartilhar o mesmo espaço visual que return .

Dadas as duas opções para este ponto e assumindo que uma foi escolhida e, portanto, estabeleceu um precedente para futuros recursos potenciais:

UMA.)

try f := os.Open(filepath) else err {
    return errors.Wrap(err, "can't open")
}

B.)

f := try os.Open(filepath) else err {
    return errors.Wrap(err, "can't open")
}

Qual dos dois oferece mais flexibilidade para uso futuro de novas palavras-chave? _(Não sei a resposta para isso, pois não domino a arte sombria de escrever compiladores.)_ Uma abordagem seria mais limitante do que outra?

@davecheney @daved @crawshaw
Eu tenderia a concordar com os Daves sobre isso: nos exemplos de @crawshaw , há muitas instruções try embutidas nas linhas que têm muitas outras coisas acontecendo. Realmente difícil identificar pontos de saída. Além disso, os parênteses try parecem atrapalhar bastante as coisas em alguns dos exemplos.

Ver um monte de código stdlib transformado assim é muito útil, então peguei os mesmos exemplos, mas os reescrevi de acordo com a proposta alternativa, que é mais restritiva:

  • try como palavra-chave
  • apenas um try por linha
  • try deve estar no início de uma linha

Espero que isso nos ajude a comparar. Pessoalmente, acho que esses exemplos parecem muito mais concisos do que os originais, mas sem obscurecer o fluxo de controle. try permanece muito visível em qualquer lugar em que é usado.

texto/modelo

func gt(arg1, arg2 reflect.Value) (bool, error) {
        // > is the inverse of <=.
        lessOrEqual, err := le(arg1, arg2)
        if err != nil {
                return false, err
        }
        return !lessOrEqual, nil
}

torna-se:

func gt(arg1, arg2 reflect.Value) (bool, error) {
        // > is the inverse of <=.
        try lessOrEqual := le(arg1, arg2)
        return !lessOrEqual, nil
}

texto/modelo

func index(item reflect.Value, indices ...reflect.Value) (reflect.Value, error) {
...
        switch v.Kind() {
        case reflect.Map:
                index, err := prepareArg(index, v.Type().Key())
                if err != nil {
                        return reflect.Value{}, err
                }
                if x := v.MapIndex(index); x.IsValid() {
                        v = x
                } else {
                        v = reflect.Zero(v.Type().Elem())
                }
        ...
        }
}

torna-se

func index(item reflect.Value, indices ...reflect.Value) (reflect.Value, error) {
        ...
        switch v.Kind() {
        case reflect.Map:
                try index := prepareArg(index, v.Type().Key())
                if x := v.MapIndex(index); x.IsValid() {
                        v = x
                } else {
                        v = reflect.Zero(v.Type().Elem())
                }
        ...
        }
}

regexp/sintaxe:

regexp/syntax/parse.go

func Parse(s string, flags Flags) (*Regexp, error) {
        ...
        if c, t, err = nextRune(t); err != nil {
                return nil, err
        }
        p.literal(c)
        ...
}

torna-se

func Parse(s string, flags Flags) (*Regexp, error) {
        ...
        try c, t = nextRune(t)
        p.literal(c)
        ...
}

net/http

net/http/request.go:readRequest

        mimeHeader, err := tp.ReadMIMEHeader()
        if err != nil {
                return nil, err
        }
        req.Header = Header(mimeHeader)

torna-se:

        try mimeHeader := tp.ReadMIMEHeader()
        req.Header = Header(mimeHeader)

banco de dados/sql

        if driverCtx, ok := driveri.(driver.DriverContext); ok {
                connector, err := driverCtx.OpenConnector(dataSourceName)
                if err != nil {
                        return nil, err
                }
                return OpenDB(connector), nil
        }

torna-se

        if driverCtx, ok := driveri.(driver.DriverContext); ok {
                try connector := driverCtx.OpenConnector(dataSourceName)
                return OpenDB(connector), nil
        }

banco de dados/sql

        si, err := ctxDriverPrepare(ctx, dc.ci, query)
        if err != nil {
                return nil, err
        }
        ds := &driverStmt{Locker: dc, si: si}

torna-se

        try si := ctxDriverPrepare(ctx, dc.ci, query)
        ds := &driverStmt{Locker: dc, si: si}

net/http

        if http2isconnectioncloserequest(req) && dialonmiss {
                // it gets its own connection.
                http2tracegetconn(req, addr)
                const singleuse = true
                cc, err := p.t.dialclientconn(addr, singleuse)
                if err != nil {
                        return nil, err
                }
                return cc, nil
        }

torna-se

        if http2isconnectioncloserequest(req) && dialonmiss {
                // it gets its own connection.
                http2tracegetconn(req, addr)
                const singleuse = true
                try cc := p.t.dialclientconn(addr, singleuse)
                return cc, nil
        }

net/http
Este não nos salva nenhuma linha, mas acho muito mais claro porque if err == nil é uma construção relativamente incomum.

func (f *http2Framer) endWrite() error {
        ...
        n, err := f.w.Write(f.wbuf)
        if err == nil && n != len(f.wbuf) {
                err = io.ErrShortWrite
        }
        return err
}

torna-se

func (f *http2Framer) endWrite() error {
        ...
        try n := f.w.Write(f.wbuf)
        if n != len(f.wbuf) {
                return io.ErrShortWrite
        }
        return nil
}

net/http

        if f, err := fr.ReadFrame(); err != nil {
                return nil, err
        } else {
                hc = f.(*http2ContinuationFrame) // guaranteed by checkFrameOrder
        }

torna-se

        try f := fr.ReadFrame()
        hc = f.(*http2ContinuationFrame) // guaranteed by checkFrameOrder
}

internet:

        if ctrlFn != nil {
                c, err := newRawConn(fd)
                if err != nil {
                        return err
                }
                if err := ctrlFn(fd.ctrlNetwork(), laddr.String(), c); err != nil {
                        return err
                }
        }

torna-se

        if ctrlFn != nil {
                try c := newRawConn(fd)
                try ctrlFn(fd.ctrlNetwork(), laddr.String(), c)
        }

@james-lawrence Em resposta a https://github.com/golang/go/issues/32437#issuecomment -500116099 : Não me lembro de ideias como , err opcionais sendo seriamente consideradas, não. Pessoalmente, acho que é uma má ideia, porque significa que se uma função for alterada para adicionar um parâmetro error à direita, o código existente continuará a compilar, mas agirá de maneira muito diferente.

Usar defer para lidar com os erros faz muito sentido, mas leva à necessidade de nomear o erro e um novo tipo de clichê if err != nil .

Os manipuladores externos precisam fazer isso:

func handler(err *error) {
  if *err != nil {
    *err = handle(*err)
  }
} 

que é usado como

defer handler(&err)

Os manipuladores externos precisam ser escritos apenas uma vez, mas seria necessário haver duas versões de muitas funções de tratamento de erros: a que deve ser adiada e a que deve ser usada de maneira regular.

Os manipuladores internos precisam fazer isso:

defer func() {
  if err != nil {
    err = handle(err)
  }
}()

Em ambos os casos, o erro da função externa deve ser nomeado para ser acessado.

Como mencionei anteriormente no tópico, isso pode ser abstraído em uma única função:

func catch(err *error, handle func(error) error) {
  if *err != nil && handle != nil {
    *err = handle(*err)
  }
}

Isso vai contra a preocupação de @griesemer sobre a ambiguidade das funções do manipulador nil e tem seu próprio clichê defer e func(err error) error , além de ter que nomear err na função externa.

Se try acabar como uma palavra-chave, pode fazer sentido ter uma palavra-chave catch , a ser descrita abaixo também.

Sintaticamente, seria muito parecido com handle :

catch err {
  return handleThe(err)
}

Semanticamente, seria açúcar para o código do manipulador interno acima:

defer func() {
  if err != nil {
    err = handleThe(err)
  }
}()

Como é um pouco mágico, pode pegar o erro da função externa, mesmo que não tenha sido nomeada. (O err depois catch é mais como um nome de parâmetro para o bloco catch ).

catch teria a mesma restrição que try que deve estar em uma função que tenha um retorno de erro final, pois ambos são açúcares que dependem disso.

Isso não é nem de longe tão poderoso quanto a proposta original handle , mas evitaria o requisito de nomear um erro para lidar com ele e removeria o novo clichê discutido acima para manipuladores internos, tornando fácil o suficiente para não requerem versões separadas de funções para manipuladores externos.

O tratamento de erros complicado pode exigir não usar catch da mesma forma que pode exigir não usar try .

Como ambos são açúcar, não há necessidade de usar catch com try . Os manipuladores catch são executados sempre que a função retorna um erro não nil , permitindo, por exemplo, manter algum registro rápido:

catch err {
  log.Print(err)
  return err
}

ou apenas envolvendo todos os erros retornados:

catch err {
  return fmt.Errorf("foo: %w", err)
}

@ianlancetaylor

_"Acho que é uma má ideia, porque significa que, se uma função for alterada para adicionar um parâmetro error à direita, o código existente continuará a compilar, mas agirá de maneira muito diferente."_

Essa é provavelmente a maneira correta de olhar para isso, se você puder controlar o código upstream e downstream para que, se precisar alterar uma assinatura de função para também retornar um erro, poderá fazê-lo.

Mas eu pediria para você considerar o que acontece quando alguém não controla upstream ou downstream de seus próprios pacotes? E também considerar os casos de uso em que os erros podem ser adicionados e o que acontece se os erros precisarem ser adicionados, mas você não puder forçar a alteração do código downstream?

Você pode pensar em um exemplo em que alguém mudaria a assinatura para adicionar um valor de retorno? Para mim, eles normalmente se enquadram na categoria de _"Não percebi que um erro ocorreria"_ ou _"Estou me sentindo preguiçoso e não quero me esforçar porque o erro provavelmente não acontecerá". _

Em ambos os casos, posso adicionar um retorno de erro porque fica aparente que um erro precisa ser tratado. Quando isso acontecer, se eu não puder alterar a assinatura porque não quero quebrar a compatibilidade de outros desenvolvedores usando meus pacotes, o que fazer? Meu palpite é que na grande maioria das vezes o erro irá ocorrer e que o código que chamou a função que não retorna o erro agirá de forma bem diferente, _de qualquer forma._

Na verdade, raramente faço o último, mas com muita frequência faço o primeiro. Mas notei que pacotes de terceiros frequentemente ignoram a captura de erros onde eles deveriam estar, e eu sei disso porque quando eu trago seu código nos sinalizadores GoLand em laranja brilhante todas as instâncias. Eu adoraria poder enviar solicitações de pull para adicionar tratamento de erros aos pacotes que uso muito, mas se eu fizer isso, a maioria não os aceitará porque eu estaria quebrando suas assinaturas de código.

Ao não oferecer uma maneira compatível com versões anteriores de adicionar erros a serem retornados por funções, os desenvolvedores que distribuem código e se preocupam em não quebrar as coisas para seus usuários não poderão evoluir seus pacotes para incluir o tratamento de erros como deveriam.


Talvez, em vez de considerar o problema como o código agirá de forma diferente, veja o problema como um desafio de engenharia sobre como minimizar a desvantagem de um método que não está capturando ativamente um erro? Isso teria um valor mais amplo e de longo prazo.

Por exemplo, considere adicionar um manipulador de erros de pacote que deve ser definido antes de poder ignorar erros?


Para ser franco, o idioma de Go de retornar erros, além de valores de retorno regulares, foi uma de suas melhores inovações. Mas, como muitas vezes acontece quando você melhora as coisas, muitas vezes expõe outras fraquezas e eu argumentarei que o tratamento de erros de Go não inovou o suficiente.

Nós, Gophers, nos tornamos mergulhados em retornar um erro em vez de lançar uma exceção, então a pergunta que tenho é _"Por que não deveríamos retornar erros de todas as funções?"_ Nem sempre fazemos isso porque escrever código sem tratamento de erros é mais conveniente do que codificar com ele. Portanto, omitimos o tratamento de erros quando achamos que podemos nos livrar dele. Mas frequentemente adivinhamos errado.

Então, realmente, se fosse possível descobrir como tornar o código elegante e legível, eu argumentaria que valores de retorno e erros realmente deveriam ser tratados separadamente, e que _toda_ função deveria ter a capacidade de retornar erros independentemente de suas assinaturas de função anteriores. E obter o código existente para lidar com o código que agora gera erros seria um esforço que valeria a pena.

Eu não propus nada porque não consegui imaginar uma sintaxe viável, mas se quisermos ser honestos conosco, tudo neste tópico e relacionado ao tratamento de erros do Go em geral foi sobre o fato de que o tratamento de erros e lógica do programa são companheiros estranhos, então, idealmente, os erros seriam melhor tratados fora da banda de alguma forma?

try como palavra-chave certamente ajuda na legibilidade (vs. uma chamada de função) e parece menos complexo. @brynbellomy @crawshaw obrigado por dedicar um tempo para escrever os exemplos.

Suponho que meu pensamento geral é que try faz demais. Ele resolve: chamar função, atribuir variáveis, verificar erro e retornar erro se existir. Proponho que, em vez disso, cortemos o escopo e resolvamos apenas o retorno condicional: "retorne se o último argumento não for nil".

Esta provavelmente não é uma ideia nova... Mas depois de examinar as propostas no wiki de feedback de erros , não a encontrei (não significa que não esteja lá)

Mini proposta de devolução condicional

excerto:

err, thing := newThing(name)
refuse nil, err

Eu adicionei ao wiki também em "idéias alternativas"

Não fazer nada também parece uma opção muito razoável.

@alexhornbake que me dá uma ideia um pouco diferente que seria mais útil

assert(nil, err)
assert(len(a), len(b))
assert(true, condition)
assert(expected, given)

dessa forma, não se aplicaria apenas à verificação de erros, mas a muitos tipos de erros lógicos.

O dado seria envolvido em um erro e retornado.

@alexhornbake

Assim como try não está realmente tentando, refuse não está realmente "recusando". A intenção comum aqui é que estamos definindo um "relé de proteção" ( relay é curto, preciso e aliterativo para return ) que "dispara" quando um dos valores com fio atende a uma condição (ou seja, é um erro não nulo). É uma espécie de disjuntor e, acredito, pode agregar valor se seu design for limitado a casos desinteressantes para simplesmente reduzir alguns dos clichês mais baixos. Qualquer coisa remotamente complexa deve depender do código Go simples.

Eu também recomendo Cranshaw por seu trabalho olhando através da biblioteca padrão, mas cheguei a uma conclusão muito diferente... Acho que isso torna quase todos esses trechos de código mais difíceis de ler e mais propensos a mal-entendidos.

        req.Header = Header(try(tp.ReadMIMEHeader())

Muitas vezes sentirei falta de que isso pode dar erro. Uma leitura rápida me dá "ok, defina o cabeçalho para Header of ReadMimeHeader of the thing".

        if driverCtx, ok := driveri.(driver.DriverContext); ok {
                return OpenDB(try(driverCtx.OpenConnector(dataSourceName))), nil
        }

Este, meus olhos apenas cruzam tentando analisar essa linha do OpenDB. Há tanta densidade lá... Isso mostra o maior problema que todas as chamadas de função aninhadas têm, em que você tem que ler de dentro para fora, e você tem que analisá-lo em sua cabeça para descobrir onde está a parte mais interna .

Observe também que isso pode retornar de dois lugares diferentes na mesma linha... você estará depurando, e dirá que houve um erro retornado dessa linha, e a primeira coisa que todos farão é tentar descubra por que o OpenDB está falhando com esse erro estranho, quando na verdade é o OpenConnector falhando (ou vice-versa).

        ds := &driverStmt{
                Locker: dc,
                si: try(ctxDriverPrepare(ctx, dc.ci, query)),
        }   

Este é um lugar onde o código pode falhar onde anteriormente seria impossível. Sem try , a construção literal de estrutura não pode falhar . Meus olhos passarão por ele como "ok, construindo um driverStmt ... seguindo em frente ..." e será tão fácil perder que, na verdade, isso pode causar um erro na sua função. A única maneira que teria sido possível antes é se ctxDriverPrepare entrasse em pânico... e todos nós sabemos que é um caso que 1.) basicamente nunca deveria acontecer e 2.) se acontecer, significa que algo está drasticamente errado.

Tentar uma palavra-chave e uma declaração corrige muitos dos meus problemas com ela. Eu sei que não é compatível com versões anteriores, mas não acho que usar uma versão pior seja a solução para o problema de compatibilidade com versões anteriores.

@daved Não tenho certeza se sigo. Você não gosta do nome, ou não gosta da ideia?

De qualquer forma, postei isso aqui como uma alternativa... Se houver interesse legítimo, posso abrir um novo tópico para discussão, não quero poluir este tópico (talvez tarde demais?) Polegares para cima/baixo na ideia original darão nos uma sensação... Claro aberto a nomes alternativos. A parte importante é "retorno condicional sem tentar manipular a atribuição".

Embora eu goste da proposta de captura de @jimmyfrasche , gostaria de propor uma alternativa:
go handler fmt.HandleErrorf("copy %s %s", src, dst)
seria equivalente a:
go defer func(){ if(err != nil){ fmt.HandleErrorf(&err,"copy %s %s", src, dst) } }()
onde err é o último valor de retorno nomeado, com erro de tipo. No entanto, os manipuladores também podem ser usados ​​quando os valores de retorno não são nomeados. O caso mais geral também seria permitido:
go handler func(err *error){ *err = fmt.Errorf("foo: %w", *err) }() `
O principal problema que tenho com o uso de valores de retorno nomeados (que catch não resolve) é que err é supérfluo. Ao adiar uma chamada para um manipulador como fmt.HandleErrorf , não há um primeiro argumento razoável, exceto um ponteiro para o valor de retorno do erro, por que dar ao usuário a opção de cometer um erro?

Comparado com catch, a principal diferença é que handler torna um pouco mais fácil chamar handlers predefinidos ao custo de tornar mais detalhado para defini-los no local. Não tenho certeza se isso é o ideal, mas acho que está mais de acordo com a proposta original.

@yiyus catch , como eu defini, não requer que err seja nomeado na função que contém o catch .

Em catch err { , o err é o nome do erro dentro do bloco catch . É como um nome de parâmetro de função.

Com isso, não há necessidade de algo como fmt.HandleErrorf porque você pode simplesmente usar o fmt.Errorf normal:

func f() error {
  catch err {
    return fmt.Errorf("foo: %w", err)
  }
  return errors.New("bar")
}

que retorna um erro que é impresso como foo: bar .

Eu não gosto dessa abordagem, por causa de:

  • A chamada de função try() interrompe a execução do código na função pai.
  • não há nenhuma palavra-chave return , mas o código realmente retorna.

Muitas maneiras de fazer manipuladores estão sendo propostas, mas acho que muitas vezes faltam dois requisitos principais:

  1. Tem que ser significativamente diferente e melhor do que if x, err := thingie(); err != nil { handle(err) } . Acho que sugestões ao longo das linhas de try x := thingie else err { handle(err) } não atendem a essa barra. Por que não dizer if ?

  2. Deve ser ortogonal à funcionalidade existente de defer . Ou seja, deve ser diferente o suficiente para que fique claro que o mecanismo de manipulação proposto é necessário por si só, sem criar casos de canto estranhos quando manipular e diferir interagem.

Por favor, mantenha esses desideratos em mente enquanto discutimos mecanismos alternativos para try /handle.

@carlmjohnson Eu gosto da ideia de catch de @jimmyfrasche em relação ao seu ponto 2 - é apenas açúcar de sintaxe para um defer que economiza 2 linhas e permite evitar ter que nomear o valor de retorno do erro (que em turn também exigiria que você nomeasse todos os outros, se ainda não o tivesse feito). Não levanta um problema de ortogonalidade com defer , porque é defer .

ecoando o que @ubombi disse:

A chamada da função try() interrompe a execução do código na função pai.; não há palavra-chave de retorno, mas o código realmente retorna.

Em Ruby, procs e lambdas são um exemplo do que try faz... Um proc é um bloco de código que sua instrução return retorna não do próprio bloco, mas do chamador.

Isso é exatamente o que try faz... é apenas um proc Ruby pré-definido.

Acho que se fôssemos por esse caminho, talvez pudéssemos deixar o usuário definir sua própria função try introduzindo proc functions

Ainda prefiro if err != nil , porque é mais legível, mas acho que try seria mais benéfico se o usuário definisse seu próprio proc:

proc try(err *error, msg string) {
  if *err != nil {
    *err = fmt.Errorf("%v: %w", msg, *err)
    return
  }
}

E então você pode chamá-lo:

func someFunc() (string, error) {
  err := doSomething()
  try(&err, "someFunc failed")
}

O benefício aqui é que você define o tratamento de erros em seus próprios termos. E você também pode fazer um proc exposto, privado ou interno.

Também é melhor do que a cláusula handle {} na proposta Go2 original porque você pode definir isso apenas uma vez para toda a base de código e não em cada função.

Uma consideração para legibilidade é que um func() e um proc() podem ser chamados de forma diferente, como func() e proc!() para que um programador saiba que uma chamada de proc pode realmente retornar do função de chamada.

@marwan-at-work, try(err, "someFunc failed") não deveria ser try(&err, "someFunc failed") no seu exemplo?

@dpinela obrigado pela correção, atualizei o código :)

A prática comum que estamos tentando substituir aqui é o que o desenrolamento de pilha padrão em muitos idiomas sugere em uma exceção (e, portanto, a palavra "tentar" foi selecionada ...).
Mas se pudéssemos apenas permitir uma função (...try() ou outra) que retrocedesse dois níveis no rastreamento, então

try := handler(err error) {     //which corelates with - try := func(err error) 
   if err !=nil{
       //do what ever you want to do when there's an error... log/print etc
       return2   //2 levels
   }
} 

e, em seguida, um código como
f := try(os.Open(nome do arquivo))
poderia fazer exatamente como a proposta aconselha, mas como é uma função (ou na verdade uma "função de manipulador") o desenvolvedor terá muito mais controle sobre o que a função faz, como formata o erro em diferentes casos, use um manipulador semelhante em todos os lugares o código para manipular (digamos) os.Open, em vez de escrever fmt.Errorf("erro ao abrir arquivo %s ....") todas as vezes.
Isso também forçaria o tratamento de erros como se "try" não fosse definido - é um erro de tempo de compilação.

@guybrand Ter um retorno de dois níveis return2 (ou "retorno não local", como o conceito geral é chamado em Smalltalk) seria um bom mecanismo de propósito geral (também sugerido por @mikeschinkel em #32473) . Mas parece que try ainda é necessário em sua sugestão, então não vejo uma razão para o return2 - o try pode apenas fazer o return . Seria mais interessante se também se pudesse escrever try localmente, mas isso não é possível para assinaturas arbitrárias.

@griesemer

_"então eu não vejo uma razão para o return2 - o try pode apenas fazer o return ."_

Uma razão - como apontei em #32473 _(obrigado pela referência)_ - seria permitir vários níveis de break e continue , além de return .

Obrigado novamente a todos por todos os novos comentários; é um investimento de tempo significativo para acompanhar a discussão e escrever um feedback extenso. E melhor ainda, apesar dos argumentos às vezes apaixonados, este tem sido um tópico bastante civilizado até agora. Obrigado!

Aqui está outro resumo rápido, desta vez um pouco mais condensado; desculpas para aqueles que eu não mencionei, esqueci ou representei errado. Neste ponto, acho que alguns temas maiores estão surgindo:

1) Em geral, usar um built-in para a funcionalidade try é considerado uma má escolha: Dado que afeta o fluxo de controle, deve ser _pelo menos_ uma palavra-chave ( @carloslenz "prefere fazer uma declaração sem parêntese"); try como expressão não parece uma boa ideia, prejudica a legibilidade ( @ChrisHines , @jimmyfrasche), são "retornos sem return ". @brynbellomy fez uma análise real de try usados ​​como identificadores; parece haver muito poucos em termos de porcentagem, então pode ser possível seguir a rota da palavra-chave sem afetar muito código.

2) @crawshaw levou algum tempo para analisar algumas centenas de casos de uso da biblioteca std e chegou à conclusão de que try , conforme proposto, quase sempre melhorava a legibilidade. @jimmyfrasche chegou à conclusão oposta .

3) Outro tema é que usar defer para decoração de erros não é o ideal. @josharian aponta que os defer 's sempre são executados no retorno da função, mas se eles estão aqui para decoração de erro, nós só nos importamos com seu corpo se houver um erro, o que pode ser uma fonte de confusão.

4) Muitos escreveram sugestões para melhorar a proposta. @zeebo , @patrick-nyt apoiam gofmt formatando simples if em uma única linha (e fique feliz com o status quo). @jargv sugeriu que try() (sem argumentos) poderia retornar um ponteiro para o erro "pendente" atualmente, o que removeria a necessidade de nomear o resultado do erro apenas para que se tenha acesso a ele em um defer ; @masterada sugeriu usar errorfunc() . @velovix reviveu a ideia de um try de 2 argumentos onde o segundo argumento seria um manipulador de erros.

@klaidliadon , @networkimprov são a favor de "operadores de atribuição" especiais, como f, # := os.Open() em vez de try . A @networkimprov apresentou uma proposta alternativa mais abrangente investigando essas abordagens (consulte a edição nº 32500). @mikeschinkel também apresentou uma proposta alternativa sugerindo a introdução de dois novos recursos de linguagem de uso geral que também poderiam ser usados ​​para tratamento de erros, em vez de um try específico de erro (consulte a edição #32473). @josharian reviveu uma possibilidade que discutimos na GopherCon no ano passado, onde try não retorna após um erro, mas pula (com goto ) para um rótulo chamado error (alternativamente , try pode ter o nome de um rótulo de destino).

5) Sobre o tema try como palavra-chave, surgiram duas linhas de pensamento. @brynbellomy sugeriu uma versão que pode, alternativamente, especificar um manipulador:

a, b := try f()
a, b := try f() else err { /* handle error */ }

@thepudds vai um passo além e sugere try no início da linha, dando try a mesma visibilidade que um return :

try a, b := f()

Ambos podem funcionar com defer .

@griesemer

Obrigado pela referência a @mikeschinkel #32473, tem muito em comum.

em relação a

Mas parece que ainda é necessário tentar na sua sugestão
Embora minha sugestão possa ser implementada com "qualquer" manipulador e não com um "build in/keyword/expression" reservado, não acho que "try()" seja uma má ideia (e, portanto, não votei negativamente), estou tentando para "ampliá-lo" - para mostrar mais vantagens, muitos esperavam "uma vez que o go 2.0 fosse introduzido"

Eu acho que isso também pode ser a fonte das "vibrações mistas" que você relatou em seu último resumo - não é "try() não melhora o tratamento de erros" - com certeza sim, está "esperando que o Go 3.0 resolva algum outro erro importante" lidar com dores" as pessoas afirmaram acima, parece muito longo :)

Estou conduzindo uma pesquisa sobre "dores de manipulação de erros" (e alguns dos problemas são apenas "eu não uso boas práticas", enquanto alguns eu nem imaginava que as pessoas (principalmente vindas de outros idiomas) quisessem fazer - de legal para WTF).

Espero poder compartilhar alguns resultados interessantes em breve.

por último -Obrigado pelo incrível trabalho e paciência!

Olhando simplesmente para o comprimento da sintaxe proposta atual versus o que está disponível agora, o caso em que o erro só precisa ser retornado sem manipulá-lo ou decorado é o caso em que a maior conveniência é obtida. Um exemplo com minha sintaxe favorita até agora:

try a, b := f() else err { return fmt.Errorf("Decorated: %s", err); }
if a,b, err :=f;  err != nil { return fmt.Errorf("Decorated: %s", err); }
try a, b := f()
if a,b, err :=f;  err != nil { return err; }

Então, diferente do que eu pensava antes, talvez seja simplesmente suficiente alterar go fmt, pelo menos para o caso de erro decorado/tratado. Enquanto para apenas passar o caso de erro, algo como try ainda pode ser desejável como açúcar sintático para este caso de uso muito comum.

Em relação try else , acho que funções de erro condicional como fmt.HandleErrorf (editar: estou assumindo que retorna nil quando a entrada é nil) no comentário inicial funciona bem, então adicionar else é desnecessário.

a, b, err := doSomething()
try fmt.HandleErrorf(&err, "Decorated "...)

Como muitos outros aqui, prefiro que try seja uma declaração em vez de uma expressão, principalmente porque uma expressão que altera o fluxo de controle é completamente estranha ao Go. Também porque isso não é uma expressão, deve estar no início da linha.

Também concordo com @daved que o nome não é apropriado. Afinal, o que estamos tentando alcançar aqui é uma atribuição protegida, então por que não usar guard como em Swift e tornar a cláusula else opcional? Algo como

GuardStmt = "guard" ( Assignment | ShortVarDecl | Expression ) [  "else" Identifier Block ] .

em que Identifier é o nome da variável de erro vinculada no seguinte Block . Sem a cláusula else , apenas retorne da função atual (e use um manipulador de defer para decorar erros, se necessário).

Inicialmente, não gostei de uma cláusula else porque é apenas um açúcar sintático em torno da atribuição usual seguida por if err != nil , mas depois de ver alguns dos exemplos, faz sentido: usar guard torna a intenção mais clara.

EDIT: alguns sugeriram usar coisas como catch para especificar de alguma forma diferentes manipuladores de erros. Acho else igualmente viável semanticamente falando e já está no idioma.

Embora eu goste da instrução try-else, que tal essa sintaxe?

a, b, (err) := func() else { return err }

A expressão try - else é um operador ternário.

a, b := try f() else err { ... }
fmt.Println(try g() else err { ... })`

Declaração try - else é uma declaração if .

try a, b := f() else err { ... }
// (modulo scope of err) same as
a, b, err := f()
if err != nil { ... }

try com um manipulador opcional pode ser obtido com uma função auxiliar (abaixo) ou não usando try (não ilustrado, todos nós sabemos como é).

a, b := try(f(), decorate)
// same as
a, b := try(g())
// where g is
func g() (whatever, error) {
  x, err := f()
  if err != nil {
    try(decorate(err))
  }
  return x, nil
}

Todos os três reduzem o clichê e ajudam a conter o escopo dos erros.

Ele oferece a maior economia para try embutido, mas isso tem os problemas mencionados no documento de design.

Para o extrato try - else , ele oferece uma vantagem sobre o uso if em vez de try . Mas a vantagem é tão marginal que tenho dificuldade em vê-la se justificar, embora goste dela.

Todos os três assumem que é comum precisar de tratamento de erros especial para erros individuais.

O tratamento de todos os erros igualmente pode ser feito em defer . Se o mesmo tratamento de erros está sendo feito em cada bloco else , isso é um pouco repetitivo:

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

Eu certamente sei que há momentos em que um determinado erro requer tratamento especial. Essas são as instâncias que ficam na minha memória. Mas, se isso acontecer, digamos, 1 em cada 100 vezes, não seria melhor manter try simples e não usar try nessas situações? Por outro lado, se for mais como 1 em cada 10 vezes, adicionar else /handler parece mais razoável.

Seria interessante ver uma distribuição real de quantas vezes try sem um else /handler vs try com um else /handler seria útil, embora não são dados fáceis de coletar.

Quero expandir o comentário recente de @jimmyfrasche .

O objetivo desta proposta é reduzir o clichê

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

Este código é fácil de ler. Só vale a pena estender a linguagem se conseguirmos uma redução considerável no clichê. Quando eu vejo algo como

    try a, b := f() else err { return nil, err }

Não posso deixar de sentir que não estamos economizando tanto. Estamos salvando três linhas, o que é bom, mas pelas minhas contas estamos cortando de 56 para 46 caracteres. Isso não é muito. Comparado a

    a, b := try(f())

que corta de 56 para 18 caracteres, uma redução muito mais significativa. E enquanto a declaração try torna a mudança potencial de fluxo de controle mais clara, no geral não acho a declaração mais legível. Embora no lado positivo a instrução try facilite a anotação do erro.

De qualquer forma, meu ponto é: se vamos mudar alguma coisa, isso deve reduzir significativamente o clichê ou deve ser significativamente mais legível. O último é bem difícil, então qualquer mudança precisa realmente funcionar no primeiro. Se conseguirmos apenas uma pequena redução no clichê, então, na minha opinião, não vale a pena fazer.

Como outros, gostaria de agradecer a @crawshaw pelos exemplos.

Ao ler esses exemplos, encorajo as pessoas a tentarem adotar uma mentalidade na qual você não se preocupe com o fluxo de controle devido à função try . Acredito, talvez incorretamente, que esse fluxo de controle se tornará rapidamente uma segunda natureza para as pessoas que conhecem o idioma. No caso normal, acredito que as pessoas simplesmente deixarão de se preocupar com o que acontece no caso de erro. Tente ler esses exemplos enquanto vitrifica try assim como você já vitrifica if err != nil { return err } .

Depois de ler tudo aqui, e após uma reflexão mais aprofundada, não tenho certeza se vejo tentar, mesmo como uma declaração, algo que valha a pena acrescentar.

  1. a razão para isso parece estar reduzindo o código de placa de caldeira de manipulação de erros. IMHO ele "organiza" o código, mas realmente não remove a complexidade; apenas o obscurece . Isso não parece uma razão forte o suficiente. O "ir" lindamente capturada iniciando um thread simultâneo. Não tenho esse tipo de sensação de "aha!" aqui. Não parece certo. A relação custo / benefício não é grande o suficiente.

  2. seu nome não reflete sua função. Em sua forma mais simples, o que ele faz é isso: "se uma função retornar um erro, retorne do chamador com um erro", mas isso é muito longo :-) No mínimo, um nome diferente é necessário.

  3. com o retorno implícito do try no erro, parece que o Go está meio que relutantemente voltando para o tratamento de exceções. Isto é, se A chama em um try guard e B chama C em um try guard, e C chama D em um try guard, se D retornar um erro com efeito, você causou um goto não local. Parece muito "mágico".

  4. e ainda acredito que uma maneira melhor pode ser possível. Escolher tentar agora fechará essa opção.

@ianlancetaylor
Se eu entendi a proposta "try else" corretamente, parece que o bloco else é opcional e reservado para o manuseio fornecido pelo usuário. No seu exemplo try a, b := f() else err { return nil, err } a cláusula else é realmente redundante, e a expressão inteira pode ser escrita simplesmente como try a, b := f()

Concordo com @ianlancetaylor ,
Legibilidade e clichê são duas preocupações principais e talvez o impulso para
o tratamento de erros do go 2.0 (embora eu possa adicionar algumas outras preocupações importantes)

Também, que a corrente

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

É altamente legível.
E já que eu acredito

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

É quase tão legível, mas teve seus "problemas" de escopo, talvez um

ifErr a, b, err := f() {
    return nil, err
}

Isso seria apenas o ; err != nil part, e não criaria um escopo, ou

similarmente

tente a, b, err := f() {
retornar zero, err
}

Mantém as duas linhas extras, mas ainda é legível.

Em terça-feira, 11 de junho de 2019, 20:19 Dmitriy Matrenichev, [email protected]
escrevi:

@ianlancetaylor https://github.com/ianlancetaylor
Se eu entendi a proposta "tente mais" corretamente, parece que o bloco else
é opcional e reservado para manuseio fornecido pelo usuário. No seu exemplo
tente a, b := f() else err { return nil, err } a cláusula else é na verdade
redundante, e toda a expressão pode ser escrita simplesmente como try a, b :=
f()


Você está recebendo isso porque foi mencionado.
Responda a este e-mail diretamente, visualize-o no GitHub
https://github.com/golang/go/issues/32437?email_source=notifications&email_token=ABNEY4XPURMASWKZKOBPBVDPZ7NALA5CNFSM4HTGCZ72YY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGODXN3VDA#issuecomment-500933LNMVXHJKTDN5WW2ZLOORPWSZGODXN3VDA#issuecomment-50093
ou silenciar o thread
https://github.com/notifications/unsubscribe-auth/ABNEY4SAFK4M5NLABF3NZO3PZ7NALANCNFSM4HTGCZ7Q
.

@ianlancetaylor

De qualquer forma, meu ponto é: se vamos mudar alguma coisa, isso deve reduzir significativamente o clichê ou deve ser significativamente mais legível. O último é bem difícil, então qualquer mudança precisa realmente funcionar no primeiro. Se conseguirmos apenas uma pequena redução no clichê, então, na minha opinião, não vale a pena fazer.

Concordo, e considerando que um else seria apenas açúcar sintático (com uma sintaxe estranha!), muito provavelmente usado apenas raramente, não me importo muito com isso. Eu ainda prefiro try para ser uma declaração.

@ianlancetaylor Ecoando @DmitriyMV , o bloco else seria opcional. Deixe-me dar um exemplo que ilustra ambos (e não parece muito errado em termos da proporção relativa de blocos try manipulados versus não manipulados em código real):

func createMergeCommit(r *repo.Repo, index *git.Index, remote *git.Remote, remoteBranch *git.Branch) error {
    headRef, err := r.Head()
    if err != nil {
        return err
    }

    parentObjOne, err := headRef.Peel(git.ObjectCommit)
    if err != nil {
        return err
    }

    parentObjTwo, err := remoteBranch.Reference.Peel(git.ObjectCommit)
    if err != nil {
        return err
    }

    parentCommitOne, err := parentObjOne.AsCommit()
    if err != nil {
        return err
    }

    parentCommitTwo, err := parentObjTwo.AsCommit()
    if err != nil {
        return err
    }

    treeOid, err := index.WriteTree()
    if err != nil {
        return err
    }

    tree, err := r.LookupTree(treeOid)
    if err != nil {
        return err
    }

    remoteBranchName, err := remoteBranch.Name()
    if err != nil {
        return err
    }

    userName, userEmail, err := r.UserIdentityFromConfig()
    if err != nil {
        userName = ""
        userEmail = ""
    }

    var (
        now       = time.Now()
        author    = &git.Signature{Name: userName, Email: userEmail, When: now}
        committer = &git.Signature{Name: userName, Email: userEmail, When: now}
        message   = fmt.Sprintf(`Merge branch '%v' of %v`, remoteBranchName, remote.Url())
        parents   = []*git.Commit{
            parentCommitOne,
            parentCommitTwo,
        }
    )

    _, err = r.CreateCommit(headRef.Name(), author, committer, message, tree, parents...)
    if err != nil {
        return err
    }
    return nil
}
func createMergeCommit(r *repo.Repo, index *git.Index, remote *git.Remote, remoteBranch *git.Branch) error {
    try headRef := r.Head()
    try parentObjOne := headRef.Peel(git.ObjectCommit)
    try parentObjTwo := remoteBranch.Reference.Peel(git.ObjectCommit)
    try parentCommitOne := parentObjOne.AsCommit()
    try parentCommitTwo := parentObjTwo.AsCommit()
    try treeOid := index.WriteTree()
    try tree := r.LookupTree(treeOid)
    try remoteBranchName := remoteBranch.Name()
    try userName, userEmail := r.UserIdentityFromConfig() else err {
        userName = ""
        userEmail = ""
    }

    var (
        now       = time.Now()
        author    = &git.Signature{Name: userName, Email: userEmail, When: now}
        committer = &git.Signature{Name: userName, Email: userEmail, When: now}
        message   = fmt.Sprintf(`Merge branch '%v' of %v`, remoteBranchName, remote.Url())
        parents   = []*git.Commit{
            parentCommitOne,
            parentCommitTwo,
        }
    )

    try r.CreateCommit(headRef.Name(), author, committer, message, tree, parents...)
    return nil
}

Embora o padrão try / else não economize muitos caracteres sobre o composto if , ele salva:

  • unifique a sintaxe de tratamento de erros com o try não tratado
  • deixar claro de relance que um bloco condicional está lidando com uma condição de erro
  • nos dar uma chance de reduzir a estranheza de escopo que os compostos if sofrem de

No entanto, try não manipulados provavelmente serão os mais comuns.

@ianlancetaylor

Tente ler esses exemplos enquanto vitrifica tente assim como você já vitrifica if err != nil { return err }.

Eu não acho que isso seja possível/igualável. Perder que uma tentativa existe em uma linha lotada, ou o que ela envolve exatamente, ou que existem várias instâncias em uma linha... Isso não é o mesmo que marcar facilmente/rapidamente um ponto de retorno e não se preocupar com as especificidades nele.

@ianlancetaylor

Quando vejo um sinal de pare, reconheço-o pela forma e pela cor mais do que lendo a palavra impressa nele e ponderando suas implicações mais profundas.

Meus olhos podem vidrar sobre if err != nil { return err } mas ao mesmo tempo ainda registra - clara e instantaneamente.

O que eu gosto na variante try -statement é que ela reduz o clichê, mas de uma maneira que é fácil de esmaltar, mas difícil de perder.

Pode significar uma linha extra aqui ou ali, mas ainda é menos linhas do que o status quo.

@brynbellomy

  1. Como você oferece para lidar com funções que retornam vários valores, como:
    func createMergeCommit(r *repo.Repo, index *git.Index, remote *git.Remote, remoteBranch *git.Branch) (hash , error) {
  2. Como você manteria o rastreamento da linha correta que retornou o erro
  3. descartando o problema de escopo (que pode ser resolvido de outras maneiras), não tenho certeza
func createMergeCommit(r *repo.Repo, index *git.Index, remote *git.Remote, remoteBranch *git.Branch) error {

    if headRef, err := r.Head(); err != nil {
        return err
    } else if parentObjOne, err := headRef.Peel(git.ObjectCommit); err != nil {
        return err
    } else parentObjTwo, err := remoteBranch.Reference.Peel(git.ObjectCommit); err != nil {
        return err
    } ...

não é tão diferente em termos de legibilidade, ainda (ou fmt.Errorf("error with Getting head : %s", err.Error() ) permite que você modifique e forneça dados extras facilmente.

O que ainda é chato é o

  1. ter que verificar novamente; err!= nada
  2. retornando o erro como está se não quisermos fornecer as informações extras - o que em alguns casos não é uma boa prática, porque você depende da função que você chama para refletir um erro "bom" que indicará "o que deu errado ", em casos de file.Open , close , Remove , funções de banco de dados etc muitas das chamadas de função podem retornar o mesmo erro (podemos discutir se isso significa que o desenvolvedor que escreveu o erro fez um bom trabalho ou não ... acontece) - e então - você tem um erro, provavelmente registre-o da função que chamou
    " createMergeCommit", mas não consigo rastreá-lo até a linha exata em que ocorreu.

Desculpe se alguém já postou algo assim (há muitas boas ideias :P ) Que tal essa sintaxe alternativa:

fail := func(err error) error {
  log.Print("unexpected error", err)
  return err
}

a, b, err := f1()          // normal
c, d := f2() -> throw      // calls throw(err)
e, f := f3() -> panic      // calls panic(err)
g, h := f4() -> t.Error    // calls t.Error(err)
i, j := f5() -> fail       // calls fail(err)

ou seja, você tem um -> handler à direita de uma chamada de função que é chamada se o retorno err != nil. O manipulador é qualquer função que aceita um erro como um único argumento e, opcionalmente, retorna um erro (ou seja, func(error) ou func(error) error ). Se o manipulador retornar um erro nil, a função continuará, caso contrário, o erro será retornado.

então a := b() -> handler é equivalente a:

a, err := b()
if err != nil {
  if herr := handler(err); herr != nil {
    return herr
  }
}

Agora, como um atalho, você pode suportar um try embutido (ou palavra-chave ou operador ?= ou qualquer outra coisa) que é a abreviação de a := b() -> throw para que você possa escrever algo como:

func() error {
  a, b := try(f1())
  c, d := try(f2())
  e, f := try(f3())
  ...
  return nil
}() -> panic // or throw/fail/whatever

Pessoalmente, acho um operador ?= mais fácil de ler do que uma palavra-chave try/built-in:

func() error {
  a, b ?= f1()
  c, d ?= f2()
  e, f ?= f3()
  ...
  return nil
}() -> panic

note : aqui estou usando throw como um espaço reservado para um builtin que retornaria o erro para o chamador.

Eu não comentei sobre as propostas de tratamento de erros até agora porque geralmente sou a favor e gosto do jeito que elas estão indo. Tanto a função try definida na proposta quanto a declaração try proposta por @thepudds parecem ser adições razoáveis ​​à linguagem. Estou confiante de que o que quer que a equipe Go faça será bom.

Quero trazer à tona o que vejo como um problema menor com a maneira como o try é definido na proposta e como isso pode afetar futuras extensões.

Try é definido como uma função que recebe um número variável de argumentos.

func try(t1 T1, t2 T2, … tn Tn, te error) (T1, T2, … Tn)

Passar o resultado de uma chamada de função para try como em try(f()) funciona implicitamente devido à maneira como vários valores de retorno funcionam em Go.

Pela minha leitura da proposta, os trechos a seguir são válidos e semanticamente equivalentes.

a, b = try(f())
//
u, v, err := f()
a, b = try(u, v, err)

A proposta também levanta a possibilidade de estender try com argumentos extras.

Se determinarmos que ter alguma forma de função manipuladora de erros explicitamente fornecida, ou qualquer outro parâmetro adicional para esse assunto, é uma boa ideia, é trivialmente possível passar esse argumento adicional para uma chamada try.

Suponha que queremos adicionar um argumento de manipulador. Ele pode ir no início ou no final da lista de argumentos.

var h handler
a, b = try(h, f())
// or
a, b = try(f(), h)

Colocá-lo no início não funciona, porque (dada a semântica acima) try não seria capaz de distinguir entre um argumento de manipulador explícito e uma função que retorna um manipulador.

func f() (handler, error) { ... }
func g() (error) { ... }
try(f())
try(h, g())

Colocá-lo no final provavelmente funcionaria, mas try seria único na linguagem como sendo a única função com um parâmetro varargs no início da lista de argumentos.

Nenhum desses problemas é um impedimento, mas eles fazem try parecer inconsistente com o resto da linguagem, então não tenho certeza se try seria fácil de estender no futuro como o estados da proposta.

@mágico

Ter um manipulador é poderoso, talvez:
Eu você já declarou h,

você pode

var h handler
a, b, h = f()

ou

a, b, h.err = f()

se for um tipo de função:

h:= handler(err error){
 log(...)
 return ....
} 

Em seguida, houve a sugestão de

a, b, h(err) = f()

Todos podem invocar o manipulador
E você também pode "selecionar" o manipulador que retorna ou apenas captura o erro (conitnue/break/return) como alguns sugeriram.

E assim a questão varargs se foi.

Uma alternativa para a sugestão de else de @brynbellomy de:

a, b := try f() else err { /* handle error */ }

poderia ser para suportar uma função de decoração imediatamente após o else:

decorate := func(err error) error { return fmt.Errorf("foo failed: %v", err) }

try a, b := f() else decorate
try c, d := g() else decorate

E talvez também algumas funções utilitárias algo como:

decorate := fmt.DecorateErrorf("foo failed")

A função de decoração pode ter assinatura func(error) error , e ser chamada por try na presença de um erro, logo antes de try retornar da função associada que está sendo tentada.

Isso seria semelhante em espírito a uma das “iterações de design” anteriores do documento da proposta:

f := try(os.Open(filename), handler)              // handler will be called in error case

Se alguém quiser algo mais complexo ou um bloco de instruções, pode usar if (assim como hoje).

Dito isso, há algo de bom no alinhamento visual de try mostrado no exemplo de @brynbellomy em https://github.com/golang/go/issues/32437#issuecomment -500949780.

Tudo isso ainda pode funcionar com defer se essa abordagem for escolhida para decoração de erro uniforme (ou mesmo em teoria poderia haver uma forma alternativa de registrar uma função de decoração, mas isso é um ponto separado).

De qualquer forma, não tenho certeza do que é melhor aqui, mas queria deixar outra opção explícita.

Aqui está o exemplo de @brynbellomy reescrito com a função try , usando um bloco var para manter o bom alinhamento que @thepudds apontou em https://github.com/golang/go/issues /32437#issuecomment -500998690.

package main

import (
    "fmt"
    "time"
)

func createMergeCommit(r *repo.Repo, index *git.Index, remote *git.Remote, remoteBranch *git.Branch) error {
    var (
        headRef          = try(r.Head())
        parentObjOne     = try(headRef.Peel(git.ObjectCommit))
        parentObjTwo     = try(remoteBranch.Reference.Peel(git.ObjectCommit))
        parentCommitOne  = try(parentObjOne.AsCommit())
        parentCommitTwo  = try(parentObjTwo.AsCommit())
        treeOid          = try(index.WriteTree())
        tree             = try(r.LookupTree(treeOid))
        remoteBranchName = try(remoteBranch.Name())
    )

    userName, userEmail, err := r.UserIdentityFromConfig()
    if err != nil {
        userName = ""
        userEmail = ""
    }

    var (
        now       = time.Now()
        author    = &git.Signature{Name: userName, Email: userEmail, When: now}
        committer = &git.Signature{Name: userName, Email: userEmail, When: now}
        message   = fmt.Sprintf(`Merge branch '%v' of %v`, remoteBranchName, remote.Url())
        parents   = []*git.Commit{
            parentCommitOne,
            parentCommitTwo,
        }
    )

    _, err = r.CreateCommit(headRef.Name(), author, committer, message, tree, parents...)
    return err
}

É tão sucinto quanto a versão try -statement, e eu diria que é igualmente legível. Como try é uma expressão, algumas dessas variáveis ​​intermediárias podem ser eliminadas, ao custo de alguma legibilidade, mas isso parece mais uma questão de estilo do que qualquer outra coisa.

Isso levanta a questão de como try funciona em um bloco var . Suponho que cada linha do var conta como uma instrução separada, em vez de todo o bloco ser uma única instrução, até a ordem do que é atribuído quando.

Seria bom se a proposta "tentar" chamasse explicitamente as consequências para ferramentas como cmd/cover que aproximam estatísticas de cobertura de teste usando contagem de instruções ingênuas. Eu me preocupo que o fluxo de controle de erro invisível possa resultar em subcontagem.

@thepudds

tente a, b := f() senão decore

Talvez seja uma queimadura muito profunda nas células do meu cérebro, mas isso me atinge muito como um

try a, b := f() ;catch(decorate)

e um declive escorregadio para um

a, b := f()
catch(decorate)

Eu acho que você pode ver onde isso está levando, e para mim comparando

    try headRef := r.Head()
    try parentObjOne := headRef.Peel(git.ObjectCommit)
    try parentObjTwo := remoteBranch.Reference.Peel(git.ObjectCommit)
    try parentCommitOne := parentObjOne.AsCommit()
    try parentCommitTwo := parentObjTwo.AsCommit()
    try treeOid := index.WriteTree()
    try tree := r.LookupTree(treeOid)
    try remoteBranchName := remoteBranch.Name()

com

    try ( 
    headRef := r.Head()
    parentObjOne := headRef.Peel(git.ObjectCommit)
    parentObjTwo := remoteBranch.Reference.Peel(git.ObjectCommit)
    parentCommitOne := parentObjOne.AsCommit()
    parentCommitTwo := parentObjTwo.AsCommit()
    treeOid := index.WriteTree()
    tree := r.LookupTree(treeOid)
    remoteBranchName := remoteBranch.Name()
    )

(ou até uma pegadinha no final)
O segundo é mais legível, mas enfatiza o fato de que as funções abaixo retornam 2 vars, e magicamente descartamos um, coletando-o em um "err retornado por magia" .

    try(err) ( 
    headRef := r.Head()
    parentObjOne := headRef.Peel(git.ObjectCommit)
    parentObjTwo := remoteBranch.Reference.Peel(git.ObjectCommit)
    parentCommitOne := parentObjOne.AsCommit()
    parentCommitTwo := parentObjTwo.AsCommit()
    treeOid := index.WriteTree()
    tree := r.LookupTree(treeOid)
    remoteBranchName := remoteBranch.Name()
    ); err!=nil {
      //handle the err
    }

pelo menos define explicitamente a variável para retornar, e deixe-me lidar com isso dentro da função, sempre que eu quiser.

Apenas inserindo um comentário específico, pois não vi ninguém mencionar explicitamente, especificamente sobre a alteração de gofmt para suportar a seguinte formatação de linha única ou qualquer variante:

if f() { return nil, err }

Por favor não. Se quisermos uma única linha if , faça uma única linha if , por exemplo:

if f() then return nil, err

Mas, por favor, por favor, não adote a salada de sintaxe removendo as quebras de linha que facilitam a leitura do código que usa chaves.

Gosto de enfatizar algumas coisas que podem ter sido esquecidas no calor da discussão:

1) O objetivo desta proposta é fazer com que o tratamento de erros comuns fique em segundo plano - o tratamento de erros não deve dominar o código. Mas ainda deve ser explícito. Qualquer uma das sugestões alternativas que fazem com que o tratamento de erros se destaque ainda mais está perdendo o ponto. Como @ianlancetaylor já disse, se essas sugestões alternativas não reduzirem significativamente a quantidade de clichê, podemos ficar com as declarações if . (E o pedido para reduzir o clichê vem de você, a comunidade Go.)

2) Uma das reclamações sobre a proposta atual é a necessidade de nomear o resultado do erro para ter acesso a ele. Qualquer proposta alternativa terá o mesmo problema, a menos que a alternativa introduza sintaxe extra, ou seja, mais clichê (como ... else err { ... } e similares) para nomear explicitamente essa variável. Mas o que é interessante: Se não nos importamos em decorar um erro e não nomearmos os parâmetros de resultado, mas ainda exigirmos um return explícito porque há um manipulador explícito de tipos, essa instrução return terá que enumerar todos os valores de resultado (normalmente zero), já que um retorno nu não é permitido neste caso. Especialmente se uma função faz muitos retornos de erro sem decorar o erro, esses retornos explícitos ( return nil, err , etc.) são adicionados ao clichê. A proposta atual e qualquer alternativa que não exija um return explícito acaba com isso. Por outro lado, se se quer decorar o erro, a proposta atual _requer_ que se dê um nome ao resultado do erro (e com isso todos os outros resultados) para ter acesso ao valor do erro. Isso tem o bom efeito colateral de que em um manipulador explícito pode-se usar um retorno nu e não é necessário repetir todos os outros valores de resultado. (Eu sei que existem alguns fortes sentimentos sobre retornos nu, mas a realidade é que quando tudo o que nos importa é o resultado do erro, é um verdadeiro incômodo ter que enumerar todos os outros valores de resultado (normalmente zero) - isso não acrescenta nada ao compreensão do código). Em outras palavras, ter que nomear o resultado do erro para que ele possa ser decorado permite uma redução ainda maior do clichê.

@magical Obrigado por apontar isso . Percebi o mesmo logo após postar a proposta (mas não a trouxe para não causar mais confusão). Você está certo de que, como está, try não pode ser estendido. Felizmente, a correção é bastante fácil. (Acontece que nossas propostas internas anteriores não tiveram esse problema - ele foi introduzido quando reescrevi nossa versão final para publicação e tentei simplificar try para corresponder mais às regras de passagem de parâmetros existentes. Parecia uma boa - mas como se vê, falho e principalmente inútil - benefício para poder escrever try(a, b, c, handle) .)

Uma versão anterior de try a definia aproximadamente da seguinte forma: try(expr, handler) recebe uma (ou talvez duas) expressões como argumentos, onde a primeira expressão pode ter vários valores (só pode acontecer se a expressão for uma chamada de função). O último valor dessa expressão (possivelmente com vários valores) deve ser do tipo error e esse valor é testado em relação a nil. (etc. - o resto você pode imaginar).

De qualquer forma, o ponto é que try aceita sintaticamente apenas uma, ou talvez duas expressões. (Mas é um pouco mais difícil descrever a semântica de try .) A consequência seria esse código como:

a, b := try(u, v, err)

não seria mais permitido. Mas há pouca razão para fazer isso funcionar em primeiro lugar: Na maioria dos casos (a menos que a e b sejam resultados nomeados) este código - se importante por algum motivo - pode ser reescrito facilmente em

a, b := u, v  // we don't care if the assignment happens in case of an error
try(err)

(ou use uma instrução if conforme necessário). Mas, novamente, isso parece sem importância.

essa instrução de retorno terá que enumerar todos os valores de resultado (normalmente zero), pois um retorno nu não é permitido neste caso

Um retorno nu não é permitido, mas tentar seria. Uma coisa que eu gosto em tentar (seja como uma função ou uma instrução) é que não precisarei mais pensar em como definir valores sem erro ao retornar um erro, apenas usarei try.

@griesemer Obrigado pela explicação. Essa é a conclusão a que cheguei também.

Um breve comentário sobre try como uma declaração: como acho que pode ser visto no exemplo em https://github.com/golang/go/issues/32437#issuecomment -501035322, o try enterra o lede. O código se torna uma série de instruções try , que obscurece o que o código está realmente fazendo.

O código existente pode reutilizar uma variável de erro recém-declarada após o bloco if err != nil . Ocultar a variável quebraria isso, e adicionar uma variável de retorno nomeada à assinatura da função nem sempre a corrigirá.

Talvez seja melhor deixar a declaração/atribuição de erro como está e encontrar um stmt de tratamento de erros de uma linha.

err := f() // followed by one of

on err, return err            // any type can be tested for non-zero
on err, return fmt.Errorf(...)
on err, fmt.Println(err)      // doesn't stop the function
on err, hname err             // handler invocation without parens
on err, ignore err            // optional ignore handler logs error if defined

if err, return err            // alternatively, use if

handle hname(err error, clr caller) { // type caller has results of runtime.Caller()
   if err == io.Bad { return err }
   fmt.Println(clr.name, err)
}

Uma subexpressão try pode entrar em pânico, significando que o erro nunca é esperado. Uma variante disso poderia ignorar qualquer erro.

f(try g()) // panic on error
f(try_ g()) // ignore any error

O objetivo desta proposta é fazer com que o tratamento de erros comuns fique em segundo plano - o tratamento de erros não deve dominar o código. Mas ainda deve ser explícito.

Eu gosto da ideia dos comentários listando try como uma declaração. É explícito, ainda fácil de encobrir (já que é de comprimento fixo), mas não tão fácil de encobrir (já que está sempre no mesmo lugar) que eles podem ser escondidos em uma linha lotada. Ele também pode ser combinado com o defer fmt.HandleErrorf(...) como observado anteriormente, no entanto, ele tem a armadilha de abusar de parâmetros nomeados para encapsular erros (o que ainda parece um hack inteligente para mim. hacks inteligentes são ruins.)

Uma das razões pelas quais eu não gostei de try como uma expressão é que é muito fácil de encobrir, ou não é fácil o suficiente para encobrir. Veja os dois exemplos a seguir:

Tente como uma expressão

// Too hidden, it's in a crowded function with many symbols that complicate the function.

f, err := os.OpenFile(try(FileName()), os.O_APPEND|os.O_WRONLY, 0600)

// Not easy enough, the word "try" already increases horizontal complexity, and it
// being an expression only encourages more horizontal complexity.
// If this code weren't collapsed to multiple lines, it would be extremely
// hard to read and unclear as to what's executing when.

fullContents := try(io.CopyN(
    os.Stdout,
    try(net.Dial("tcp", "localhost:"+try(buf.ReadString("\n"))),
    try(strconv.Atoi(try(buf.ReadString("\n")))),
))

Tente como uma declaração

// easy to see while still not being too verbose

try name := FileName()
os.OpenFile(name, os.O_APPEND|os.O_WRONLY, 0600)

// does not allow for clever one-liners, code remains much more readable.
// also, the code reads in sequence from top-to-bottom.

try port := r.ReadString("\n")
try lengthStr := r.ReadString("\n")
try length := strconv.Atoi(lengthStr)

try con := net.Dial("tcp", "localhost:"+port)
try io.CopyN(os.Stdout, con, length)

Aprendizado

Este código é definitivamente artificial, eu admito. Mas o que estou chegando é que, em geral, try como uma expressão não funciona bem em:

  1. No meio de expressões lotadas que não precisam de muita verificação de erros
  2. Instruções de várias linhas relativamente simples que exigem muita verificação de erros

No entanto, concordo com @ianlancetaylor que iniciar cada linha com try parece atrapalhar a parte importante de cada instrução (a variável que está sendo definida ou a função que está sendo executada). No entanto, acho que, por estar no mesmo local e ter uma largura fixa, é muito mais fácil encobrir, enquanto ainda notamos. No entanto, os olhos de todos são diferentes.

Eu também acho que encorajar one-liners inteligentes no código é apenas uma má ideia em geral. Estou surpreso que eu possa criar uma linha tão poderosa como no meu primeiro exemplo, é um trecho que merece sua própria função porque está fazendo muito - mas se encaixa em uma linha se eu não o tivesse recolhido para múltiplo por questões de legibilidade. Tudo em uma linha:

fullContents := try(io.CopyN(os.Stdout, try(net.Dial("tcp", "localhost:"+try(r.ReadString("\n"))), try(strconv.Atoi(try(r.ReadString("\n"))))))

Ele lê uma porta de um *bufio.Reader , inicia uma conexão TCP e copia um número de bytes especificados pelo mesmo *bufio.Reader para stdout . Todos com tratamento de erros. Para uma linguagem com convenções de codificação tão rígidas, acho que isso nem deveria ser permitido. Acho que gofmt poderia ajudar com isso, no entanto.

Para uma linguagem com convenções de codificação tão rígidas, acho que isso nem deveria ser permitido.

É possível escrever código abominável em Go. É até possível formatá-lo terrivelmente; existem apenas normas e ferramentas fortes contra isso. Go ainda tem goto .

Durante as revisões de código, às vezes peço às pessoas que dividam expressões complicadas em várias instruções, com nomes intermediários úteis. Eu faria algo semelhante para try s profundamente aninhados, pelo mesmo motivo.

O que é tudo para dizer: não vamos nos esforçar muito para proibir códigos ruins, ao custo de distorcer a linguagem. Temos outros mecanismos para manter o código limpo que são mais adequados para algo que envolve fundamentalmente o julgamento humano caso a caso.

É possível escrever código abominável em Go. É até possível formatá-lo terrivelmente; existem apenas normas e ferramentas fortes contra isso. Vai mesmo tem que.

Durante as revisões de código, às vezes peço às pessoas que dividam expressões complicadas em várias instruções, com nomes intermediários úteis. Eu faria algo semelhante para tentativas profundamente aninhadas, pelo mesmo motivo.

O que é tudo para dizer: não vamos nos esforçar muito para proibir códigos ruins, ao custo de distorcer a linguagem. Temos outros mecanismos para manter o código limpo que são mais adequados para algo que envolve fundamentalmente o julgamento humano caso a caso.

Este é um bom ponto. Não devemos proibir uma boa ideia só porque ela pode ser usada para criar códigos ruins. No entanto, acho que se tivermos uma alternativa que promova um código melhor, pode ser uma boa ideia. Eu realmente não vi muita conversa _contra_ a ideia crua por trás de try como uma declaração (sem todo o lixo else { ... } ) até o comentário de @ianlancetaylor , no entanto, eu posso ter perdido isso.

Além disso, nem todo mundo tem revisores de código, algumas pessoas (especialmente em um futuro distante) terão que manter o código Go não revisado. Go como uma linguagem normalmente faz um trabalho muito bom para garantir que quase todo o código escrito seja bem-manutenível (pelo menos depois de go fmt ), o que não é um feito a ser esquecido.

Dito isto, estou sendo terrivelmente crítico dessa ideia quando ela realmente não é horrível.

Try as uma declaração reduz significativamente o clichê, e mais do que try como uma expressão, se permitirmos que ele funcione em um bloco de expressões como foi proposto antes, mesmo sem permitir um bloco else ou um manipulador de erros. Usando isso, o exemplo do deandeveloper se torna:

try (
    name := FileName()
    file := os.OpenFile(name, os.O_APPEND|os.O_WRONLY, 0600)
    port := r.ReadString("\n")
    lengthStr := r.ReadString("\n")
    length := strconv.Atoi(lengthStr)
    con := net.Dial("tcp", "localhost:"+port)
    io.CopyN(os.Stdout, con, length)
)

Se o objetivo é reduzir o clichê if err!= nil {return err} , então acho que a declaração try que permite pegar um bloco de código tem o maior potencial para fazer isso, sem se tornar obscura.

@beoran Nesse ponto, por que tentar? Apenas permita uma atribuição onde o último valor de erro está faltando e faça com que ele se comporte como se fosse uma instrução try (ou chamada de função). Não que eu esteja propondo, mas reduziria ainda mais o clichê.

Eu acho que o clichê seria reduzido de forma eficiente por esses blocos var, mas temo que isso possa levar uma enorme quantidade de código a ser indentada em um nível adicional, o que seria lamentável.

@deanveloper

fullContents := try(io.CopyN(os.Stdout, try(net.Dial("tcp", "localhost:"+try(r.ReadString("\n"))), try(strconv.Atoi(try(r.ReadString("\n"))))))

Devo admitir que não é legível para mim, provavelmente sentiria que devo:

fullContents := try(io.CopyN(os.Stdout, 
                               try(net.Dial("tcp", "localhost:"+try(r.ReadString("\n"))),
                                     try(strconv.Atoi(try(r.ReadString("\n"))))))

ou similar, para facilitar a leitura, e então voltamos com um "try" no início de cada linha, com recuo.

Bem, acho que ainda precisaríamos tentar a compatibilidade com versões anteriores, e também ser explícito sobre um retorno que pode acontecer no bloco. Mas note que estou apenas seguindo a lógica de reduzir a placa de caldeira e depois ver onde isso nos leva. Há sempre uma tensão entre reduzir o clichê e a clareza. Eu acho que a principal questão em questão nesta questão é que todos nós parecemos discordar sobre onde o equilíbrio deveria estar.

Quanto aos travessões, é para isso que serve o go fmt, então, pessoalmente, não acho que seja um grande problema.

Eu gostaria de entrar na briga para mencionar outras duas possibilidades, cada uma delas independente, então vou mantê-las em posts separados.

Eu pensei que a sugestão de que try() (sem argumentos) pudesse ser definido para retornar um ponteiro para a variável de retorno de erro era interessante, mas eu não estava interessado nesse tipo de trocadilho - cheira a sobrecarga de função , algo que Go evita.

No entanto, gostei da ideia geral de um identificador predefinido que se refere ao valor do erro local.

Então, que tal predefinir o próprio identificador err para ser um alias para a variável de retorno de erro? Então isso seria válido:

func foo() error {
    defer handleError(&err, etc)
    try(something())
}

Seria funcionalmente idêntico a:

func foo() (err error) {
    defer handleError(&err, etc)
    try(something())
}

O identificador err seria definido no escopo do universo, mesmo que ele atue como um alias de função local, portanto, qualquer definição de nível de pacote ou definição de função local de err a substituiria. Isso pode parecer perigoso, mas eu escaneei as linhas de 22 milhões de Go no corpus Go e é muito raro. Existem apenas 4 instâncias distintas err usadas como global (tudo como uma variável, não um tipo ou constante) - isso é algo que vet poderia alertar.

É possível que haja duas variáveis ​​de retorno de erro de função no escopo; nesse caso, acho melhor que o compilador reclame que há uma ambiguidade e exija que o usuário nomeie explicitamente a variável de retorno correta. Então isso seria inválido:

func foo() error {
    f := func() error {
        defer handleError(&err, etc)
        try(something())
        return nil
    }
    return f()
}

mas você sempre pode escrever isso:

func foo() error {
    f := func() (err error) {
        defer handleError(&err, etc)
        try(something())
        return nil
    }
    return f()
}

Sobre o assunto de try como um identificador predefinido em vez de um operador,
Eu me vi tendendo a uma preferência pelo último depois de errar repetidamente os colchetes ao escrever:

try(try(os.Create(filename)).Write(data))

Em "Por que não podemos usar ? como Rust", o FAQ diz:

Até agora, evitamos abreviações ou símbolos enigmáticos na linguagem, incluindo operadores incomuns como ?, que têm significados ambíguos ou não óbvios.

Não tenho certeza absoluta de que isso seja verdade. O operador .() é incomum até você conhecer o Go, assim como os operadores de canal. Se adicionássemos um operador ? , acredito que em breve ele se tornaria onipresente o suficiente para não ser uma barreira significativa.

O operador Rust ? é adicionado após o colchete de fechamento de uma chamada de função, e isso significa que é fácil perder quando a lista de argumentos é longa.

Que tal adicionar ?() como operador de chamada:

Então, em vez de:

x := try(foo(a, b))

você faria:

x := foo?(a, b)

A semântica de ?() seria muito semelhante à do proposto try embutido. Ele agiria como uma chamada de função, exceto que a função ou método que está sendo chamado deve retornar um erro como seu último argumento. Assim como try , se o erro for diferente de zero, a instrução ?() o retornará.

Parece que a discussão se concentrou o suficiente para que agora estejamos circulando em torno de uma série de trocas bem definidas e discutidas. Isso é animador, pelo menos para mim, já que o compromisso está muito no espírito dessa linguagem.

@ianlancetaylor Eu admito absolutamente que vamos acabar com dezenas de linhas prefixadas por try . No entanto, não vejo como isso é pior do que dezenas de linhas pós-fixadas por uma expressão condicional de duas a quatro linhas declarando explicitamente a mesma expressão return . Na verdade, try (com cláusulas else ) torna um pouco mais fácil identificar quando um manipulador de erros está fazendo algo especial/não padrão. Além disso, tangencialmente, re: expressões condicionais if , acho que elas enterram o lede mais do que a proposta try -as-a-statement: a chamada da função vive na mesma linha que a condicional , a própria condicional termina no final de uma linha já cheia, e as atribuições de variáveis ​​são delimitadas para o bloco (o que exige uma sintaxe diferente se você precisar dessas variáveis ​​após o bloco).

@josharian Eu tive esse pensamento um pouco recentemente. A Go busca o pragmatismo, não a perfeição, e seu desenvolvimento frequentemente parece ser mais orientado por dados do que por princípios. Você pode escrever um Go terrível, mas geralmente é mais difícil do que escrever um Go decente (o que é bom o suficiente para a maioria das pessoas). Também vale a pena ressaltar — temos muitas ferramentas para combater códigos ruins: não apenas gofmt e go vet , mas nossos colegas e a cultura que essa comunidade (com muito cuidado) criou para se guiar. Eu odiaria evitar melhorias que ajudem o caso geral simplesmente porque alguém em algum lugar pode se matar.

@beoran Isso é elegante e, quando você pensa sobre isso, é semanticamente diferente dos blocos try de outras linguagens, pois tem apenas um resultado possível: retornar da função com um erro não tratado. No entanto: 1) isso provavelmente é confuso para novos codificadores Go que trabalharam com essas outras linguagens (honestamente, não é minha maior preocupação; confio na inteligência dos programadores) e 2) isso levará a enormes quantidades de código sendo indentadas em muitos bases de código. No que diz respeito ao meu código, costumo evitar os blocos type / const / var existentes por esse motivo. Além disso, as únicas palavras-chave que atualmente permitem blocos como esse são definições, não instruções de controle.

@yiyus Discordo da remoção da palavra-chave, pois a clareza é (na minha opinião) uma das virtudes do Go. Mas eu concordaria que recuar grandes quantidades de código para aproveitar as expressões try é uma má ideia. Então, talvez nenhum bloco try ?

@rogpeppe Acho que esse tipo de operador sutil só é razoável para chamadas que nunca devem retornar erros e, portanto, entre em pânico se o fizerem. Ou chamadas onde você sempre ignora o erro. Mas ambos parecem ser raros. Se você estiver aberto a um novo operador, consulte #32500.

Sugeri que f(try g()) entrasse em pânico em https://github.com/golang/go/issues/32437#issuecomment -501074836, junto com um stmt de manipulação de 1 linha:
on err, return ...

Eu acho que o opcional else em try ... else { ... } empurrará o código muito para a direita, possivelmente obscurecendo-o. Espero que o bloco de erro deva ter pelo menos 25 caracteres na maioria das vezes. Além disso, até agora os blocos não são mantidos na mesma linha por go fmt e espero que esse comportamento seja mantido por try else . Portanto, devemos discutir e comparar amostras onde o bloco else está em uma linha separada. Mas mesmo assim não tenho certeza sobre a legibilidade de else { no final da linha.

@yiyus https://github.com/golang/go/issues/32437#issuecomment -501139662

@beoran Nesse ponto, por que tentar? Apenas permita uma atribuição onde o último valor de erro está faltando e faça com que ele se comporte como se fosse uma instrução try (ou chamada de função). Não que eu esteja propondo, mas reduziria ainda mais o clichê.

Isso não pode ser feito porque Go1 já permite chamar func foo() error como apenas foo() . Adicionar , error aos valores de retorno do chamador mudaria o comportamento do código existente dentro dessa função. Consulte https://github.com/golang/go/issues/32437#issuecomment -500289410

@rogpeppe Em seu comentário sobre como acertar os parênteses com try aninhados: Você tem alguma opinião sobre a precedência de try ? Veja também o documento de projeto detalhado sobre este assunto .

@griesemer Na verdade, não estou muito interessado em try como um operador de prefixo unário pelas razões apontadas lá. Ocorreu-me que uma abordagem alternativa seria permitir try como um pseudo-método em uma tupla de retorno de função:

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

Isso resolve o problema de precedência, eu acho, mas não é muito parecido com o Go.

@rogpeppe

Muito interessante! . Você pode realmente estar em algo aqui.

E que tal estendermos essa ideia assim?

for _,fp := range filepaths {
    f := os.Open(path).try(func(err error)bool{
        fmt.Printf( "Cannot open file %s\n", fp );
        continue;
    });
}

BTW, eu posso preferir um nome diferente vs try() , como talvez guard() mas eu não deveria trocar o nome antes da arquitetura ser discutida por outros.

vs :

for _,fp := range filepaths {
    if f,err := os.Open(path);err!=nil{
        fmt.Printf( "Cannot open file %s\n", fp )
    }
}

?

Eu gosto do try a,b := foo() em vez de if err!=nil {return err} porque substitui um clichê para um caso realmente simples. Mas para todo o resto que adiciona contexto, realmente precisamos de algo mais do que if err!=nil {...} (será muito difícil encontrar melhor) ?

Se uma linha extra é normalmente necessária para decoração/envelopamento, vamos apenas "alocar" uma linha para ela.

f, err := os.Open(path)  // normal Go \o/
on err, return fmt.Errorf("Cannot open %s, due to %v", path, err)

@networkimprov Acho que também poderia gostar disso. Empurrando um termo mais aliterativo e descritivo que eu já trouxe...

f, err := os.Open(path)
relay err { nil, fmt.Errorf("Cannot open %s, due to %v", path, err) }

// marginally shorter, doesn't trigger vertical formatting unless excessively wide
// enclosed expression restricted to a list of values that match the return args

ou

f, err := os.Open(path)
relay(err) nil, fmt.Errorf("Cannot open %s, due to %v", path, err)

// somewhere between statement and func, prob more pleasing to type w/out completion
// trailing expression restricted to a list of values that match the return args
// maybe excessive width triggers linting noise - with a reformatter available
// providing a reformatter would make swapping old (narrow enough) code easy

@daved que bom que gostou! on err, ... permitiria qualquer manipulador de stmt único:

err := f() // followed by one of

on err, return err            // any type can be tested for non-zero
on err, return fmt.Errorf(...)
on err, fmt.Println(err)      // doesn't stop the function
on err, continue              // retry in a loop
on err, hname err             // named handler invocation without parens
on err, ignore err            // logs error if handle ignore() defined

handle hname(err error, clr caller) { // type caller has results of runtime.Caller()
   if err == io.Bad { return err } // non-local return
   fmt.Println(clr, err)
}

EDIT: on empresta de Javascript. Eu não queria sobrecarregar if .
Uma vírgula não é essencial, mas eu não gosto de ponto e vírgula lá. Talvez cólon?

Eu não sigo relay ; significa retorno em erro?

Um relé de proteção é desarmado quando alguma condição é atendida. Neste caso, quando um valor de erro não é nulo, o relé altera o fluxo de controle para retornar utilizando os valores subsequentes.

*Eu não gostaria de sobrecarregar , para este caso, e não sou fã do termo on , mas gosto da premissa e da aparência geral da estrutura do código.

Para o ponto anterior de @josharian , sinto que grande parte da discussão sobre a correspondência de parênteses é principalmente hipotética e usando exemplos artificiais. Eu não sei você, mas eu não tenho dificuldade em escrever chamadas de função na minha programação do dia-a-dia. Se chego a um ponto em que uma expressão fica difícil de ler ou compreender, eu a divido em várias expressões usando variáveis ​​intermediárias. Não vejo por que try() com sintaxe de chamada de função seria diferente a esse respeito na prática.

@eandre Normalmente, as funções não possuem uma definição tão dinâmica. Muitas formas desta proposta diminuem a segurança em torno da comunicação do fluxo de controle, e isso é problemático.

@networkimprov @daved Não gosto dessas duas ideias, mas elas não parecem uma melhoria suficiente em relação a simplesmente permitir instruções if err != nil { ... } de uma única linha para garantir uma mudança de idioma. Além disso, ele faz alguma coisa para reduzir o clichê repetitivo no caso em que você está simplesmente retornando o erro? Ou é a ideia de que você sempre tem que escrever o return ?

@brynbellomy No meu exemplo, não há return . relay é um relé de proteção definido como "se este erro não for nulo, o seguinte será retornado".

Usando meu segundo exemplo anterior:

f, err := os.Open(path)
relay(err) nil, fmt.Errorf("Cannot open %s, due to %v", path, err)

Também pode ser algo como:

f, err := os.Open(path)
relay(err)

Com o erro que dispara o relé sendo retornado junto com valores zero para outros valores de retorno (ou quaisquer valores definidos para valores retornados nomeados). Outro formulário que pode ser útil:

wrap := func(err error, msg string) error {
    if err != nil {
        fmt.Errorf("%s: %s", msg, err)
    }
    return nil
}

// ...

f, err := os.Open(path)
relay(err, wrap(err, fmt.Sprintf("Cannot open %s", path)))

Onde o segundo relé arg não é chamado a menos que o relé seja desarmado pelo primeiro relé arg. O argumento opcional de erro do segundo relé seria o valor retornado.

_go fmt_ deve permitir if de linha única, mas não case, for, else, var () ? Gostaria de todos, por favor ;-)

A equipe Go rejeitou muitas solicitações de verificações de erros de linha única.

As instruções on err, return err podem ser repetitivas, mas são explícitas, concisas e claras.

@magical Seu feedback foi abordado na versão atualizada da proposta detalhada .

Uma coisa pequena, mas se try for uma palavra-chave, ela poderá ser reconhecida como uma instrução final, portanto, em vez de

func f() error {
  try(g())
  return nil
}

você pode apenas fazer

func f() error {
  try g()
}

( try -statement obtém isso de graça, try -operator precisaria de tratamento especial, percebo que o exemplo acima não é um ótimo exemplo: mas é mínimo)

@jimmyfrasche try pode ser reconhecido como uma instrução final, mesmo que não seja uma palavra-chave - já fazemos isso com panic , não há necessidade de tratamento especial extra além do que já fazemos. Mas, além desse ponto, try não é uma declaração final, e tentar torná-la artificialmente parece estranho.

Todos os pontos válidos. Eu acho que só poderia ser considerado de forma confiável como uma declaração final se for a última linha de uma função que retorna apenas um erro, como CopyFile na proposta detalhada, ou está sendo usado como try(err) em um if onde se sabe que err != nil . Não parece valer a pena.

Como esse tópico está ficando longo e difícil de seguir (e começa a se repetir até certo ponto), acho que todos concordaríamos que precisaríamos comprometer "algumas das vantagens que qualquer proposta oferece.

À medida que continuamos gostando ou não gostando das permutações de código propostas acima, não estamos nos ajudando a ter uma noção real de "este é um compromisso mais sensato do que outro/o que já foi oferecido"?

Acho que precisamos de alguns critérios objetivos para avaliar nossas variações "tentativas" e propostas alternativas.

  • Diminui o clichê?
  • Legibilidade
  • Complexidade adicionada ao idioma
  • Padronização de erros
  • Go-ish
    ...
    ...
  • esforço de implementação e riscos
    ...

É claro que também podemos definir algumas regras básicas para não ir (nenhuma compatibilidade com versões anteriores seria uma) e deixar uma área cinzenta para "parece atraente/intuição etc (os critérios "difíceis" acima também podem ser discutíveis .. .).

Se testarmos qualquer proposta nesta lista e classificarmos cada ponto (boilerplate 5 points , readability 4 points etc), então acho que podemos alinhar em:
Nossas opções são provavelmente A, B e C, além disso, alguém que deseje adicionar uma nova proposta, pode testar (até certo ponto) se sua proposta atende aos critérios.

Se isso faz sentido, dê o polegar para cima , podemos tentar revisar a proposta original
https://github.com/golang/proposal/blob/master/design/32437-try-builtin.md

E talvez algumas das outras propostas incorporem os comentários ou links, talvez aprendêssemos alguma coisa, ou até mesmo chegássemos a uma mistura que tivesse uma classificação mais alta.

Critérios += reutilização do código de tratamento de erros, no pacote e dentro da função

Obrigado a todos pelo feedback contínuo sobre esta proposta.

A discussão se desviou um pouco da questão central. Também se tornou dominado por cerca de uma dúzia de colaboradores (você sabe quem você é) discutindo o que equivale a propostas alternativas.

Então, deixe-me apenas lembrar que esta edição é sobre uma proposta _específica_. Isso _não_ é uma solicitação de novas idéias sintáticas para tratamento de erros (o que é uma boa coisa a se fazer, mas não é _este_ problema).

Vamos deixar a discussão mais focada novamente e de volta aos trilhos.

O feedback é mais produtivo se ajudar a identificar _fatos_ técnicos que perdemos, como "esta proposta não funciona bem neste caso" ou "terá essa implicação que não percebemos".

Por exemplo, @magical apontou que a proposta escrita não era tão extensível quanto alegada (o texto original tornaria impossível adicionar um segundo argumento futuro). Felizmente este foi um problema menor que foi facilmente resolvido com um pequeno ajuste na proposta. Sua contribuição diretamente ajudou a melhorar a proposta.

@crawshaw teve tempo para analisar algumas centenas de casos de uso da biblioteca std e mostrou que try raramente acaba dentro de outra expressão, refutando diretamente a preocupação de que try possa ficar oculto e invisível. Isso é um feedback baseado em fatos muito útil, neste caso, validando o design.

Em contraste, julgamentos estéticos pessoais não são muito úteis. Podemos registrar esse feedback, mas não podemos agir de acordo com ele (além de apresentar outra proposta).

Sobre a apresentação de propostas alternativas: A proposta atual é fruto de muito trabalho, começando com o projeto do ano passado. Nós repetimos esse design várias vezes e solicitamos feedback de muitas pessoas antes de nos sentirmos confortáveis ​​o suficiente para publicá-lo e recomendar avançar para a fase real do experimento, mas ainda não fizemos o experimento. Faz sentido voltar à prancheta se o experimento falhar ou se o feedback nos disser antecipadamente que ele claramente falhará. Se reprojetarmos rapidamente, com base nas primeiras impressões, estaremos apenas desperdiçando o tempo de todos e, pior, não aprenderemos nada no processo.

Dito isso, a preocupação mais significativa expressa por muitos com esta proposta é que ela não encoraja explicitamente a decoração de erros além do que já podemos fazer na linguagem. Obrigado, registramos esse feedback. Recebemos o mesmo feedback internamente, antes de publicar esta proposta. Mas nenhuma das alternativas que consideramos é melhor do que a que temos agora (e analisamos muitas em profundidade). Em vez disso, decidimos propor uma ideia mínima que aborda bem uma parte do tratamento de erros e que pode ser estendida, se necessário, exatamente para abordar essa preocupação (a proposta fala sobre isso em detalhes).

Obrigado.

(Observo que algumas pessoas que defendem propostas alternativas começaram seus próprios problemas separados. Isso é uma boa coisa a fazer e ajuda a manter os respectivos problemas focados. Obrigado.)

@griesemer
Concordo totalmente que devemos nos concentrar e foi exatamente isso que me levou a escrever:

Se isso faz sentido, dê o polegar para cima , podemos tentar revisar a proposta original
https://github.com/golang/proposal/blob/master/design/32437-try-builtin.md

Duas questões:

  1. Você concorda que se marcarmos as vantagens (redução de clichê, legibilidade, etc) versus desvantagens (sem decoração de erro explícita/menor rastreabilidade da fonte da linha de erro) podemos realmente afirmar: esta proposta visa resolver a,b, um pouco ajuda c, não visa resolver d,e
    E com isso perder toda a confusão de "mas não d" , "como pode e" e ir mais para questões técnicas como @magical apontou
    E também desencorajar comentários de "mas a solução XXX resolve d,e melhor.
  2. muitos posts inline são "sugestões para pequenas mudanças na proposta" - eu sei que é uma linha tênue, mas acho que faz sentido mantê-los.

LMKWYT.

Está usando try() com zero argumentos (ou um builtin diferente) ainda em consideração ou isso foi descartado.

Após as alterações na proposta, ainda estou preocupado em como ela torna o uso de valores de retorno nomeados mais "comuns". No entanto, não tenho dados para fazer backup disso :upside_down_ face:.
Se try() com zero argumentos (ou um builtin diferente) for adicionado à proposta, os exemplos na proposta podem ser atualizados para usar try() (ou um builtin diferente) para evitar retornos nomeados?

@guybrand Votar positivo e negativo é uma boa coisa para expressar _sentimento_ - mas é isso. Não há mais informações lá. Não vamos tomar uma decisão com base na contagem de votos, ou seja, apenas no sentimento. Claro, se todo mundo - digamos 90% + - odeia uma proposta, isso é provavelmente um mau sinal e devemos pensar duas vezes antes de seguir em frente. Mas não parece ser o caso aqui. Um bom número de pessoas parece estar feliz em experimentar coisas, e mudou para outras coisas (e não se incomode em comentar neste tópico).

Como tentei expressar acima , o sentimento nesta fase da proposta não se baseia em nenhuma experiência real com o recurso; é um sentimento. Os sentimentos tendem a mudar com o tempo, especialmente quando se teve a chance de realmente experimentar o assunto sobre os quais os sentimentos são... :-)

@Goodwine Ninguém descartou try() para chegar ao valor do erro; embora _if_ algo assim seja necessário, pode ser melhor ter uma variável err pré-declarada como @rogpeppe sugeriu (eu acho).

Novamente, esta proposta não descarta nada disso. Vamos lá se descobrirmos que é necessário.

@griesemer
Acho que você me entendeu totalmente errado.
Eu não estou pensando em votar a favor/não votar nesta ou em qualquer proposta, eu estava apenas procurando uma maneira de ter uma boa noção de "Achamos que faz sentido tomar uma decisão com base em critérios rígidos em vez de 'eu gosto de x ' ou 'y não parece legal'"

Pelo que você escreveu - é EXATAMENTE o que você pensa ... então, por favor, vote no meu comentário dizendo:

"Acho que devemos fazer uma lista do que esta proposta visa melhorar, e com base nisso podemos
A. decidir se isso é significativo o suficiente
B. decidir se parece que a proposta realmente resolve o que pretende resolver
C. (como você adicionou) faça um esforço extra tentando ver se é viável ...

@guybrand eles estão evidentemente convencidos de que vale a pena prototipar no pré-lançamento 1.14(?) e coletar feedback de usuários práticos. IOW uma decisão foi tomada.

Além disso, arquivado #32611 para discussão de on err, <statement>

@guybrand Minhas desculpas. Sim, concordo que precisamos analisar as várias propriedades de uma proposta, como redução padrão, se ela resolve o problema em questão etc. Mas uma proposta é mais do que a soma de suas partes - no final das contas, precisa olhar para o quadro geral. Isso é engenharia, e engenharia é confusa: há muitos fatores que influenciam um projeto e, mesmo que objetivamente (com base em critérios rígidos) uma parte de um projeto não seja satisfatória, ainda pode ser o projeto "certo" em geral. Portanto, estou um pouco hesitante em apoiar uma decisão baseada em algum tipo de classificação _independente_ dos aspectos individuais de uma proposta.

(Espero que isso aborde melhor o que você quis dizer.)

Mas em relação aos critérios relevantes, acredito que esta proposta deixa claro o que ela tenta abordar. Ou seja, a lista à qual você está se referindo já existe:

..., nosso objetivo é tornar o tratamento de erros mais leve, reduzindo a quantidade de código-fonte dedicado exclusivamente à verificação de erros. Também queremos tornar mais conveniente escrever código de tratamento de erros, para aumentar a probabilidade de os programadores levarem tempo para fazê-lo. Ao mesmo tempo, queremos manter o código de tratamento de erros explicitamente visível no texto do programa.

Acontece que, para decoração de erro, sugerimos usar um defer e parâmetros de resultado nomeados (ou ye olde if ) porque isso não precisa de uma mudança de idioma - o que é uma coisa fantástica porque as mudanças de idioma têm enormes custos ocultos. Percebemos que muitos comentaristas acham que essa parte do design "totalmente é uma droga". Ainda assim, neste ponto, no quadro geral, com tudo o que sabemos, achamos que pode ser bom o suficiente. Por outro lado, precisamos de uma mudança de idioma - suporte de idioma, em vez disso - para nos livrarmos do clichê, e try é a mudança mínima que poderíamos fazer. E claramente, tudo ainda está explícito no código.

Eu diria que as razões pelas quais há tantas reações e tantas mini-propostas é que esse é um problema em que quase todos concordam que a linguagem Go precisa fazer algo para diminuir o clichê do tratamento de erros, mas nós realmente não concordar em como fazê-lo.

Esta proposta, em essência, resume-se a uma "macro" incorporada para um caso muito comum, mas específico de clichê, muito parecido com a função append() incorporada. Portanto, embora seja útil para o caso de uso específico id err!=nil { return err } , isso também é tudo o que ele faz. Como não é muito útil em outros casos, nem realmente aplicável em geral, eu diria que é decepcionante. Tenho a sensação de que a maioria dos programadores de Go esperava um pouco mais e, portanto, a discussão neste tópico continua.

É contra-intuitivo como uma função. Porque não é possível em Go ter função com essa ordem de argumentos func(... interface{}, error) .
Digitado primeiro, o número variável de qualquer padrão está em toda parte nos módulos Go.

Quanto mais penso, mais gosto da proposta atual, tal como está.

Se precisarmos de tratamento de erros, sempre temos a instrução if.

Olá a todos. Obrigado pela discussão calma, respeitosa e construtiva até agora. Passei algum tempo fazendo anotações e acabei ficando frustrado o suficiente para criar um programa para me ajudar a manter uma visão diferente desse tópico de comentários que deveria ser mais navegável e completo do que o GitHub mostra. (Ele também carrega mais rápido!) Consulte https://swtch.com/try.html. Vou mantê-lo atualizado, mas em lotes, não minuto a minuto. (Esta é uma discussão que requer uma reflexão cuidadosa e não é ajudada pelo "tempo de internet".)

Eu tenho alguns pensamentos a acrescentar, mas isso provavelmente terá que esperar até segunda-feira. Obrigado novamente.

@mishak87 Abordamos isso na proposta detalhada . Observe que temos outros built-ins ( try , make , unsafe.Offsetof , etc.) que são "irregulares" - é para isso que servem os built-ins.

@rsc , super útil! Se você ainda estiver revisando, talvez vincule as referências do problema #id? E estilo de fonte sem serifa?

Isso provavelmente já foi abordado antes, então peço desculpas por adicionar ainda mais ruído, mas só queria fazer um ponto sobre try builtin vs the try ... else idea.

Eu acho que tentar a função interna pode ser um pouco frustrante durante o desenvolvimento. Podemos ocasionalmente querer adicionar símbolos de depuração ou adicionar mais contexto específico de erro antes de retornar. Alguém teria que reescrever uma linha como

user := try(getUser(userID))

para

user, err := getUser(userID)
if err != nil {  
    // inspect error here
    return err
}

Adicionar uma instrução defer pode ajudar, mas ainda não é a melhor experiência quando uma função gera vários erros, pois seria acionada para cada chamada try().

Reescrever várias chamadas try() aninhadas na mesma função seria ainda mais irritante.

Por outro lado, adicionar contexto ou código de inspeção

user := try getUser(userID)

seria tão simples quanto adicionar uma instrução catch no final seguida pelo código

user := try getUser(userID) catch {
   // inspect error here
}

Remover ou desabilitar temporariamente um manipulador seria tão simples quanto quebrar a linha antes de capturar e comentar.

Alternar entre try() e if err != nil parece muito mais irritante IMO.

Isso também se aplica à adição ou remoção de contexto de erro. Pode-se escrever try func() enquanto prototipa algo muito rapidamente e, em seguida, adicionar contexto a erros específicos conforme necessário à medida que o programa amadurece, em oposição a try() como um built-in onde seria necessário reescrever o linhas para adicionar contexto ou adicionar código de inspeção extra durante a depuração.

Tenho certeza de que try() seria útil, mas como imagino usá-lo no meu dia a dia de trabalho, não posso deixar de imaginar como try ... catch seria muito mais útil e muito menos irritante quando eu ' d precisa adicionar/remover código extra específico para alguns erros.


Além disso, acho que adicionar try() e recomendar o uso de if err != nil para adicionar contexto é muito semelhante a ter make() vs new() vs := vs var . Esses recursos são úteis em diferentes cenários, mas não seria bom se tivéssemos menos maneiras ou até mesmo uma única maneira de inicializar variáveis? É claro que ninguém está forçando ninguém a usar try e as pessoas podem continuar a usar if err != nil, mas sinto que isso dividirá o tratamento de erros em Go, assim como as várias maneiras de atribuir novas variáveis. Acho que qualquer método adicionado à linguagem também deve fornecer uma maneira de adicionar/remover manipuladores de erro facilmente, em vez de forçar as pessoas a reescrever linhas inteiras para adicionar/remover manipuladores. Isso não parece um bom resultado para mim.

Desculpe novamente pelo barulho, mas queria apontá-lo caso alguém quisesse escrever uma proposta detalhada separada para a ideia try ... else .

//cc @brynbellomy

Obrigado, @owais , por trazer isso à tona novamente - é um ponto justo (e o problema de depuração já foi mencionado antes ). try deixa a porta aberta para extensões, como um segundo argumento, que pode ser uma função de manipulador. Mas é verdade que uma função try não facilita a depuração - pode ser necessário reescrever o código um pouco mais do que um try - catch ou try - else .

@owais

Adicionar uma instrução defer pode ajudar, mas ainda não é a melhor experiência quando uma função gera vários erros, pois seria acionada para cada chamada try().

Você sempre pode incluir uma opção de tipo na função adiada que trataria (ou não) diferentes tipos de erro de maneira apropriada antes de retornar.

Dada a discussão até agora – especificamente as respostas da equipe Go – estou tendo a forte impressão de que a equipe planeja avançar com a proposta que está na mesa. Se sim, então um comentário e um pedido:

  1. A proposta atual da IMO resultará em uma redução não insignificante na qualidade do código nos repositórios disponíveis publicamente. Minha expectativa é que muitos desenvolvedores sigam o caminho de menor resistência, usem efetivamente técnicas de manipulação de exceções e optem por usar try() em vez de lidar com erros no momento em que ocorrerem. Mas, dado o sentimento predominante neste tópico, percebo que qualquer arrogância agora seria apenas uma batalha perdida, então estou apenas registrando minha objeção para a posteridade.

  2. Supondo que a equipe avance com a proposta atualmente escrita, você pode adicionar uma opção de compilador que desabilitará try() para aqueles que não desejam nenhum código que ignore erros dessa maneira e não permita programadores que eles contratam de usá-lo? _(via CI, claro.)_ Agradecemos antecipadamente por esta consideração.

você pode adicionar uma opção de compilador que desabilitará o try()

Isso teria que estar em uma ferramenta de linting, não no compilador IMO, mas concordo

Isso teria que estar em uma ferramenta de linting, não no compilador IMO, mas concordo

Estou solicitando explicitamente uma opção do compilador e não uma ferramenta de linting porque não permite a compilação, como opção. Caso contrário, será muito fácil _"esquecer"_ o fiapo durante o desenvolvimento local.

@mikeschinkel Não seria tão fácil esquecer de ativar a opção do compilador nessa situação?

Os sinalizadores do compilador não devem alterar a especificação da linguagem. Isso é muito mais adequado para veterinário / fiapos

Não seria tão fácil esquecer de ativar a opção do compilador nessa situação?

Não ao usar ferramentas como GoLand, onde não há como forçar um lint a ser executado antes de uma compilação.

Os sinalizadores do compilador não devem alterar a especificação da linguagem.

-nolocalimports altera a especificação e -s avisa.

Os sinalizadores do compilador não devem alterar a especificação da linguagem.

-nolocalimports altera a especificação e -s avisa.

Não, não altera a especificação. Não apenas a gramática da linguagem continua a mesma, mas a especificação afirma especificamente:

A interpretação do ImportPath depende da implementação, mas normalmente é uma substring do nome completo do arquivo do pacote compilado e pode ser relativa a um repositório de pacotes instalados.

Não ao usar ferramentas como GoLand, onde não há como forçar um lint a ser executado antes de uma compilação.

https://github.com/vmware/dispatch/wiki/Configure-GoLand-with-golint

@deanveloper

https://github.com/vmware/dispatch/wiki/Configure-GoLand-with-golint

Certamente isso existe, mas você está comparando maçã com órgão. O que você está mostrando é um observador de arquivos que é executado em arquivos que mudam e, como o GoLand salva automaticamente os arquivos, isso significa que ele é executado constantemente, o que gera muito mais ruído do que sinal.

O lint sempre não é e não pode (AFAIK) ser configurado como pré-condição para executar o compilador:

image

Não, não altera a especificação. Não apenas a gramática da linguagem continua a mesma, mas a especificação afirma especificamente:

Você está brincando com a semântica aqui em vez de se concentrar no resultado. Então eu vou fazer o mesmo.

Solicito que seja adicionada uma opção de compilador que não permita a compilação de código com try() . Isso não é uma solicitação para alterar a especificação da linguagem, é apenas uma solicitação para que o compilador pare neste caso especial.

E se isso ajudar, a especificação do idioma pode ser atualizada para dizer algo como:

A interpretação de try() depende da implementação, mas normalmente é uma que aciona um retorno quando o último parâmetro é um erro, mas pode ser implementado para não ser permitido.

A hora de pedir uma troca de compilador ou verificação veterinária é depois que o protótipo try() chega à dica 1.14(?). Nesse ponto, você registraria um novo problema para ele (e sim, acho que é uma boa ideia). Fomos solicitados a restringir os comentários aqui a informações factuais sobre o documento de design atual.

Oi, apenas para adicionar a todo o problema com a adição de instruções de depuração e tal durante o desenvolvimento.
Eu acho que a ideia do segundo parâmetro é boa para a função try() , mas outra ideia apenas para jogá-la fora é adicionar uma cláusula emit para ser uma segunda parte para try() .

Por exemplo, acredito que ao desenvolver e tal pode haver um caso em que eu queira chamar fmt neste instante para imprimir o erro. Então eu poderia partir disso:

func writeStuff(filename string) (io.ReadCloser, error) {
    f := try(os.Open(filename))
    try(fmt.Fprintf(f, "stuff\n"))

    return f, nil
}

Pode ser reescrito para algo assim para instruções de depuração ou manipulação geral ou o erro antes de retornar.

func writeStuff(filename string) (io.ReadCloser, error) {
    emit err {
        fmt.Printf("something happened [%v]\n", err.Error())
        return nil, err
    }

    f := try(os.Open(filename))
    try(fmt.Fprintf(f, "stuff\n"))

    return f, nil
}

Então aqui acabei colocando uma proposta para uma nova palavra-chave emit que poderia ser uma declaração ou uma linha para retorno imediato como a funcionalidade inicial try() :

emit return nil, err

O que a emissão seria é essencialmente apenas uma cláusula onde você pode colocar qualquer lógica que desejar se o try() for acionado por um erro não igual a nil. Outra habilidade com a palavra-chave emit é que você pode acessar o erro ali mesmo se adicionar logo após a palavra-chave um nome de variável como eu fiz no primeiro exemplo usando-a.

Esta proposta cria um pouco de verbosidade para a função try() , mas acho que é pelo menos um pouco mais claro sobre o que está acontecendo com o erro. Dessa forma, você também pode decorar os erros sem tê-los presos em uma linha e pode ver como os erros são tratados imediatamente quando estiver lendo a função.

Esta é uma resposta para @mikeschinkel , estou colocando minha resposta em um bloco de detalhes para não atrapalhar muito a discussão. De qualquer forma, @networkimprov está correto que esta discussão deve ser apresentada até que esta proposta seja implementada (se for).

detalhes sobre um sinalizador para desabilitar try
@mikeschinkel

O lint sempre não é e não pode (AFAIK) ser configurado como pré-condição para executar o compilador:

Reinstalei o GoLand apenas para testar isso. Isso parece funcionar bem, a única diferença é que, se o lint encontrar algo que não gosta, ele não falhará na compilação. Isso pode ser facilmente corrigido com um script personalizado, que executa golint e falha com um código de saída diferente de zero se houver alguma saída.
image

(Edit: Corrigi o erro que estava tentando me dizer na parte inferior. Ele estava funcionando bem mesmo enquanto o erro estava presente, mas alterar "Run Kind" para o diretório removeu o erro e funcionou bem)

Também outra razão pela qual NÃO deve ser um sinalizador do compilador - todo o código Go é compilado a partir da fonte. Isso inclui bibliotecas. Isso significa que se você quiser desativar $# try pelo compilador, você também desativará try para cada uma das bibliotecas que estiver usando. É apenas uma má ideia tê-lo como um sinalizador do compilador.

Você está brincando com a semântica aqui em vez de se concentrar no resultado.

Não, não estou. Os sinalizadores do compilador não devem alterar a especificação da linguagem. A especificação é muito bem definida e para que algo seja "Go", precisa seguir a especificação. Os sinalizadores do compilador que você mencionou alteram o comportamento da linguagem, mas não importa o que aconteça, eles garantem que a linguagem ainda siga a especificação. Este é um aspecto importante do Go. Contanto que você siga a especificação Go, seu código deve compilar em qualquer compilador Go.

Solicito que seja adicionada uma opção de compilador que não permita a compilação de código com try(). Isso não é uma solicitação para alterar a especificação da linguagem, é apenas uma solicitação para que o compilador pare neste caso especial.

É um pedido para alterar a especificação. Esta proposta em si é uma solicitação para alterar a especificação. As funções incorporadas são incluídas muito especificamente na especificação. . Pedir para ter um sinalizador do compilador que remova o try embutido seria, portanto, um sinalizador do compilador que alteraria a especificação da linguagem que está sendo compilada.

Dito isso, acho que ImportPath deveria ser padronizado na especificação. Posso fazer uma proposta para isso.

E se ajudar, a especificação do idioma pode ser atualizada para dizer algo como [...]

Embora isso seja verdade, você não deseja que a implementação de try seja dependente da implementação. Ele foi feito para ser uma parte importante do tratamento de erros da linguagem, que é algo que precisaria ser o mesmo em todos os compiladores Go.

@deanveloper

_"De qualquer forma, @networkimprov está correto que esta discussão deve ser apresentada até que esta proposta seja implementada (se for)."_

Então, por que você decidiu ignorar essa sugestão e postar neste tópico de qualquer maneira, em vez de esperar mais tarde? Você argumentou seus pontos aqui e, ao mesmo tempo, afirmou que eu não deveria contestar seus pontos. Pratique o que você prega...

Dada a sua escolha, optarei por responder também, também em um bloco de detalhes

aqui:

_"Isso pode ser facilmente corrigido com um script personalizado, que executa golint e falha com um código de saída diferente de zero se houver alguma saída."_

Sim, com codificação suficiente, qualquer problema pode ser corrigido. Mas nós dois sabemos por experiência que quanto mais complexa é uma solução, menos pessoas que querem usá-la acabarão por usá-la.

Então, eu estava explicitamente pedindo uma solução simples aqui, não uma solução própria.

_"você estaria desativando o try para cada uma das bibliotecas que você está usando também."_

E essa é _explicitamente_ a razão pela qual eu solicitei. Porque eu quero garantir que todo o código que usa esse problemático _"recurso"_ não chegue aos executáveis ​​que distribuímos.

_"É uma solicitação para alterar a especificação. Esta proposta em si é uma solicitação para alterar a especificação._"

ABSOLUTAMENTE não é uma mudança na especificação. É uma solicitação de um switch para alterar o _behavior_ do comando build , não uma alteração na especificação do idioma.

Se alguém solicitar que o comando go tenha uma opção para exibir sua saída de terminal em mandarim, isso não é uma alteração na especificação do idioma.

Da mesma forma, se go build visse essa opção, ele simplesmente emitiria uma mensagem de erro e pararia quando encontrar um try() . Nenhuma alteração de especificação de idioma é necessária.

_"Ele foi feito para ser uma parte importante do tratamento de erros da linguagem, que é algo que precisaria ser o mesmo em todos os compiladores Go."_

Será uma parte problemática do tratamento de erros da linguagem e torná-lo opcional permitirá que aqueles que desejam evitar seus problemas possam fazê-lo.

Sem o switch, é provável que a maioria das pessoas apenas veja como um novo recurso e o abrace e nunca se pergunte se de fato ele deve ser usado.

_Com o switch_ — e artigos explicando o novo recurso que menciona o switch — muitas pessoas entenderão que ele tem potencial problemático e, portanto, permitirá que a equipe Go estude se foi uma boa inclusão ou não, vendo quanto código público evita usá-lo vs. como o código público o usa. Isso poderia informar o design do Go 3.

_"Não, não estou. Os sinalizadores do compilador não devem alterar a especificação do idioma."_

Dizer que você não está jogando semântica não significa que você não está jogando semântica.

Multar. Em seguida, solicito um novo comando de nível superior chamado _(algo como)_ build-guard usado para proibir recursos problemáticos durante a compilação, começando com a proibição de try() .

É claro que o melhor resultado é se o recurso try() for apresentado com um plano para reconsiderar a solução do problema de uma maneira diferente no futuro, uma maneira com a qual a grande maioria concorda. Mas temo que o navio já tenha navegado em try() então espero minimizar sua desvantagem.


Então, agora, se você realmente concorda com @networkimprov , aguarde sua resposta até mais tarde, como eles sugeriram.

Desculpe interromper, mas tenho fatos a relatar :-)

Tenho certeza de que a equipe Go já testou o adiamento, mas não vi nenhum número...

$ go test -bench=.
goos: linux
goarch: amd64
BenchmarkAlways2-2      20000000                72.3 ns/op
BenchmarkAlways4-2      20000000                68.1 ns/op
BenchmarkAlways6-2      20000000                68.0 ns/op

BenchmarkNever2-2       100000000               16.5 ns/op
BenchmarkNever4-2       100000000               13.1 ns/op
BenchmarkNever6-2       100000000               13.5 ns/op

Fonte

package deferbench

import (
   "fmt"
   "errors"
   "testing"
)

func Always(iM, iN int) (err error) {
   defer func() {
      if err != nil {
         err = fmt.Errorf("d: %v", err)
      }
   }()
   if iN % iM == 0 {
      return errors.New("e")
   }
   return nil
}

func Never(iM, iN int) (err error) {
   if iN % iM == 0 {
      return fmt.Errorf("r: %v", errors.New("e"))
   }
   return nil
}

func BenchmarkAlways2(iB *testing.B) { for a := 0; a < iB.N; a++ { Always(1e2, a) }}
func BenchmarkAlways4(iB *testing.B) { for a := 0; a < iB.N; a++ { Always(1e4, a) }}
func BenchmarkAlways6(iB *testing.B) { for a := 0; a < iB.N; a++ { Always(1e6, a) }}

func BenchmarkNever2(iB *testing.B) { for a := 0; a < iB.N; a++ { Never(1e2, a) }}
func BenchmarkNever4(iB *testing.B) { for a := 0; a < iB.N; a++ { Never(1e4, a) }}
func BenchmarkNever6(iB *testing.B) { for a := 0; a < iB.N; a++ { Never(1e6, a) }}

@networkimprov

De https://github.com/golang/proposal/blob/master/design/32437-try-builtin.md#efficiency -of-defer (minha ênfase em negrito)

Independentemente, a equipe do compilador e do runtime Go tem discutido opções alternativas de implementação e acreditamos que podemos fazer usos típicos de adiamento para tratamento de erros tão eficiente quanto o código “manual” existente. Esperamos disponibilizar essa implementação de adiamento mais rápida no Go 1.14 (consulte também * CL 171758 * que é um primeiro passo nessa direção).

ou seja, adiar agora é 30% de melhoria de desempenho para go1.13 para uso comum e deve ser mais rápido e tão eficiente quanto o modo não adiado em go 1.14

Talvez alguém possa postar números para 1,13 e 1,14 CL?

Otimizações nem sempre sobrevivem ao contato com o inimigo... er, ecossistema.

1.13 adiamentos serão cerca de 30% mais rápidos:

name     old time/op  new time/op  delta
Defer-4  52.2ns ± 5%  36.2ns ± 3%  -30.70%  (p=0.000 n=10+10)

Isto é o que eu recebo nos testes do @networkimprov acima (1.12.5 para dica):

name       old time/op  new time/op  delta
Always2-4  59.8ns ± 1%  47.5ns ± 1%  -20.57%  (p=0.008 n=5+5)
Always4-4  57.9ns ± 2%  43.5ns ± 1%  -24.96%  (p=0.008 n=5+5)
Always6-4  57.6ns ± 2%  44.1ns ± 1%  -23.43%  (p=0.008 n=5+5)
Never2-4   13.7ns ± 8%   3.8ns ± 4%  -72.27%  (p=0.008 n=5+5)
Never4-4   10.5ns ± 6%   1.3ns ± 2%  -87.76%  (p=0.008 n=5+5)
Never6-4   10.8ns ± 6%   1.2ns ± 1%  -88.46%  (p=0.008 n=5+5)

(Não sei por que os Never são muito mais rápidos. Talvez o inlining mude?)

As otimizações para adiamentos para 1.14 ainda não foram implementadas, então não sabemos qual será o desempenho. Mas achamos que devemos nos aproximar do desempenho de uma chamada de função regular.

Então, por que você decidiu ignorar essa sugestão e postar neste tópico de qualquer maneira, em vez de esperar mais tarde?

O bloco de detalhes foi editado mais tarde, depois de ler o comentário de @networkimprov . Desculpe por fazer parecer que eu tinha entendido o que ele disse e ignorado. Estou encerrando a discussão após esta declaração, queria me explicar já que você me perguntou por que postei o comentário.


Em relação às otimizações a serem adiadas, estou animado por elas. Eles ajudam um pouco nessa proposta, tornando defer HandleErrorf(...) um pouco menos pesado. Ainda não gosto da ideia de abusar dos parâmetros nomeados para que esse truque funcione. Quanto é esperado para acelerar para 1,14? Eles devem correr em velocidades semelhantes?

@griesemer Uma área que pode valer a pena expandir um pouco mais é como as transições funcionam em um mundo com try , talvez incluindo:

  • O custo de transição entre estilos de decoração de erro.
  • As classes de possíveis erros que podem resultar na transição entre estilos.
  • Quais classes de erros seriam (a) detectadas imediatamente por um erro do compilador, vs. (b) detectadas por vet ou staticcheck ou similar, vs. (c) podem levar a um bug que pode não ser notado ou precisaria ser capturado por meio de testes.
  • O grau em que as ferramentas podem mitigar o custo e a chance de erro ao fazer a transição entre estilos e, em particular, se gopls (ou outro utilitário) pode ou deve ter um papel na automação de transições de estilo de decoração comuns.

Estágios de decoração de erro

Isso não é exaustivo, mas um conjunto representativo de estágios pode ser algo como:

0. Nenhuma decoração de erro (por exemplo, usando try sem qualquer decoração).
1. Decoração de erro uniforme (por exemplo, usando try + defer para decoração de uniforme).
2. N-1 pontos de saída têm decoração de erro uniforme , mas 1 ponto de saída tem decoração diferente (por exemplo, talvez uma decoração de erro detalhada permanente em apenas um local, ou talvez um log de depuração temporário, etc.).
3. Todos os pontos de saída têm uma decoração de erro única , ou algo que se aproxime de algo único.

Qualquer função não terá uma progressão estrita por esses estágios, então talvez "estágios" seja a palavra errada, mas algumas funções farão a transição de um estilo de decoração para outro, e pode ser útil ser mais explícito sobre o que essas transições são como quando ou se acontecem.

O estágio 0 e o estágio 1 parecem ser pontos ideais para a proposta atual e também são casos de uso bastante comuns. Uma transição de estágio 0->1 parece direta. Se você estava usando try sem qualquer decoração no estágio 0, você pode adicionar algo como defer fmt.HandleErrorf(&err, "foo failed with %s", arg1) . Nesse momento, você também pode precisar introduzir parâmetros de retorno nomeados na proposta, conforme escrito inicialmente. No entanto, se a proposta adotar uma das sugestões ao longo das linhas de uma variável interna predefinida que é um alias para o parâmetro de resultado do erro final, então o custo e o risco de erro aqui podem ser pequenos?

Por outro lado, uma transição de estágio 1->2 parece estranha (ou "irritante" como alguns outros disseram) se o estágio 1 for uma decoração de erro uniforme com um defer . Para adicionar um pedaço específico de decoração em um ponto de saída, primeiro você precisaria remover o defer (para evitar decoração dupla), então parece que seria necessário visitar todos os pontos de retorno para remover o try usa em instruções if , com N-1 dos erros sendo decorados da mesma maneira e 1 sendo decorado de forma diferente.

Uma transição de estágio 1->3 também parece estranha se feita manualmente.

Erros na transição entre estilos de decoração

Alguns erros que podem acontecer como parte de um processo manual de redução de açúcar incluem sombrear acidentalmente uma variável ou alterar como um parâmetro de retorno nomeado é afetado etc. Por exemplo, se você observar o primeiro e maior exemplo na seção "Exemplos" do try, a função CopyFile tem 4 usos try , incluindo nesta seção:

        w := try(os.Create(dst))
        defer func() {
                w.Close()
                if err != nil {
                        os.Remove(dst) // only if a “try” fails
                }
        }()

Se alguém fizesse uma remoção manual "óbvia" de w := try(os.Create(dst)) , essa linha poderia ser expandida para:

        w, err := os.Create(dst)
        if err != nil {
            // do something here
            return err
        }

Isso parece bom à primeira vista, mas dependendo do bloco em que a alteração está, isso também pode ocultar acidentalmente o parâmetro de retorno nomeado err e interromper o tratamento de erros no defer subsequente.

Automatizando a transição entre estilos de decoração

Para ajudar com o custo de tempo e o risco de erros, talvez gopls (ou outro utilitário) possa ter algum tipo de comando para desugar um try específico, ou um comando desugar todos os usos de try em uma determinada função que pode estar livre de erros 100% das vezes. Uma abordagem pode ser qualquer comando gopls se concentrar apenas na remoção e substituição try , mas talvez um comando diferente possa eliminar todos os usos de try enquanto também transforma pelo menos casos comuns de coisas como defer fmt.HandleErrorf(&err, "copy %s %s", src, dst) no topo da função para o código equivalente em cada um dos locais try anteriores (o que ajudaria na transição do estágio 1->2 ou do estágio 1->3). Essa não é uma ideia totalmente elaborada, mas talvez valha a pena pensar mais sobre o que é possível ou desejável ou atualizar a proposta com o pensamento atual.

Resultados idiomáticos?

Um comentário relacionado é que não é imediatamente óbvio com que frequência uma transformação programática livre de erros de um try acabaria parecendo um código Go idiomático normal. Adaptando um dos exemplos da proposta, se por exemplo você quiser desaçucar:

x1, x2, x3 = try(f())

Em alguns casos, uma transformação programática que preserva o comportamento pode resultar em algo como:

t1, t2, t3, te := f()  // visible temporaries
if te != nil {
        return x1, x2, x3, te
}
x1, x2, x3 = t1, t2, t3

Essa forma exata pode ser rara, e parece que os resultados de um editor ou IDE fazendo desaçucar programático muitas vezes podem acabar parecendo mais idiomáticos, mas seria interessante ouvir o quão verdadeiro isso é, inclusive em face de parâmetros de retorno nomeados possivelmente se tornando mais comum, e levando em consideração o sombreamento, := vs = , outros usos de err na mesma função, etc.

A proposta fala sobre possíveis diferenças de comportamento entre if e try devido a parâmetros de resultado nomeados, mas nessa seção em particular parece estar falando principalmente sobre a transição de if para try (na seção que conclui _"Embora esta seja uma diferença sutil, acreditamos que casos como esses são raros. Se o comportamento atual for esperado, mantenha a instrução if."_). Em contraste, pode haver diferentes erros possíveis que valem a pena elaborar ao fazer a transição de try de volta para if , preservando o comportamento idêntico.


De qualquer forma, desculpe o longo comentário, mas parece que o medo de altos custos de transição entre estilos está subjacente a algumas das preocupações expressas em alguns dos outros comentários postados aqui e, portanto, a sugestão de ser mais explícito sobre esses custos de transição e potenciais mitigações.

@thepudds Eu amo você está destacando os custos e possíveis bugs associados a como os recursos da linguagem podem afetar positiva ou negativamente a refatoração. Não é um tópico que vejo frequentemente discutido, mas um que pode ter um grande efeito a jusante.

uma transição de estágio 1->2 parece estranha se o estágio 1 for uma decoração de erro uniforme com um adiamento. Para adicionar um bit específico de decoração em um ponto de saída, primeiro você precisaria remover o defer (para evitar decoração dupla), então parece que seria necessário visitar todos os pontos de retorno para remover o açúcar que o try usa em instruções if, com N -1 dos erros sendo decorado da mesma forma e 1 sendo decorado de forma diferente.

É aqui que usar break em vez de return brilha com 1.12. Use-o em um bloco for range once { ... } onde once = "1" para demarcar a sequência de código que você pode querer sair e então se você precisar decorar apenas um erro você faz isso no ponto de break . E se você precisar decorar todos os erros, faça isso antes do único return no final do método.

A razão de ser um padrão tão bom é que ele é resiliente às mudanças de requisitos; você raramente precisa quebrar o código de trabalho para implementar novos requisitos. E é uma abordagem IMO mais limpa e óbvia do que voltar ao início do método antes de sair dele.

fwiw

Os resultados de @randall77 para meu benchmark mostram uma sobrecarga de mais de 40 ns por chamada para 1,12 e gorjeta. Isso implica que adiar pode inibir otimizações, renderizando melhorias para adiar discutíveis em alguns casos.

@networkimprov Defer atualmente inibe otimizações, e isso é parte do que gostaríamos de corrigir. Por exemplo, seria bom embutir o corpo da função defer'd assim como fazemos chamadas regulares embutidas.

Não consigo ver como quaisquer melhorias que fizermos seriam discutíveis. De onde vem essa afirmação?

De onde vem essa afirmação?

A sobrecarga de mais de 40 ns por chamada para uma função com um adiamento para encapsular o erro não mudou.

As mudanças em 1.13 são uma parte da otimização do adiamento. Há outras melhorias planejadas. Isso é abordado no documento de design e na parte do documento de design citado em algum ponto acima.

Re swtch.com/try.html e https://github.com/golang/go/issues/32437#issuecomment -502192315:

@rsc , super útil! Se você ainda estiver revisando, talvez vincule as referências do problema #id? E estilo de fonte sem serifa?

Essa página é sobre conteúdo. Não se concentre nos detalhes de renderização. Estou usando a saída de blackfriday no markdown de entrada inalterado (portanto, não há links #id específicos do GitHub) e estou feliz com a fonte serif.

Re desabilitar/verificar tente :

Desculpe, mas não haverá opções do compilador para desabilitar recursos específicos do Go, nem haverá verificações veterinárias dizendo para não usar esses recursos. Se o recurso for ruim o suficiente para desabilitar ou vetar, não o colocaremos. Por outro lado, se o recurso estiver lá, não há problema em usá-lo. Existe uma linguagem Go, não uma linguagem diferente para cada desenvolvedor com base em sua escolha de sinalizadores do compilador.

@mikeschinkel , duas vezes neste problema você descreveu o uso de try como _ignoring_ errors.
Em 7 de junho você escreveu, sob o título "Torna mais fácil para os desenvolvedores ignorarem erros":

Esta é uma repetição total do que os outros têm comentários, mas o que basicamente fornecer try() é análogo em muitos aspectos a simplesmente abraçar o seguinte como código idomático, e este é um código que nunca encontrará seu caminho em nenhum código -respeitando os navios do desenvolvedor:

f, _ := os.Open(filename)

Eu sei que posso ser melhor em meu próprio código, mas também sei que muitos de nós dependem da generosidade de outros desenvolvedores Go que publicam alguns pacotes tremendamente úteis, mas pelo que vi em _"Other People's Code(tm)"_ as melhores práticas no tratamento de erros são frequentemente ignoradas.

Então, sério, nós realmente queremos tornar mais fácil para os desenvolvedores ignorarem erros e permitir que eles poluam o GitHub com pacotes não robustos?

E então, em 14 de junho, novamente você se referiu ao uso de try como "código que ignora erros dessa maneira".

Se não fosse o trecho de código f, _ := os.Open(filename) , eu pensaria que você estava simplesmente exagerando ao caracterizar "verificar um erro e devolvê-lo" como "ignorar" um erro. Mas o trecho de código, junto com as muitas perguntas já respondidas no documento da proposta ou na especificação da linguagem, me fazem pensar se estamos falando da mesma semântica afinal. Então, apenas para ser claro e responder às suas perguntas:

Ao estudar o código da proposta, descobri que o comportamento não é óbvio e um pouco difícil de raciocinar.

Quando vejo try() envolvendo uma expressão, o que acontecerá se um erro for retornado?

Quando você vir try(f()) , se f() retornar um erro, o try interromperá a execução do código e retornará esse erro da função em cujo corpo o try aparece.

O erro será simplesmente ignorado?

Não. O erro nunca é ignorado. Ele é retornado, o mesmo que usar uma instrução return. Como:

{ err := f(); if err != nil { return err } }

Ou pulará para o primeiro ou o mais recente defer ,

A semântica é a mesma do uso de uma instrução de retorno.

As funções adiadas são executadas " na ordem inversa em que foram adiadas ".

e se assim for, ele definirá automaticamente uma variável chamada err dentro do encerramento que, ou passará como um parâmetro _(não vejo um parâmetro?)_.

A semântica é a mesma do uso de uma instrução de retorno.

Se você precisar fazer referência a um parâmetro de resultado em um corpo de função adiado, poderá dar um nome a ele. Veja o exemplo result em https://golang.org/ref/spec#Defer_statements.

E se não for um nome de erro automático, como nomeio? E isso significa que não posso declarar minha própria variável err na minha função, para evitar conflitos?

A semântica é a mesma do uso de uma instrução de retorno.

Uma instrução return sempre atribui aos resultados reais da função, mesmo se o resultado não tiver nome e mesmo se o resultado for nomeado, mas sombreado.

E ele vai chamar todos os defer s? Na ordem inversa ou na ordem normal?

A semântica é a mesma do uso de uma instrução de retorno.

As funções adiadas são executadas " na ordem inversa em que foram adiadas ". (A ordem inversa é a ordem regular.)

Ou ele retornará do fechamento e do func onde o erro foi retornado? _(Algo que eu nunca teria considerado se não tivesse lido aqui palavras que implicam isso.)_

Eu não sei o que isso significa, mas provavelmente a resposta é não. Eu recomendaria focar no texto da proposta e nas especificações e não em outros comentários aqui sobre o que esse texto pode ou não significar.

Depois de ler a proposta e todos os comentários até agora, honestamente, ainda não sei as respostas para as perguntas acima. É esse o tipo de recurso que queremos adicionar a uma linguagem cujos defensores defendem como sendo _"Capitão Óbvio?"_

Em geral, buscamos uma linguagem simples e fácil de entender. Lamento que tenha feito tantas perguntas. Mas esta proposta realmente está reutilizando o máximo possível da linguagem existente (em particular, adia), então deve haver muito poucos detalhes adicionais para aprender. Uma vez que você sabe disso

x, y := try(f())

meios

tmp1, tmp2, tmpE := f()
if tmpE != nil {
   return ..., tmpE
}
x, y := tmp1, tmp2

quase todo o resto deve seguir as implicações dessa definição.

Isso não é "ignorar" erros. Ignorar um erro é quando você escreve:

c, _ := net.Dial("tcp", "127.0.0.1:1234")
io.Copy(os.Stdout, c)

e o código entra em pânico porque net.Dial falhou e o erro foi ignorado, c é nil e a chamada de io.Copy para c.Read falha. Em contraste, este código verifica e retorna o erro:

 c := try(net.Dial("tcp", "127.0.0.1:1234"))
 io.Copy(os.Stdout, c)

Para responder à sua pergunta sobre se queremos encorajar o último em vez do primeiro: sim.

@damienfamed75 Sua declaração emit proposta se parece essencialmente com a declaração handle do projeto de rascunho . A principal razão para abandonar a declaração handle foi sua sobreposição com defer . Não está claro para mim por que alguém não pode simplesmente usar defer para obter o mesmo efeito que emit consegue.

@dominikh perguntou :

A acme começará a destacar try?

Muito sobre a proposta de tentativa é indeciso, no ar, desconhecido.

Mas esta pergunta eu posso responder definitivamente: não.

@rsc

Obrigado pela sua resposta.

_"duas vezes neste problema você descreveu o uso de try como ignorar erros."_

Sim, eu estava comentando usando minha perspectiva e não sendo tecnicamente correto.

O que eu quis dizer foi _"Permitir que erros sejam transmitidos sem serem decorados."_ Para mim isso é _"ignorar"_ — muito parecido com como as pessoas que usam manipulação de exceção _"ignorar"_ erros — mas certamente posso ver como os outros fariam ver a minha formulação como não sendo tecnicamente correcta.

_"Quando você vir try(f()) , se f() retornar um erro, o try interromperá a execução do código e retornará esse erro da função em cujo corpo o try aparece."_

Essa foi uma resposta a uma pergunta do meu comentário há algum tempo, mas agora eu descobri isso.

E acaba fazendo duas coisas que me deixam triste. Razões:

  1. Ele fará o caminho de menor resistência para evitar erros de decoração - incentivando muitos desenvolvedores a fazer exatamente isso - e muitos publicarão esse código para outros usarem, resultando em código disponível publicamente de qualidade inferior com tratamento de erros/relatórios de erros menos robustos .

  2. Para aqueles como eu que usam break e continue para tratamento de erros em vez de return — um padrão que é mais resiliente a mudanças de requisitos — nem poderemos usar try() , mesmo quando realmente não há motivo para anotar o erro.

_"Ou retornará tanto do encerramento quanto do func onde o erro foi retornado? (Algo que eu nunca teria considerado se não tivesse lido aqui palavras que implicam isso.)"_

_"Eu não sei o que isso significa, mas provavelmente a resposta é não. Eu recomendaria focar no texto da proposta e nas especificações e não em outros comentários aqui sobre o que esse texto pode ou não significar."_

Novamente, essa pergunta foi há mais de uma semana, então eu tenho uma melhor compreensão agora.

Para esclarecer, para a posteridade, o defer tem um fechamento, certo? Se você retornar desse fechamento, então - a menos que eu tenha entendido errado - ele não apenas retornará do fechamento, mas também retornará do func onde ocorreu o erro, certo? _(Não há necessidade de responder se sim.)_

func example() {
    defer func(err) {
       return err // returns from both defer and example()
    }
    try(SomethingThatReturnsAnError)    
} 

BTW, meu entendimento é que o motivo de try() é porque os desenvolvedores reclamaram do clichê. Também acho isso triste porque acho que o requisito de aceitar erros retornados que resulta nesse clichê é o que ajuda a tornar os aplicativos Go mais robustos do que em muitas outras linguagens.

Eu pessoalmente preferiria que você tornasse mais difícil não decorar erros do que tornar mais fácil ignorá-los. Mas reconheço que pareço estar em minoria nisso.


BTW, algumas pessoas propuseram uma sintaxe como uma das seguintes _(adicionei um hipotético .Extend() para manter meus exemplos concisos):_

f := try os.Open(filename) else err {
    err.Extend("Cannot open file %s",filename)
    //break, continue or return err   
}

Ou

try f := os.Open(filename) else err {
    err.Extend("Cannot open file %s",filename)
    //break, continue or return err    
}

E então outros afirmam que realmente não salva nenhum personagem sobre isso:

f,err := os.Open(filename)
if err != nil {
    err.Extend("Cannot open file %s",filename)
    //break, continue or return err    
}

Mas uma coisa que falta a crítica é que ele passa de 5 linhas para 4 linhas, uma redução do espaço vertical e isso parece significativo, principalmente quando você precisa de muitas dessas construções em um func .

Ainda melhor seria algo assim, que eliminaria 40% do espaço vertical _(embora dados os comentários sobre palavras-chave eu duvide que isso seja considerado):_

try f := os.Open(filename) 
    else err().Extend("Cannot open file %s",filename)
    end //break, continue or return err    

#fwiw


DE QUALQUER FORMA , como eu disse anteriormente, acho que o navio já partiu, então vou aprender a aceitá-lo.

Metas

Alguns comentários aqui questionaram o que estamos tentando fazer com a proposta. Como lembrete, o Error Handling Problem Statement que publicamos em agosto passado diz na seção “Metas” :

“Para Go 2, gostaríamos de tornar as verificações de erros mais leves, reduzindo a quantidade de texto do programa Go dedicado à verificação de erros. Também queremos tornar mais conveniente escrever o tratamento de erros, aumentando a probabilidade de que os programadores tenham tempo para fazê-lo.

Tanto as verificações de erros quanto o tratamento de erros devem permanecer explícitos, ou seja, visíveis no texto do programa. Não queremos repetir as armadilhas do tratamento de exceções.

O código existente deve continuar funcionando e permanecer tão válido quanto hoje. Quaisquer alterações devem interoperar com o código existente.”

Para obter mais informações sobre “as armadilhas do tratamento de exceções”, consulte a discussão na seção “Problema” mais longa. Em particular, as verificações de erros devem ser claramente anexadas ao que está sendo verificado.

@mikeschinkel ,

Para esclarecer, para a posteridade, o defer tem um fechamento, certo? Se você retornar desse fechamento, então - a menos que eu tenha entendido errado - ele não apenas retornará do fechamento, mas também retornará do func onde ocorreu o erro, certo? _(Não há necessidade de responder se sim.)_

Não. Não se trata de tratamento de erros, mas de funções adiadas. Nem sempre são fechamentos. Por exemplo, um padrão comum é:

func (d *Data) Op() int {
    d.mu.Lock()
    defer d.mu.Unlock()

     ... code to implement Op ...
}

Qualquer retorno de d.Op executa a chamada de desbloqueio adiada após a instrução return, mas antes que o código seja transferido para o chamador de d.Op. Nada feito dentro de d.mu.Unlock afeta o valor de retorno de d.Op. Uma instrução de retorno em d.mu.Unlock retorna do Unlock. Ele por si só não retorna de d.Op. Claro, uma vez que d.mu.Unlock retorna, o mesmo acontece com d.Op, mas não diretamente por causa de d.mu.Unlock. É um ponto sutil, mas importante.

Chegando ao seu exemplo:

func example() {
    defer func(err) {
       return err // returns from both defer and example()
    }
    try(SomethingThatReturnsAnError)    
} 

Pelo menos como está escrito, este é um programa inválido. Não estou tentando ser pedante aqui - os detalhes importam. Aqui está um programa válido:

func example() (err error) {
    defer func() {
        if err != nil {
            println("FAILED:", err.Error())
        }
    }()

    try(funcReturningError())
    return nil
}

Qualquer resultado de uma chamada de função adiada é descartado quando a chamada é executada, portanto, no caso em que o que é adiado é uma chamada para um encerramento, não faz sentido escrever o encerramento para retornar um valor. Então, se você escrever return err dentro do corpo da closure, o compilador lhe dirá "too many arguments to return" .

Portanto, não, escrever return err não retorna tanto da função adiada quanto da função externa em nenhum sentido real, e no uso convencional nem é possível escrever código que pareça fazer isso.

Muitas das contrapropostas postadas para este problema, sugerindo outras construções de tratamento de erros mais capazes, duplicam construções de linguagem existentes, como a instrução if. (Ou eles entram em conflito com o objetivo de “tornar as verificações de erros mais leves, reduzindo a quantidade de texto do programa Go para a verificação de erros.” Ou ambos.)

Em geral, Go já possui uma construção de tratamento de erros perfeitamente capaz: a linguagem inteira, especialmente as instruções if. @DavexPro estava certo ao se referir à entrada do blog Go Erros são valores . Não precisamos projetar uma sublinguagem totalmente separada preocupada com erros, nem deveríamos. Acho que o principal insight ao longo do último meio ano foi remover “handle” da proposta “check/handle” em favor de reutilizar a linguagem que já temos, incluindo voltar às declarações if quando apropriado. Essa observação sobre fazer o mínimo possível elimina de consideração a maioria das ideias em torno da parametrização adicional de um novo construto.

Com agradecimentos a @brynbellomy por seus muitos bons comentários, usarei seu try-else como exemplo ilustrativo. Sim, podemos escrever:

func doSomething() (int, error) {
    // Inline error handler
    a, b := try SomeFunc() else err {
        return 0, errors.Wrap(err, "error in doSomething:")
    }

    // Named error handlers
    handler logAndContinue err {
        log.Errorf("non-critical error: %v", err)
    }
    handler annotateAndReturn err {
        return 0, errors.Wrap(err, "error in doSomething:")
    }

    c, d := try SomeFunc() else logAndContinue
    e, f := try OtherFunc() else annotateAndReturn

    // ...

    return 123, nil
}

mas considerando todas as coisas, isso provavelmente não é uma melhoria significativa em relação ao uso de construções de linguagem existentes:

func doSomething() (int, error) {
    a, b, err := SomeFunc()
    if err != nil {
        return 0, errors.Wrap(err, "error in doSomething:")
    }

    // Named error handlers
    logAndContinue := func(err error) {
        log.Errorf("non-critical error: %v", err)
    }
    annotate:= func(err error) (int, error) {
        return 0, errors.Wrap(err, "error in doSomething:")
    }

    c, d, err := SomeFunc()
    if err != nil {
        logAndContinue(err)
    }
    e, f, err := SomeFunc()
    if err != nil {
        return annotate(err)
    }

    // ...

    return 123, nil
}

Ou seja, continuar a depender da linguagem existente para escrever a lógica de tratamento de erros parece preferível a criar uma nova instrução, seja try-else, try-goto, try-arrow ou qualquer outra coisa.

É por isso que try está limitado à semântica simples if err != nil { return ..., err } e nada mais: encurte o padrão comum, mas não tente reinventar todo o fluxo de controle possível. Quando uma instrução if ou uma função auxiliar é apropriada, esperamos que as pessoas continuem a usá-las.

@rsc Obrigado por esclarecer.

Correto, não acertei os detalhes. Acho que não uso defer com frequência suficiente para lembrar sua sintaxe.

_(FWIW acho que usar defer para algo mais complexo do que fechar um identificador de arquivo é menos óbvio por causa do salto para trás no func antes de retornar. Portanto, sempre coloque esse código no final do func após o for range once{...} meu código de tratamento de erros break s fora.)_

A sugestão de fazer cada tentativa de chamada em várias linhas entra em conflito direto com o objetivo de “tornar as verificações de erros mais leves, reduzindo a quantidade de texto do programa Go para verificação de erros”.

A sugestão de executar uma instrução if de teste de erro em uma única linha também conflita diretamente com esse objetivo. As verificações de erros não se tornam substancialmente mais leves nem reduzidas ao remover os caracteres de nova linha internos. Se alguma coisa, eles se tornam mais difíceis de roçar.

O principal benefício de tentar é ter uma abreviação clara para o caso mais comum, fazendo com que os incomuns se destaquem mais e vale a pena ler com atenção.

Fazendo backup de gofmt para ferramentas gerais, a sugestão de focar em ferramentas para escrever verificações de erros em vez de uma mudança de idioma é igualmente problemática. Como Abelson e Sussman colocaram, “Os programas devem ser escritos para que as pessoas leiam, e apenas incidentalmente para que as máquinas executem”. Se a máquina-ferramenta for _necessária_ para lidar com a linguagem, então a linguagem não está fazendo seu trabalho. A legibilidade não deve ser limitada a pessoas que usam ferramentas específicas.

Algumas pessoas executaram a lógica na direção oposta: as pessoas podem escrever expressões complexas, então elas inevitavelmente o farão, então você precisaria de IDE ou outra ferramenta de suporte para encontrar as expressões try, então try é uma má ideia. Existem alguns saltos não suportados aqui, no entanto. A principal é a alegação de que, por ser _possível_ escrever código complexo e ilegível, esse código se tornará onipresente. Como @josharian observou, já é “ possível escrever código abominável em Go ”. Isso não é comum porque os desenvolvedores têm normas sobre tentar encontrar a maneira mais legível de escrever um determinado trecho de código. Portanto, certamente _não_ o caso de que o suporte IDE será necessário para ler programas envolvendo try. E nos poucos casos em que as pessoas escrevem um código realmente terrível tentando abusar, é improvável que o suporte IDE seja muito útil. Essa objeção – as pessoas podem escrever códigos muito ruins usando o novo recurso – é levantada em praticamente todas as discussões sobre cada novo recurso de linguagem em todas as linguagens. Não é muito útil. Uma objeção mais útil seria da forma “as pessoas escreverão código que parece bom no começo, mas acaba sendo menos bom por esse motivo inesperado”, como na discussão sobre depuração de impressões .

Novamente: A legibilidade não deve ser limitada a pessoas que usam ferramentas específicas.
(Ainda imprimo e leio programas em papel, embora as pessoas muitas vezes me dêem uma aparência estranha por fazer isso.)

Obrigado @rsc por fornecer seus pensamentos sobre permitir que as instruções if sejam passadas como uma única linha.

A sugestão de executar uma instrução if de teste de erro em uma única linha também conflita diretamente com esse objetivo. As verificações de erros não se tornam substancialmente mais leves nem reduzidas ao remover os caracteres de nova linha internos. Se alguma coisa, eles se tornam mais difíceis de roçar.

Eu estimo essas afirmações de forma diferente.

Acho que reduzir o número de linhas de 3 para 1 é substancialmente mais leve. O gofmt exigir que uma instrução if contenha, por exemplo, 9 (ou mesmo 5) novas linhas em vez de 3, não seria substancialmente mais pesado? É o mesmo fator (quantidade) de redução/expansão. Eu diria que literais de estrutura têm essa troca exata e, com a adição de try , permitirão o fluxo de controle tanto quanto uma instrução if .

Em segundo lugar, acho o argumento de que eles se tornam mais difíceis de aplicar para try , se não mais. Pelo menos uma instrução if teria que estar em sua própria linha. Mas talvez eu entenda mal o que se entende por "desnatar" neste contexto. Estou usando para significar "principalmente pular, mas estar ciente".

Tudo isso dito, a sugestão gofmt foi baseada em dar um passo ainda mais conservador do que try e não tem impacto em try a menos que seja suficiente. Parece que não é, então se eu quiser discutir mais vou abrir uma nova questão/proposta. :+1:

Acho que reduzir o número de linhas de 3 para 1 é substancialmente mais leve.

Acho que todos concordam que é possível que o código seja muito denso. Por exemplo, se todo o seu pacote é uma linha, acho que todos concordamos que isso é um problema. Todos nós provavelmente discordamos sobre a linha precisa. Para mim, estabelecemos

n, err := src.Read(buf)
if err == io.EOF {
    return nil
} else if err != nil {
    return err
}

como forma de formatar esse código, e acho que seria bastante chocante tentar mudar para o seu exemplo

n, err := src.Read(buf)
if err == io.EOF { return nil }
else if err != nil { return err }

em vez de. Se tivéssemos começado assim, tenho certeza que seria bom. Mas não o fizemos, e não é onde estamos agora.

Pessoalmente, acho o antigo peso mais leve na página no sentido de que é mais fácil de deslizar. Você pode ver o if-else de relance sem ler nenhuma letra real. Em contraste, a versão mais densa é difícil de distinguir de relance a partir de uma sequência de três declarações, o que significa que você tem que olhar com mais cuidado antes que seu significado fique claro.

No final, tudo bem se desenharmos a linha densidade versus legibilidade em lugares diferentes em relação ao número de novas linhas. A proposta try é focada não apenas em remover novas linhas, mas em remover completamente as construções, e isso produz uma presença de página mais leve separada da pergunta gofmt.

Algumas pessoas executaram a lógica na direção oposta: as pessoas podem escrever expressões complexas, então elas inevitavelmente o farão, então você precisaria de IDE ou outra ferramenta de suporte para encontrar as expressões try, então try é uma má ideia. Existem alguns saltos não suportados aqui, no entanto. A principal é a alegação de que, por ser _possível_ escrever código complexo e ilegível, esse código se tornará onipresente. Como @josharian observou, já é “ possível escrever código abominável em Go ”. Isso não é comum porque os desenvolvedores têm normas sobre tentar encontrar a maneira mais legível de escrever um determinado trecho de código. Portanto, certamente _não_ o caso de que o suporte IDE será necessário para ler programas envolvendo try. E nos poucos casos em que as pessoas escrevem um código realmente terrível tentando abusar, é improvável que o suporte IDE seja muito útil. Essa objeção – as pessoas podem escrever códigos muito ruins usando o novo recurso – é levantada em praticamente todas as discussões sobre cada novo recurso de linguagem em todas as linguagens. Não é muito útil.

Não é esse o motivo pelo qual Go não possui um operador ternário ?

Não é esse o motivo pelo qual Go não possui um operador ternário?

Não. Podemos e devemos distinguir entre "esse recurso pode ser usado para escrever código muito legível, mas também pode ser abusado para escrever código ilegível" e "o uso dominante desse recurso será escrever código ilegível".

A experiência com C sugere que ? : cai diretamente na segunda categoria. (Com a possível exceção de min e max, não tenho certeza se já vi código usando ? : isso não foi melhorado reescrevendo-o para usar uma instrução if. Mas este parágrafo está saindo do tópico.)

Sintaxe

Esta discussão identificou seis sintaxes diferentes para escrever a mesma semântica da proposta:

(Desculpe se eu entendi as histórias de origem erradas!)

Todos eles têm prós e contras, e o bom é que, como todos têm a mesma semântica, não é muito importante escolher entre as várias sintaxes para experimentar mais.

Achei este exemplo de @brynbellomy instigante:

headRef := try(r.Head())
parentObjOne := try(headRef.Peel(git.ObjectCommit))
parentObjTwo := try(remoteBranch.Reference.Peel(git.ObjectCommit))
parentCommitOne := try(parentObjOne.AsCommit())
parentCommitTwo := try(parentObjTwo.AsCommit())
treeOid := try(index.WriteTree())
tree := try(r.LookupTree(treeOid))

// vs

try headRef := r.Head()
try parentObjOne := headRef.Peel(git.ObjectCommit)
try parentObjTwo := remoteBranch.Reference.Peel(git.ObjectCommit)
try parentCommitOne := parentObjOne.AsCommit()
try parentCommitTwo := parentObjTwo.AsCommit()
try treeOid := index.WriteTree()
try tree := r.LookupTree(treeOid)

// vs

try (
    headRef := r.Head()
    parentObjOne := headRef.Peel(git.ObjectCommit)
    parentObjTwo := remoteBranch.Reference.Peel(git.ObjectCommit)
    parentCommitOne := parentObjOne.AsCommit()
    parentCommitTwo := parentObjTwo.AsCommit()
    treeOid := index.WriteTree()
    tree := r.LookupTree(treeOid)
)

Não há muita diferença entre esses exemplos específicos, é claro. E se a tentativa existe em todas as linhas, por que não alinhá-las ou fatorá-las? Isso não é mais limpo? Eu me perguntei sobre isso também.

Mas como @ianlancetaylor observou , “a tentativa enterra o lede. O código se torna uma série de instruções try, que obscurecem o que o código está realmente fazendo.”

Acho que esse é um ponto crítico: alinhar a tentativa dessa maneira, ou fatorá-la como no bloco, implica um falso paralelismo. Isso implica que o importante sobre essas declarações é que todas elas tentam. Isso normalmente não é a coisa mais importante sobre o código e não é o que devemos focar ao lê-lo.

Suponha, para fins de argumento, que AsCommit nunca falha e, consequentemente, não retorna um erro. Agora temos:

headRef := try(r.Head())
parentObjOne := try(headRef.Peel(git.ObjectCommit))
parentObjTwo := try(remoteBranch.Reference.Peel(git.ObjectCommit))
parentCommitOne := parentObjOne.AsCommit()
parentCommitTwo := parentObjTwo.AsCommit()
treeOid := try(index.WriteTree())
tree := try(r.LookupTree(treeOid))

// vs

try headRef := r.Head()
try parentObjOne := headRef.Peel(git.ObjectCommit)
try parentObjTwo := remoteBranch.Reference.Peel(git.ObjectCommit)
parentCommitOne := parentObjOne.AsCommit()
parentCommitTwo := parentObjTwo.AsCommit()
try treeOid := index.WriteTree()
try tree := r.LookupTree(treeOid)

// vs

try (
    headRef := r.Head()
    parentObjOne := headRef.Peel(git.ObjectCommit)
    parentObjTwo := remoteBranch.Reference.Peel(git.ObjectCommit)
)
parentCommitOne := parentObjOne.AsCommit()
parentCommitTwo := parentObjTwo.AsCommit()
try (
    treeOid := index.WriteTree()
    tree := r.LookupTree(treeOid)
)

O que você vê à primeira vista é que as duas linhas do meio são claramente diferentes das outras. Por quê? Acontece por causa do tratamento de erros. Esse é o detalhe mais importante sobre esse código, o que você deve notar à primeira vista? Minha resposta é não. Eu acho que você deve observar a lógica central do que o programa está fazendo primeiro e o tratamento de erros depois. Neste exemplo, a instrução try e o bloco try impedem essa visão da lógica principal. Para mim, isso sugere que eles não são a sintaxe correta para essa semântica.

Isso deixa as quatro primeiras sintaxes, que são ainda mais semelhantes entre si:

headRef := try(r.Head())
parentObjOne := try(headRef.Peel(git.ObjectCommit))
parentObjTwo := try(remoteBranch.Reference.Peel(git.ObjectCommit))
parentCommitOne := parentObjOne.AsCommit()
parentCommitTwo := parentObjTwo.AsCommit()
treeOid := try(index.WriteTree())
tree := try(r.LookupTree(treeOid))

// vs

headRef := try r.Head()
parentObjOne := try headRef.Peel(git.ObjectCommit)
parentObjTwo := try remoteBranch.Reference.Peel(git.ObjectCommit)
parentCommitOne := parentObjOne.AsCommit()
parentCommitTwo := parentObjTwo.AsCommit()
treeOid := try index.WriteTree()
tree := try r.LookupTree(treeOid)

// vs

headRef := r.Head()?
parentObjOne := headRef.Peel(git.ObjectCommit)?
parentObjTwo := remoteBranch.Reference.Peel(git.ObjectCommit)?
parentCommitOne := parentObjOne.AsCommit()
parentCommitTwo := parentObjTwo.AsCommit()
treeOid := index.WriteTree()?
tree := r.LookupTree(treeOid)?

// vs

headRef := r.Head?()
parentObjOne := headRef.Peel?(git.ObjectCommit)
parentObjTwo := remoteBranch.Reference.Peel?(git.ObjectCommit)
parentCommitOne := parentObjOne.AsCommit()
parentCommitTwo := parentObjTwo.AsCommit()
treeOid := index.WriteTree?()
tree := r.LookupTree?(treeOid)

É difícil ficar muito preocupado em escolher um sobre os outros. Todos eles têm seus pontos bons e ruins. As vantagens mais importantes do formulário interno são:

(1) o operando exato é muito claro, especialmente em comparação com o operador de prefixo try x.y().z() .
(2) ferramentas que não precisam saber sobre try podem tratá-lo como uma chamada de função simples, então, por exemplo, goimports funcionará bem sem nenhum ajuste, e
(3) há espaço para futuras expansões e ajustes, se necessário.

É inteiramente possível que, depois de ver o código real usando essas construções, desenvolvamos uma noção melhor se as vantagens de uma das outras três sintaxes superam as vantagens da sintaxe de chamada de função. Somente experimentos e experiências podem nos dizer isso.

Obrigado por todos os esclarecimentos. Quanto mais penso mais gosto da proposta e vejo como ela se encaixa nos objetivos.

Por que não usar uma função como recover() em vez de err que não sabemos de onde vem? Seria mais consistente e talvez mais fácil de implementar.

func f() error {
 defer func() {
   if err:=error();err!=nil {
     ...
   }
 }()
}

edit: eu nunca uso o retorno nomeado, então será estranho para mim adicionar o retorno nomeado apenas para isso

@flibustenet , consulte também https://swtch.com/try.html#named para obter algumas sugestões semelhantes.
(Respondendo a todos eles: poderíamos fazer isso, mas não é estritamente necessário dados os resultados nomeados, então podemos tentar usar o conceito existente antes de decidir que precisamos fornecer uma segunda maneira.)

Uma consequência não intencional de try() pode ser que os projetos abandonem _go fmt_ para obter verificações de erro de linha única. Isso é quase todos os benefícios de try() sem nenhum dos custos. Eu tenho feito isso por alguns anos; isso funciona bem.

Mas eu prefiro ser capaz de definir um manipulador de erro de último recurso para o pacote e eliminar todas as verificações de erro que precisam dele. O que eu definiria não é try() .

@networkimprov , você parece estar vindo de uma posição diferente dos usuários Go que estamos segmentando, e sua mensagem contribuiria mais para a conversa se contivesse detalhes ou links adicionais para que possamos entender melhor seu ponto de vista.

Não está claro quais "custos" você acredita que tentar tem. E enquanto você diz que abandonar o gofmt não tem "nenhum dos custos" de tentar (quaisquer que sejam), você parece estar ignorando que a formatação do gofmt é a usada por todos os programas que ajudam a reescrever o código-fonte Go, como goimports, por exemplo, gorename , e assim por diante. Você abandona o go fmt ao custo de abandonar esses auxiliares, ou pelo menos suportar edições acidentais substanciais em seu código quando você os invoca. Mesmo assim, se funcionar bem para você, ótimo: continue fazendo isso.

Também não está claro o que significa "definir um manipulador de erros de último recurso para o pacote" ou por que seria apropriado aplicar uma política de tratamento de erros a um pacote inteiro em vez de uma única função por vez. Se a principal coisa que você deseja fazer em um manipulador de erros é adicionar contexto, o mesmo contexto não seria apropriado em todo o pacote.

@rsc , Como você deve ter visto, enquanto sugeri a sintaxe do bloco try, mais tarde reverti para o lado "não" para esse recurso - em parte porque me sinto desconfortável em ocultar um ou mais retornos de erro condicional em uma instrução ou aplicativo de função. Mas deixe-me esclarecer um ponto. Na proposta do bloco try, permiti explicitamente declarações que não precisam de try . Então, seu último exemplo de bloco try seria:

try (
    headRef := r.Head()
    parentObjOne := headRef.Peel(git.ObjectCommit)
    parentObjTwo := remoteBranch.Reference.Peel(git.ObjectCommit)
        parentCommitOne := parentObjOne.AsCommit()
        parentCommitTwo := parentObjTwo.AsCommit()
    treeOid := index.WriteTree()
    tree := r.LookupTree(treeOid)
)

Isso simplesmente diz que quaisquer erros retornados dentro do bloco try são retornados ao chamador. Se o controle passar pelo bloco try, não houve erros no bloco.

Você disse

Eu acho que você deve observar a lógica central do que o programa está fazendo primeiro e o tratamento de erros depois.

Esta é exatamente a razão pela qual eu pensei em um bloco try! O que é fatorado não é apenas a palavra-chave, mas o tratamento de erros. Não quero ter que pensar em N lugares diferentes que podem gerar erros (exceto quando estou tentando explicitamente lidar com erros específicos).

Mais alguns pontos que podem valer a pena mencionar:

  1. O chamador não sabe exatamente de onde veio o erro dentro do chamador. Isso também vale para a proposta simples que você está considerando em geral. Eu especulei que o compilador pode ser feito para adicionar sua própria anotação no ponto de retorno do erro. Mas não tenho pensado muito nisso.
  2. Não está claro para mim se expressões como try(try(foo(try(bar)).fum()) são permitidas. Tal uso pode ser desaprovado, mas sua semântica precisa ser especificada. No caso do bloco try, o compilador tem que trabalhar mais para detectar tais usos e espremer todo o tratamento de erros para o nível do bloco try.
  3. Estou mais inclinado a gostar de return-on-error em vez de try . Isso é mais fácil de engolir em um nível de bloco!
  4. Por outro lado, quaisquer palavras-chave longas tornam as coisas menos legíveis.

FWIW, eu ainda não acho que isso vale a pena fazer.

@rsc

[...]
A principal é a afirmação de que, como é possível escrever código complexo e ilegível, esse código se tornará onipresente. Como @josharian observou, já é “possível escrever código abominável em Go”.
[...]

headRef := try(r.Head())
parentObjOne := try(headRef.Peel(git.ObjectCommit))
parentObjTwo := try(remoteBranch.Reference.Peel(git.ObjectCommit))
parentCommitOne := try(parentObjOne.AsCommit())
parentCommitTwo := try(parentObjTwo.AsCommit())

Entendo que sua posição sobre "código ruim" é que podemos escrever códigos horríveis hoje, como o bloco a seguir.

parentCommitOne := try(try(try(r.Head()).Peel(git.ObjectCommit)).AsCommit())
parentCommitTwo := try(try(remoteBranch.Reference.Peel(git.ObjectCommit)).AsCommit())

O que você acha de não permitir chamadas try aninhadas para que não possamos escrever código incorreto acidentalmente?

Se você não permitir try aninhado na primeira versão, poderá remover essa limitação posteriormente, se necessário, não seria possível o contrário.

Já discuti esse ponto, mas parece relevante - a complexidade do código deve ser dimensionada verticalmente, não horizontalmente.

try como uma expressão incentiva a complexidade do código a escalar horizontalmente, incentivando chamadas aninhadas. try como uma declaração incentiva a complexidade do código a escalar verticalmente.

@rsc , para suas perguntas,

Meu manipulador de nível de pacote de último recurso - quando o erro não é esperado:

func quit(err error) {
   fmt.Fprintf(os.Stderr, "quit after %s\n", err)
   debug.PrintStack()      // because panic(err) produces a pile of noise
   os.Exit(3)
}

Contexto: Eu uso muito o os.File (onde encontrei dois bugs: #26650 e #32088)

Um decorador em nível de pacote que adiciona contexto básico precisaria de um argumento caller -- uma estrutura gerada que fornece os resultados de runtime.Caller().

Desejo que o reescritor _go fmt_ use a formatação existente ou permita que você especifique a formatação por transformação. Eu me contento com outras ferramentas.

Os custos (ou seja, desvantagens) de try() estão bem documentados acima.

Sinceramente, estou chocado que a equipe Go nos ofereceu primeiro check/handle (por caridade, uma nova ideia), e depois o ternário try() . Não vejo por que você não emitiu uma RFP sobre tratamento de erros e, em seguida, coletou comentários da comunidade sobre algumas das propostas resultantes (consulte #29860). Há muita sabedoria aqui que você pode aproveitar!

@rsc

Sintaxe

Esta discussão identificou seis sintaxes diferentes para escrever a mesma semântica da proposta:

try {error} {optional wrap func} {optional return args in brackets}

f, err := os.Open(file)
try err wrap { a, b }

... e, IMO, melhorando a legibilidade (através de aliteração), bem como a precisão semântica:

f, err := os.Open(file)
relay err

ou

f, err := os.Open(file)
relay err wrap

ou

f, err := os.Open(file)
relay err wrap { a, b }

ou

f, err := os.Open(file)
relay err { a, b }

Eu sei que defender o revezamento versus o try é fácil de descartar como fora do tópico, mas posso imaginar tentar explicar como o try não está tentando nada e não lança nada. Não está claro E tem bagagem. relay ser um termo novo permitiria uma explicação clara, e a descrição tem uma base em circuitos (que é o que se trata de qualquer maneira).

Edite para esclarecer:
Tentar pode significar - 1. experimentar algo e depois julgá-lo subjetivamente 2. verificar algo objetivamente 3. tentar fazer algo 4. disparar vários fluxos de controle que podem ser interrompidos e lançar uma notificação interceptável se for o caso

Nesta proposta, tentar não está fazendo nada disso. Na verdade, estamos executando uma função. Em seguida, ele está religando o fluxo de controle com base em um valor de erro. Esta é literalmente a definição de um relé de proteção. Estamos religando diretamente os circuitos (isto é, curto-circuitando o escopo da função atual) de acordo com o valor de um erro testado.

Na proposta do bloco try, permiti explicitamente declarações que não precisam de try

A principal vantagem no tratamento de erros do Go que vejo sobre o sistema try-catch de linguagens como Java e Python é que sempre fica claro quais chamadas de função podem resultar em erro e quais não. A beleza do try , conforme documentado na proposta original, é que ele pode reduzir o clichê de manipulação de erros simples, mantendo esse importante recurso.

Para pegar emprestado os exemplos de @Goodwine , apesar de sua feiúra, de uma perspectiva de tratamento de erros, mesmo isso:

parentCommitOne := try(try(try(r.Head()).Peel(git.ObjectCommit)).AsCommit())
parentCommitTwo := try(try(remoteBranch.Reference.Peel(git.ObjectCommit)).AsCommit())

... é melhor do que você costuma ver em linguagens try-catch

parentCommitOne := r.Head().Peel(git.ObjectCommit).AsCommit()
parentCommitTwo := remoteBranch.Reference.Peel(git.ObjectCommit).AsCommit()

... porque você ainda pode dizer quais partes do código podem desviar o fluxo de controle devido a um erro e quais não podem.

Eu sei que @bakul não está defendendo essa proposta de sintaxe de bloco de qualquer maneira, mas acho que traz um ponto interessante sobre o tratamento de erros do Go em comparação com outros. Acho importante que qualquer proposta de tratamento de erros que Go adote não ofusque quais partes do código podem e não podem apresentar erros.

Eu escrevi uma pequena ferramenta: tryhard (que não se esforça muito no momento) opera arquivo por arquivo e usa correspondência de padrões AST simples para reconhecer candidatos em potencial para try e reportá-los (e reescrevê-los). A ferramenta é primitiva (sem verificação de tipo) e há uma boa chance de falsos positivos, dependendo do estilo de codificação predominante. Leia a documentação para obter detalhes.

Aplicando-o a $GOROOT/src nos relatórios de dicas > 5.000 (!) oportunidades para try . Pode haver muitos falsos positivos, mas verificar uma amostra decente manualmente sugere que a maioria das oportunidades é real.

O uso do recurso de reescrita mostra como o código ficará usando try . Mais uma vez, uma rápida olhada na saída mostra uma melhora significativa em minha mente.

( Cuidado: o recurso de reescrita destruirá os arquivos! Use por sua conta e risco. )

Espero que isso forneça algumas informações concretas sobre como o código pode parecer usando try e nos permita passar por especulações ociosas e improdutivas.

Obrigado e aproveite.

Entendo que sua posição sobre "código ruim" é que podemos escrever códigos horríveis hoje, como o bloco a seguir.

parentCommitOne := try(try(try(r.Head()).Peel(git.ObjectCommit)).AsCommit())
parentCommitTwo := try(try(remoteBranch.Reference.Peel(git.ObjectCommit)).AsCommit())

Minha posição é que os desenvolvedores Go fazem um trabalho decente escrevendo código claro e que quase certamente o compilador não é a única coisa que está no caminho de você ou seus colegas de trabalho escreverem código que se parece com isso.

O que você acha de não permitir chamadas try aninhadas para que não possamos escrever código incorreto acidentalmente?

Grande parte da simplicidade do Go deriva da seleção de feições ortogonais que compõem de forma independente. A adição de restrições quebra a ortogonalidade, a composição, a independência e, ao fazê-lo, quebra a simplicidade.

Hoje, é uma regra que se você tiver:

x := expression
y := f(x)

sem nenhum outro uso de x em qualquer lugar, então é uma transformação de programa válida para simplificar isso para

y := f(expression)

Se adotássemos uma restrição nas expressões try, isso quebraria qualquer ferramenta que assumisse que essa era sempre uma transformação válida. Ou se você tivesse um gerador de código que trabalhasse com expressões e pudesse processar expressões try, ele teria que se esforçar para introduzir temporários para satisfazer as restrições. E assim por diante e assim por diante.

Em suma, as restrições adicionam uma complexidade significativa. Eles precisam de uma justificativa significativa, não "vamos ver se alguém esbarra nessa parede e nos pede para derrubá-la".

Eu escrevi uma explicação mais longa há dois anos em https://github.com/golang/go/issues/18130#issuecomment -264195616 (no contexto de aliases de tipo) que se aplica igualmente bem aqui.

@bakul ,

Mas deixe-me esclarecer um ponto. Na proposta do bloco try, permiti explicitamente declarações que _não precisam_ try .

Fazer isso ficaria aquém do segundo objetivo : "Tanto as verificações de erros quanto o tratamento de erros devem permanecer explícitos, ou seja, visíveis no texto do programa. Não queremos repetir as armadilhas do tratamento de exceções."

A principal armadilha do tratamento tradicional de exceções é não saber onde estão as verificações. Considerar:

try {
    s = canThrowErrors()
    t = cannotThrowErrors()
    u = canThrowErrors() // a second call
} catch {
    // how many ways can you get here?
}

Se as funções não tiverem um nome tão útil, pode ser muito difícil dizer quais funções podem falhar e quais têm garantia de sucesso, o que significa que você não pode raciocinar facilmente sobre quais fragmentos de código podem ser interrompidos por uma exceção e quais não podem.

Compare isso com a abordagem do Swift , onde eles adotam algumas das sintaxes tradicionais de tratamento de exceções, mas na verdade estão fazendo tratamento de erros, com um marcador explícito em cada função verificada e sem como desenrolar além do quadro de pilha atual:

do {
    let s = try canThrowErrors()
    let t = cannotThrowErrors()
    let u = try canThrowErrors() // a second call
} catch {
    handle error from try above
}

Seja Rust ou Swift ou esta proposta, a melhoria chave e crítica sobre o tratamento de exceção é marcar explicitamente no texto - mesmo com um marcador muito leve - cada local onde está uma verificação.

Para obter mais informações sobre o problema de verificações implícitas, consulte a seção Problema da visão geral do problema de agosto passado, em particular os links para os dois artigos de Raymond Chen.

Edit: veja também o comentário de @velovix , três acima, que veio enquanto eu estava trabalhando neste.

@daved , fico feliz que a analogia do "relé de proteção" funcione para você. Não funciona para mim. Programas não são circuitos.

Qualquer palavra pode ser mal interpretada:
"break" não interrompe seu programa.
"continue" não continua a execução na próxima instrução normalmente.
"goto" ... bem, é impossível entender mal, na verdade. :-)

https://www.google.com/search?q=define+try diz "faça uma tentativa ou esforço para fazer algo" e "sujeito a julgamento". Ambos se aplicam a "f := try(os.Open(file))". Ele tenta fazer o os.Open (ou submete o resultado do erro à tentativa), e se a tentativa (ou o resultado do erro) falhar, ele retorna da função.

Usamos cheque em agosto passado. Essa foi uma boa palavra também. Mudamos para tentar, apesar da bagagem histórica de C++/Java/Python, porque o significado atual de try nesta proposta coincide com o significado no try de Swift (sem o do-catch circundante) e no try original de Rust! . Não será terrível se decidirmos mais tarde que cheque é a palavra certa, mas por enquanto devemos nos concentrar em outras coisas além do nome.

Aqui está um interessante falso negativo tryhard , de github.com/josharian/pct . Menciono aqui porque:

  • mostra uma maneira pela qual a detecção automatizada try é complicada
  • ilustra que o custo visual de if err != nil impacta como as pessoas (pelo menos eu) estruturam seu código, e que try pode ajudar com isso

Antes de:

var err error
switch {
case *flagCumulative:
    _, err = fmt.Fprintf(w, "% 6.2f%% % 6.2f%%% 6d %s\n", p, f*float64(runtot), line.n, line.s)
case *flagQuiet:
    _, err = fmt.Fprintln(w, line.s)
default:
    _, err = fmt.Fprintf(w, "% 6.2f%%% 6d %s\n", p, line.n, line.s)
}
if err != nil {
    return err
}

Depois (reescrita manual):

switch {
case *flagCumulative:
    try(fmt.Fprintf(w, "% 6.2f%% % 6.2f%%% 6d %s\n", p, f*float64(runtot), line.n, line.s))
case *flagQuiet:
    try(fmt.Fprintln(w, line.s))
default:
    try(fmt.Fprintf(w, "% 6.2f%%% 6d %s\n", p, line.n, line.s))
}

Alterar https://golang.org/cl/182717 menciona este problema: src: apply tryhard -r $GOROOT/src

Para uma ideia visual de try na biblioteca std, vá para CL 182717 .

Obrigado, @josharian , por isso . Sim, pode ser impossível até mesmo para uma boa ferramenta detectar todos os possíveis candidatos de uso para try . Mas felizmente esse não é o objetivo principal (desta proposta). Ter uma ferramenta é útil, mas vejo o principal benefício de try em código que ainda não foi escrito (porque haverá muito mais disso do que código que já temos).

"break" não interrompe seu programa.
"continue" não continua a execução na próxima instrução normalmente.
"goto" ... bem, é impossível entender mal, na verdade. :-)

break quebra o loop. continue continua o loop e goto vai para o destino indicado. Em última análise, eu ouço você, mas considere o que acontece quando uma função é concluída e retorna um erro, mas não reverte. Não foi uma tentativa/ensaio. Eu acho que check é muito superior a esse respeito (para "interromper o progresso de" por meio de "exame" é certamente adequado).

Mais pertinente, estou curioso sobre a forma de try/check que ofereci em oposição às outras sintaxes.
try {error} {optional wrap func} {optional return args in brackets}

f, err := os.Open(file)
try err wrap { a, b }

A biblioteca padrão acaba não sendo representativa do código Go "real", pois não gasta muito tempo coordenando ou conectando outros pacotes. Notamos isso no passado como a razão pela qual há tão pouco uso de canal na biblioteca padrão em comparação com pacotes mais acima na cadeia alimentar de dependência. Suspeito que o tratamento e propagação de erros acabe sendo semelhante aos canais a esse respeito: você encontrará mais quanto mais alto você for.

Por essa razão, seria interessante alguém executar tryhard em algumas bases de código de aplicativos maiores e ver que coisas divertidas podem ser descobertas nesse contexto. (A biblioteca padrão também é interessante, mas mais como um microcosmo do que uma amostra precisa do mundo.)

Estou curioso sobre a forma de try/check que ofereci em oposição às outras sintaxes.

Acho que esse formulário acaba recriando as estruturas de controle existentes .

@networkimprov , re https://github.com/golang/go/issues/32437#issuecomment -502879351

Sinceramente, estou chocado que a equipe Go nos ofereceu primeiro check/handle (caritativamente, uma ideia nova) e depois o try (ternário) . Não vejo por que você não emitiu uma RFP referente ao tratamento de erros e, em seguida, coletou comentários da comunidade sobre algumas das propostas resultantes (consulte #29860). Há muita sabedoria aqui que você pode aproveitar!

Como discutimos em #29860, honestamente não vejo muita diferença entre o que você está sugerindo que deveríamos ter feito no que diz respeito a solicitar feedback da comunidade e o que realmente fizemos. A página de rascunhos de projetos diz explicitamente que eles são "pontos de partida para discussão, com o objetivo final de produzir projetos bons o suficiente para serem transformados em propostas reais". E as pessoas escreveram muitas coisas, desde feedback curto até propostas alternativas completas. E a maior parte foi útil e agradeço sua ajuda em particular na organização e resumo. Você parece estar obcecado em chamá-lo de um nome diferente ou introduzir camadas adicionais de burocracia, o que, conforme discutimos sobre esse assunto, não vemos necessidade.

Mas, por favor, não diga que de alguma forma não solicitamos conselhos da comunidade ou os ignoramos. Isso simplesmente não é verdade.

Eu também não consigo ver como tentar é de alguma forma "ternaryesque", o que quer que isso signifique.

Concordo, acho que esse era o meu objetivo; Não acho que mecanismos mais complexos valham a pena. Se eu estivesse no seu lugar, o máximo que eu ofereceria é um pouco de açúcar sintático para silenciar a maioria das reclamações e nada mais.

@rsc , desculpas por desviar do assunto!
Eu criei manipuladores de nível de pacote em https://github.com/golang/go/issues/32437#issuecomment -502840914
e respondeu ao seu pedido de esclarecimento em https://github.com/golang/go/issues/32437#issuecomment -502879351

Eu vejo os manipuladores de nível de pacote como um recurso que praticamente todos podem apoiar.

use a sintaxe try {} catch{}, não construa mais rodas

use a sintaxe try {} catch{}, não construa mais rodas

eu acho que é apropriado construir rodas melhores quando as rodas que outras pessoas usam têm a forma de quadrados

@jimwei

O tratamento de erros baseado em exceção pode ser uma roda pré-existente, mas também tem alguns problemas conhecidos. A declaração do problema no projeto de rascunho original faz um ótimo trabalho ao delinear essas questões.

Para adicionar meus próprios comentários menos bem pensados, acho interessante que muitas linguagens novas de muito sucesso (ou seja, Swift, Rust e Go) não adotaram exceções. Isso me diz que a comunidade de software mais ampla está repensando as exceções depois de muitos anos que tivemos que trabalhar com eles.

Em resposta a https://github.com/golang/go/issues/32437#issuecomment -502837008 (o comentário de @rsc sobre try como uma declaração)

Você levanta um bom ponto. Desculpe por ter perdido esse comentário antes de fazer este: https://github.com/golang/go/issues/32437#issuecomment -502871889

Seus exemplos com try como uma expressão parecem muito melhores do que aqueles com try como uma declaração. O fato de que a declaração começa com try de fato torna muito mais difícil de ler. No entanto, ainda estou preocupado que as pessoas aninham chamadas de tentativa juntas para fazer código ruim, já que try como uma expressão realmente _incentiva_ esse comportamento aos meus olhos.

Acho que apreciaria um pouco mais essa proposta se golint proibisse chamadas try aninhadas. Eu acho que proibir todas as chamadas try dentro de outras expressões é um pouco estrito demais, ter try como uma expressão tem seus méritos.

Tomando emprestado seu exemplo, até mesmo o aninhamento de 2 tentativas juntas parece bastante hediondo, e posso ver os programadores Go fazendo isso, especialmente se eles trabalham sem revisores de código.

parentCommitOne := try(remoteBranch.Reference.Peel(git.ObjectCommit)).AsCommit()
parentCommitTwo := try(try(r.Head()).Peel(git.ObjectCommit)).AsCommit()
tree := try(r.LookupTree(try(index.WriteTree())))

O exemplo original realmente parecia muito bom, mas este mostra que aninhar as expressões try (mesmo apenas 2-deep) realmente prejudica a legibilidade do código drasticamente. Negar chamadas try aninhadas também ajudaria com o problema de "depuração", pois é muito mais fácil expandir um try para um if se estiver fora de uma expressão.

Novamente, eu quase gostaria de dizer que um try dentro de uma subexpressão deve ser sinalizado por golint , mas acho que isso pode ser um pouco rigoroso demais. Também sinalizaria código como este, que aos meus olhos é bom:

x := 5 + try(strconv.Atoi(input))

Dessa forma, obtemos os benefícios de ter try como uma expressão, mas não estamos promovendo a adição de muita complexidade ao eixo horizontal.

Talvez outra solução seria que golint só deveria permitir no máximo 1 try por declaração, mas é tarde, estou ficando cansado e preciso pensar nisso de forma mais racional. De qualquer forma, tenho sido bastante negativo em relação a essa proposta em alguns pontos, mas acho que posso realmente gostar dela, desde que haja alguns padrões golint relacionados a ela.

@rsc

Podemos e devemos distinguir entre _"este recurso pode ser usado para escrever código muito legível, mas também pode ser abusado para escrever código ilegível"_ e "o uso dominante desse recurso será escrever código ilegível".
A experiência com C sugere que ? : cai diretamente na segunda categoria. (Com a possível exceção de min e max,

O que primeiro me impressionou sobre try() - vs try como uma declaração - foi o quão semelhante era em capacidade de encaixe ao operador ternário e, no entanto, quão opostos eram os argumentos para try() e contra o ternário foram _(parafraseados):_

  • ternary: _"Se permitirmos, as pessoas irão aninhar e o resultado será muito código ruim"_ ignorando que algumas pessoas escrevem código melhor com eles, vs.
  • try(): _"Você pode aninhá-lo, mas duvidamos que muitos o façam porque a maioria das pessoas quer escrever um bom código"_,

Respeitosamente, esse racional para a diferença entre os dois parece tão subjetivo que eu pediria um pouco de introspecção e pelo menos consideraria se você poderia estar racionalizando uma diferença para um recurso que você prefere versus um recurso que você não gosta? #please_dont_shoot_the_messenger

_"Não tenho certeza se já vi código usando ? : isso não foi melhorado reescrevendo-o para usar uma instrução if. Mas este parágrafo está saindo do tópico.)"_

Em outras linguagens eu frequentemente melhoro as declarações reescrevendo-as de um if para um operador ternário, por exemplo, do código que escrevi hoje em PHP:

return isset( $_COOKIE[ CookieNames::CART_ID ] )
    ? intval( $_COOKIE[ CookieNames::CART_ID ] )
    : null;

Comparado a:

if ( isset( $_COOKIE[ CookieNames::CART_ID ] ) ) {
    return intval( $_COOKIE[ CookieNames::CART_ID ] );
} else { 
    return null;
}

No que me diz respeito, o primeiro é muito melhor do que o segundo.

fwiw

Acho que as críticas a esta proposta se devem em grande parte às grandes expectativas que foram levantadas pela proposta anterior, que teria sido muito mais abrangente. No entanto, acho que essas altas expectativas foram justificadas por razões de consistência. Acho que o que muitas pessoas gostariam de ver é uma construção única e abrangente para tratamento de erros que é útil em todos os casos de uso.

Compare esse recurso, por exemplo, com a função append() embutida. Anexar foi criado porque anexar a fatia era um caso de uso muito comum e, embora fosse possível fazê-lo manualmente, também era fácil fazê-lo errado. Agora append() permite anexar não apenas um, mas muitos elementos, ou mesmo uma fatia inteira, e ainda permite anexar uma string a uma fatia []byte. É poderoso o suficiente para cobrir todos os casos de uso de anexação a uma fatia. E, portanto, ninguém mais acrescenta fatias manualmente.

No entanto, try() é diferente. Não é poderoso o suficiente para que possamos usá-lo em todos os casos de tratamento de erros. E acho que essa é a falha mais grave dessa proposta. A função interna try() só é realmente útil, no sentido de que reduz o clichê, nos casos mais simples, ou seja, apenas passando um erro para o chamador, e com uma instrução defer, se todos os erros do função precisa ser tratada da mesma maneira.

Para tratamento de erros mais complexo, ainda precisaremos usar if err != nil {} . Isso leva a dois estilos distintos de tratamento de erros, onde antes havia apenas um. Se esta proposta é tudo o que temos para ajudar no tratamento de erros em Go, então, acho que seria melhor não fazer nada e continuar lidando com o tratamento de erros com if como sempre fizemos, porque pelo menos isso é consistente e teve o benefício de "só há uma maneira de fazer isso".

@rsc , desculpas por desviar do assunto!
Eu levantei manipuladores de nível de pacote em #32437 (comentário)
e respondeu ao seu pedido de esclarecimento em #32437 (comentário)

Eu vejo os manipuladores de nível de pacote como um recurso que praticamente todos podem apoiar.

Não vejo o que une o conceito de um pacote com tratamento de erros específico. É difícil imaginar o conceito de um manipulador de nível de pacote sendo útil para, digamos, net/http . De maneira semelhante, apesar de escrever pacotes menores do que net/http em geral, não consigo pensar em um único caso de uso em que eu teria preferido uma construção em nível de pacote para lidar com erros. Em geral, descobri que a suposição de que todos compartilham suas experiências, casos de uso e opiniões é perigosa :)

@beoran acredito que esta proposta possibilita melhorias adicionais. Como um decorador no último argumento de try(..., func(err) error) , ou um tryf(..., "context of my error: %w") ?

@flibustenet Embora essas extensões posteriores possam ser possíveis, a proposta como está agora parece desencorajar essas extensões, principalmente porque adicionar um manipulador de erros seria redundante com defer.

Eu acho que o problema difícil é como ter um tratamento abrangente de erros sem duplicar a funcionalidade do defe. Talvez a própria instrução defer possa ser aprimorada de alguma forma para permitir um tratamento de erros mais fácil em casos mais complexos... Mas isso é um problema diferente.

https://github.com/golang/go/issues/32437#issuecomment -502975437

Isso leva a dois estilos distintos de tratamento de erros, onde antes havia apenas um. Se esta proposta é tudo que temos para ajudar no tratamento de erros em Go, então, acho que seria melhor não fazer nada e continuar lidando com o tratamento de erros com if como sempre fizemos, porque pelo menos isso é consistente e teve o benefício de "só há uma maneira de fazer isso".

@beoran Concordo. É por isso que sugeri que unifiquemos a grande maioria dos casos de erro sob a palavra-chave try ( try e try / else ). Mesmo que a sintaxe try / else não nos dê nenhuma redução significativa no comprimento do código em relação ao estilo if err != nil existente, ela nos dá consistência com o try (sem else ) caso. Esses dois casos (try e try-else) provavelmente cobrirão a grande maioria dos casos de tratamento de erros. Eu coloquei isso em oposição à versão interna do try que só se aplica nos casos em que o programador não está realmente fazendo nada para lidar com o erro além de retornar (o que, como outros mencionaram neste tópico, não é necessariamente algo que realmente queremos encorajar em primeiro lugar).

A consistência é importante para a legibilidade.

append é a maneira definitiva de adicionar elementos a uma fatia. make é a maneira definitiva de construir um novo canal ou mapa ou fatia (com exceção de literais, com os quais não estou entusiasmado). Mas try() (como builtin, e sem else ) seria espalhado pelas bases de código, dependendo de como o programador precisa lidar com um determinado erro, de uma maneira que provavelmente é um pouco caótica e confusa para o leitor. Não parece estar no espírito dos outros builtins (ou seja, lidar com um caso que é bastante difícil ou totalmente impossível de fazer de outra forma). Se esta for a versão de try que for bem-sucedida, consistência e legibilidade me obrigarão a não usá-la, assim como tento evitar literais map/slice (e evitar new como a praga).

Se a ideia é mudar a forma como os erros são tratados, parece sensato tentar unificar a abordagem no maior número possível de casos, em vez de adicionar algo que, na melhor das hipóteses, será "pegar ou largar". Temo que o último realmente adicione ruído em vez de reduzi-lo.

@deanveloper escreveu:

Acho que apreciaria um pouco mais essa proposta se o golint proibisse as chamadas de tentativa aninhadas.

Concordo que try profundamente aninhado pode ser difícil de ler. Mas isso também é verdade para chamadas de função padrão, não apenas para a função interna try . Assim, não vejo por que golint deveria proibir isso.

@brynbellomy escreveu:

Mesmo que a sintaxe try/else não nos dê nenhuma redução significativa no comprimento do código em relação ao estilo if err != nil existente, ela nos dá consistência com o caso try (nenhum outro).

O objetivo único da função interna try é reduzir o clichê, então é difícil ver por que devemos adotar a sintaxe try/else que você propõe quando reconhece que "não nos dá nenhuma redução significativa no comprimento do código".

Você também menciona que a sintaxe que você propõe torna o caso try consistente com o caso try/else. Mas também cria uma maneira inconsistente de ramificar, quando já temos if/else. Você ganha um pouco de consistência em um caso de uso específico, mas perde muita inconsistência no resto.

Sinto a necessidade de expressar minhas opiniões pelo que elas valem. Embora nem tudo isso seja de natureza acadêmica e técnica, acho que precisa ser dito.

Acredito que essa mudança seja um desses casos em que a engenharia está sendo feita por engenharia e o "progresso" está sendo usado como justificativa. O tratamento de erros em Go não está quebrado e esta proposta viola muito a filosofia de design que eu amo em Go.

Torne as coisas fáceis de entender, não fáceis de fazer
Esta proposta está escolhendo otimizar por preguiça em vez de correção. O foco está em tornar o tratamento de erros mais fácil e, em troca, uma enorme quantidade de legibilidade está sendo perdida. A natureza tediosa ocasional do tratamento de erros é aceitável devido aos ganhos de legibilidade e depuração.

Evite nomear argumentos de retorno
Existem alguns casos extremos com instruções defer em que nomear o argumento de retorno é válido. Fora destes, deve ser evitado. Esta proposta promove o uso de argumentos de retorno de nomenclatura. Isso não vai ajudar a tornar o código Go mais legível.

O encapsulamento deve criar uma nova semântica onde é absolutamente preciso
Não há precisão nesta nova sintaxe. Ocultar a variável de erro e o retorno não ajuda a tornar as coisas mais fáceis de entender. Na verdade, a sintaxe parece muito estranha de qualquer coisa que fazemos em Go hoje. Se alguém escrevesse uma função semelhante, acredito que a comunidade concordaria que a abstração está escondendo o custo e não vale a simplicidade que está tentando fornecer.

Quem estamos tentando ajudar?
Estou preocupado que essa mudança esteja sendo implementada em uma tentativa de atrair desenvolvedores corporativos para longe de seus idiomas atuais e para Go. Implementar mudanças de linguagem, apenas para aumentar os números, estabelece um mau precedente. Eu acho que é justo fazer essa pergunta e obter uma resposta para o problema de negócio que está tentando ser resolvido e o ganho esperado que está tentando ser alcançado?

Eu já vi isso antes várias vezes agora. Parece bastante claro, com toda a atividade recente da equipe de idiomas, esta proposta está basicamente definida. Há mais defesa da implementação do que um debate real sobre a implementação em si. Tudo isso começou há 13 dias. Veremos o impacto que essa mudança tem no idioma, na comunidade e no futuro do Go.

O tratamento de erros em Go não está quebrado e esta proposta viola muito a filosofia de design que eu amo em Go.

Bill expressa meus pensamentos perfeitamente.

Eu não posso impedir que try seja apresentado, mas se for, eu não o usarei; Não vou ensiná-lo e não vou aceitá-lo nas relações públicas que reviso. Ele será simplesmente adicionado à lista de outras 'coisas em Go que eu nunca uso' (veja a conversa divertida de Mat Ryer no YouTube para mais informações).

@ardan-bkennedy, obrigado por seus comentários.

Você perguntou sobre o "problema de negócios que está tentando ser resolvido". Eu não acredito que estamos visando os problemas de qualquer negócio em particular, exceto talvez "Go Programming". Mas, de forma mais geral, articulamos o problema que estamos tentando resolver em agosto passado no início da discussão do projeto do Gophercon (consulte a Visão geral do problema , especialmente a seção Metas). O fato de essa conversa estar acontecendo desde agosto passado também contradiz categoricamente sua afirmação de que "tudo isso começou há 13 dias".

Você não é a única pessoa a sugerir que isso não é um problema ou que não vale a pena resolver. Consulte https://swtch.com/try.html#nonissue para outros comentários desse tipo. Nós anotamos isso e queremos ter certeza de que estamos resolvendo um problema real. Parte da maneira de descobrir é avaliar a proposta em bases de código reais. Ferramentas como o tryhard de Robert nos ajudam a fazer isso. Eu pedi anteriormente para as pessoas nos informarem o que encontraram em suas próprias bases de código. Essa informação será criticamente importante para avaliar se a mudança vale a pena ou não. Você tem um palpite e eu tenho outro, e tudo bem. A resposta é substituir os dados por essas suposições.

Faremos o que for necessário para garantir que estamos resolvendo um problema real.

Novamente, o caminho a seguir são dados experimentais, não reações intestinais. Infelizmente, os dados exigem mais esforço para serem coletados. Neste ponto, eu encorajaria as pessoas que querem ajudar a sair e coletar dados.

@ardan-bkennedy, desculpe pelo segundo acompanhamento, mas em relação a:

Estou preocupado que essa mudança esteja sendo implementada em uma tentativa de atrair desenvolvedores corporativos para longe de seus idiomas atuais e para Go. Implementar mudanças de linguagem, apenas para aumentar os números, estabelece um mau precedente.

Há dois problemas sérios com esta linha que não consigo passar.

Em primeiro lugar, rejeito a afirmação implícita de que existem classes de desenvolvedores - neste caso "desenvolvedores empresariais" - que de alguma forma não são dignos de usar Go ou ter seus problemas considerados. No caso específico de "empresa", estamos vendo muitos exemplos de pequenas e grandes empresas usando Go de forma muito eficaz.

Em segundo lugar, desde o início do projeto Go, nós – Robert, Rob, Ken, Ian e eu – avaliamos mudanças e recursos de linguagem com base em nossa experiência coletiva na construção de muitos sistemas. Perguntamos "isso funcionaria bem nos programas que escrevemos?" Essa tem sido uma receita de sucesso com ampla aplicabilidade e é a que pretendemos continuar usando, novamente ampliada pelos dados que solicitei no comentário anterior e relatos de experiência em geral. Nós não sugerimos ou apoiamos uma mudança de idioma que não podemos nos ver usando em nossos próprios programas ou que achamos que não se encaixa bem no Go. E certamente não sugeriríamos ou apoiaríamos uma mudança ruim apenas para ter mais programadores Go. Nós usamos Go também, afinal.

@rsc
Não faltarão locais onde essa comodidade possa ser colocada. Que métrica está sendo buscada para provar a substância do mecanismo além disso? Existe uma lista de casos de tratamento de erros classificados? Como o valor será derivado dos dados quando grande parte do processo público é impulsionado pelo sentimento?

As ferramentas tryhard são muito informativas!
Pude ver que uso frequentemente return ...,err , mas somente quando sei que chamo uma função que já envolve o erro (com pkg/errors ), principalmente em manipuladores http. Eu ganho em legibilidade com menos linha de código.
Então, neste manipulador http, eu adicionaria um defer fmt.HandleErrorf(&err, "handler xyz") e, finalmente, adicionaria mais contexto do que antes.

Também vejo muitos casos em que não me importo com o erro fmt.Printf e farei isso com try .
Será possível, por exemplo, fazer defer try(f.Close()) ?

Então, talvez try finalmente ajude a adicionar contexto e impulsionar as melhores práticas, e não o contrário.

Estou muito impaciente para testar ao vivo!

@flibustenet A proposta como está não permitirá defer try(f()) (veja a justificativa ). Há todos os tipos de problemas com isso.

Ao usar esta ferramenta tryhard para ver as alterações em uma base de código, poderíamos também comparar a proporção de if err != nil antes e depois para ver se é mais comum adicionar contexto ou apenas passar o erro de volta?

Meu pensamento é que talvez um grande projeto hipotético possa ver 1000 lugares onde try() foi adicionado, mas existem 10000 if err != nil que adicionam contexto, então mesmo que 1000 pareçam enormes, são apenas 10% da coisa completa .

@Goodwine Sim. Provavelmente não conseguirei fazer essa alteração esta semana, mas o código é bastante direto e autocontido. Sinta-se à vontade para experimentá-lo (sem trocadilhos), clonar e ajustar conforme necessário.

defer try(f()) não seria equivalente a

defer func() error {
    if err:= f(); err != nil { return err }
    return nil
}()

Esta (a versão if) não é atualmente desabilitada, certo? Parece-me que você não deve abrir uma exceção aqui - pode gerar um aviso? E não está claro se o código de adiamento acima está necessariamente errado. E se close(file) falhar em uma instrução defer ? Devemos relatar esse erro ou não?

Eu li o raciocínio que parece falar sobre defer try(f) não defer try(f()) . Pode ser um erro de digitação?

Um argumento semelhante pode ser feito para go try(f()) , que se traduz em

go func() error {
    if err:= f(); err != nil { return err }
    return nil
}()

Aqui try não faz nada útil, mas é inofensivo.

@ardan-bkennedy Obrigado por seus pensamentos. Com todo o respeito, acredito que você deturpou a intenção desta proposta e fez várias alegações infundadas .

Em relação a alguns dos pontos que o @rsc não abordou anteriormente:

  • Nós nunca dissemos que o tratamento de erros está quebrado. O design é baseado na observação (pela comunidade Go!) de que o manuseio atual é bom, mas detalhado em muitos casos - isso é indiscutível. Essa é uma premissa importante da proposta.

  • Tornar as coisas mais fáceis de fazer também pode torná-las mais fáceis de entender - esses dois não se excluem mutuamente, nem mesmo implicam um ao outro. Exorto-vos a olhar para este código para um exemplo. Usar try remove uma quantidade significativa de clichê, e esse clichê não adiciona praticamente nada à compreensão do código. A fatoração de código repetitivo é uma prática de codificação padrão e amplamente aceita para melhorar a qualidade do código.

  • Sobre "essa proposta viola muito a filosofia do design": O importante é que não sejamos dogmáticos sobre a "filosofia do design" - que muitas vezes é a queda de boas ideias (além disso, acho que sabemos uma coisa ou duas sobre filosofia de design da Go). Há muito "fervor religioso" (por falta de um termo melhor) em torno de parâmetros de resultado nomeados versus não nomeados. Mantras como "você nunca deve usar parâmetros de resultado nomeados" fora de contexto não têm sentido. Eles podem servir como diretrizes gerais, mas não como verdades absolutas. Os parâmetros de resultado nomeados não são inerentemente "ruins". Parâmetros de resultado bem nomeados podem ser adicionados à documentação de uma API de maneiras significativas. Em suma, não vamos usar slogans para tomar decisões de design de linguagem.

  • É um ponto desta proposta não introduzir nova sintaxe. Apenas propõe uma nova função. Não podemos escrever essa função na linguagem, então um built-in é o lugar natural para ela em Go. Não só é uma função simples, como também é definida com muita precisão. Escolhemos essa abordagem mínima em vez de soluções mais abrangentes exatamente porque ela faz uma coisa muito bem e não deixa quase nada para decisões arbitrárias de design. Também não estamos muito fora do caminho já que outras linguagens (por exemplo, Rust) têm construções muito semelhantes. Sugerir que "a comunidade concordaria que a abstração está escondendo o custo e não vale a simplicidade que está tentando fornecer" é colocar palavras na boca de outras pessoas. Embora possamos ouvir claramente os oponentes vocais dessa proposta, há uma porcentagem significativa (estimada em 40%) de pessoas que expressaram aprovação para avançar com o experimento. Não vamos privá-los com hipérboles.

Obrigado.

return isset( $_COOKIE[ CookieNames::CART_ID ] )
    ? intval( $_COOKIE[ CookieNames::CART_ID ] )
    : null;

Tenho certeza que isso deve ser return intval( $_COOKIE[ CookieNames::CART_ID ] ) ?? null; FWIW. 😁

@bakul porque os argumentos são avaliados imediatamente, na verdade é aproximadamente equivalente a:

<result list> := f()
defer try(<result list>)

Isso pode ser um comportamento inesperado para alguns, pois o f() não é adiado para mais tarde, ele é executado imediatamente. A mesma coisa se aplica a go try(f()) .

@bakul O documento menciona defer try(f) (em vez de defer try(f()) porque try em geral se aplica a qualquer expressão, não apenas a uma chamada de função (você pode dizer try(err) para exemplo, se err for do tipo error ). Portanto, não é um erro de digitação, mas talvez confuso no início. f simplesmente representa uma expressão, que geralmente é uma função chamar.

@deanveloper , @griesemer Não importa :-) Obrigado.

@carl-mastrangelo

_"Com certeza isso deve ser return intval( $_COOKIE[ CookieNames::CART_ID ] ) ?? null; _

Você está assumindo o PHP 7.x. Eu não estava. Mas, novamente, dado o seu rosto sarcástico, você sabe que não era o ponto. :piscadela:

Estou preparando uma pequena demonstração para exibir esta discussão durante um encontro go que acontecerá amanhã, e ouvir algumas novas ideias, pois acredito que a maioria dos participantes deste tópico (colaboradores ou observadores) são aqueles que estão envolvidos mais profundamente no idioma e provavelmente "não é o desenvolvedor médio" (apenas um palpite).

Enquanto fazia isso, lembrei que na verdade tivemos um encontro sobre erros e uma discussão sobre dois padrões:

  1. Estenda a estrutura de erro enquanto suporta a interface de erro mystruct.Error()
  2. Incorpore o erro como um campo ou campo anônimo da estrutura
type ExtErr struct{
  error
  someOtherField string
}  

Eles são usados ​​em algumas pilhas que minhas equipes realmente construíram.

A proposta de perguntas e respostas afirma
P: O último argumento passado para try deve ser do tipo error. Por que não é suficiente que o argumento de entrada seja atribuível ao erro?
R: "... Podemos rever esta decisão no futuro, se necessário"

Alguém pode comentar de casos de uso semelhantes para que possamos entender se essa necessidade é comum para ambas as opções de extensão de erro acima?

@mikeschinkel Eu não sou o Carl que você está procurando.

@daved , re:

Não faltarão locais onde essa comodidade possa ser colocada. Que métrica está sendo buscada para provar a substância do mecanismo além disso? Existe uma lista de casos de tratamento de erros classificados? Como o valor será derivado dos dados quando grande parte do processo público é impulsionado pelo sentimento?

A decisão é baseada em quão bem isso funciona em programas reais. Se as pessoas nos mostrarem que try é ineficaz na maior parte de seu código, isso é um dado importante. O processo é conduzido por esse tipo de dados. É _não_ impulsionado pelo sentimento.

Contexto de erro

A preocupação semântica mais importante que foi levantada nesta edição é se try irá encorajar melhor ou pior anotação de erros com contexto.

A Visão geral do problema de agosto passado fornece uma sequência de exemplos de implementações de CopyFile nas seções Problema e Objetivos. É um objetivo explícito, tanto naquela época quanto hoje, que qualquer solução torne _mais provável_ que os usuários adicionem contexto apropriado aos erros. E achamos que tentar pode fazer isso, ou não teríamos proposto.

Mas antes de tentarmos, vale a pena ter certeza de que estamos todos na mesma página sobre o contexto de erro apropriado. O exemplo canônico é os.Open. Citando a postagem do blog Go “ Error handling and Go ”:

É responsabilidade da implementação do erro resumir o contexto.
O erro retornado por os.Open formata como "abrir /etc/passwd: permissão negada", não apenas "permissão negada".

Veja também a seção do Effective Go sobre Erros .

Observe que essa convenção pode diferir de outras linguagens com as quais você está familiarizado e também é seguida de forma inconsistente no código Go. Um objetivo explícito de tentar simplificar o tratamento de erros é tornar mais fácil para as pessoas seguirem essa convenção e adicionar contexto apropriado e, assim, torná-la seguida de forma mais consistente.

Há muito código seguindo a convenção Go hoje, mas também há muito código assumindo a convenção oposta. É muito comum ver código como:

f, err := os.Open(file)
if err != nil {
    log.Fatalf("opening %s: %v", file, err)
}

que obviamente imprime a mesma coisa duas vezes (muitos exemplos nesta discussão são assim). Parte desse esforço terá que ser garantir que todos conheçam e sigam a convenção.

No código que segue a convenção de contexto de erro Go, esperamos que a maioria das funções adicione corretamente o mesmo contexto a cada retorno de erro, para que uma decoração se aplique em geral. Por exemplo, no exemplo CopyFile, o que precisa ser adicionado em cada caso são detalhes sobre o que estava sendo copiado. Outros retornos específicos podem adicionar mais contexto, mas normalmente em adição e não em substituição. Se estivermos errados sobre essa expectativa, seria bom saber. Evidências claras de bases de código reais ajudariam.

O projeto de rascunho de verificação/manipulação do Gophercon teria usado código como:

func CopyFile(src, dst string) error {
    handle err {
        return fmt.Errorf("copy %s %s: %v", src, dst, err)
    }

    r := check os.Open(src)
    defer r.Close()

    w := check os.Create(dst)
    ...
}

Esta proposta revisou isso, mas a ideia é a mesma:

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))
    ...
}

e queremos adicionar um auxiliar ainda sem nome para este padrão comum:

func CopyFile(src, dst string) (err error) {
    defer HelperToBeNamedLater(&err, "copy %s %s", src, dst)

    r := try(os.Open(src))
    defer r.Close()

    w := try(os.Create(dst))
    ...
}

Em suma, a razoabilidade e o sucesso dessa abordagem dependem dessas suposições e etapas lógicas:

  1. As pessoas devem seguir a convenção Go declarada “o chamado adiciona contexto relevante que conhece”.
  2. Portanto, a maioria das funções só precisa adicionar contexto de nível de função descrevendo o
    operação, não a sub-peça específica que falhou (essa sub-peça já relatou).
  3. Muito código Go hoje não adiciona o contexto de nível de função porque é muito repetitivo.
  4. Fornecer uma maneira de escrever o contexto de nível de função uma vez tornará mais provável que
    desenvolvedores fazem isso.
  5. O resultado final será mais código Go seguindo a convenção e adicionando o contexto apropriado.

Se houver uma suposição ou passo lógico que você acha que é falso, queremos saber. E a melhor maneira de nos dizer é apontar evidências em bases de código reais. Mostre-nos padrões comuns que você tem em que tentar é inadequado ou piora as coisas. Mostre-nos exemplos de coisas em que tentar foi mais eficaz do que você esperava. Tente quantificar quanto de sua base de código cai de um lado ou de outro. E assim por diante. Os dados importam.

Obrigado.

Obrigado @rsc pelas informações adicionais sobre as melhores práticas de contexto de erro. Este ponto sobre as melhores práticas em particular me aludiu, mas melhora significativamente a relação try s com o contexto de erro.

Portanto, a maioria das funções só precisa adicionar contexto de nível de função descrevendo o
operação, não a sub-peça específica que falhou (essa sub-peça já relatou).

Então o lugar onde try não ajuda é quando precisamos reagir aos erros, não apenas contextualizá-los.

Para adaptar um exemplo do Cleaner, mais elegante e errado , aqui está o exemplo de uma função que está sutilmente errada em seu tratamento de erros. Eu o adaptei para Go usando try e defer -style error wraping:

func AddNewGuy(name string) (guy Guy, err error) {
    defer func() {
        if err != nil {
            err = fmt.Errorf("adding guy %v: %v", name, err)
        }
    }()

    guy = Guy{name: name}
    guy.Team = ChooseRandomTeam()
    try(guy.Team.Add(guy))
    try(AddToLeague(guy))
    return guy, nil
}

Esta função está incorreta porque se guy.Team.Add(guy) for bem-sucedido, mas AddToLeague(guy) falhar, o time terá um objeto Guy inválido que não está em uma liga. O código correto ficaria assim, onde revertemos guy.Team.Add(guy) e não podemos mais usar try :

func AddNewGuy(name string) (guy Guy, err error) {
    defer func() {
        if err != nil {
            err = fmt.Errorf("adding guy %v: %v", name, err)
        }
    }()

    guy = Guy{name: name}
    guy.Team = ChooseRandomTeam()
    try(guy.Team.Add(guy))
    if err := AddToLeague(guy); err != nil {
        guy.Team.Remove(guy)
        return Guy{}, err
    }
    return guy, nil
}

Ou, se quisermos evitar fornecer valores zero para os valores de retorno sem erro, podemos substituir return Guy{}, err por try(err) . Independentemente disso, a função defer -ed ainda é executada e o contexto é adicionado, o que é bom.

Novamente, isso significa que try aposta em reagir a erros, mas não em adicionar contexto a eles. Essa é uma distinção que me fez alusão e talvez a outros. Isso faz sentido porque a maneira como uma função adiciona contexto a um erro não é de interesse particular para o leitor, mas a maneira como uma função reage a erros é importante. Deveríamos tornar as partes menos interessantes do nosso código menos detalhadas, e é isso que try faz.

Você não é a única pessoa a sugerir que isso não é um problema ou que não vale a pena resolver. Consulte https://swtch.com/try.html#nonissue para outros comentários desse tipo. Nós anotamos isso e queremos ter certeza de que estamos resolvendo um problema real.

@rsc Também acho que não há problema com o código de erro atual. Então, por favor, conte comigo.

Ferramentas como o tryhard de Robert nos ajudam a fazer isso. Eu pedi anteriormente para as pessoas nos informarem o que encontraram em suas próprias bases de código. Essa informação será criticamente importante para avaliar se a mudança vale a pena ou não. Você tem um palpite e eu tenho outro, e tudo bem. A resposta é substituir os dados por essas suposições.

Eu olhei https://go-review.googlesource.com/c/go/+/182717/1/src/cmd/link/internal/ld/macho_combine_dwarf.go e gosto mais de código antigo. É surpreendente para mim que a chamada de função try possa interromper a execução atual. Não é assim que o Go atual funciona.

Eu suspeito, você vai achar que as opiniões variam. Acho que isso é muito subjetivo.

E, suspeito, a maioria dos usuários não está participando desse debate. Eles nem sabem que essa mudança está chegando. Eu mesmo estou bastante envolvido com o Go, mas não participo dessa mudança, porque não tenho tempo livre.

Acho que precisaríamos reeducar todos os usuários de Go existentes para pensar de maneira diferente agora.

Também precisaríamos decidir o que fazer com alguns usuários/empresas que se recusarão a usar try em seu código. Haverá alguns com certeza.

Talvez tivéssemos que mudar gofmt para reescrever o código atual automaticamente. Para forçar esses usuários "desonestos" a usar a nova função try. É possível fazer gofmt fazer isso?

Como lidaríamos com erros de compilação quando as pessoas usam go1.13 e antes para construir código com try?

Provavelmente perdi muitos outros problemas que teríamos que superar para implementar essa mudança. Vale a pena o problema? Eu não acredito.

Alex

@griesemer
Ao tentar tentar em um arquivo com 97 erros, nenhum foi capturado, descobri que os 2 padrões não foram traduzidos
1:

    if err := updateItem(tx, fields, entityView.DataBinding, entityInstance); err != nil {
        tx.Rollback()
        return nil, err
    }

Não é substituído, provavelmente porque o tx.Rollback() entre err := e a linha de retorno,
O que eu suponho que só deve ser tratado por defer - e se todos os caminhos de erro precisarem tx.Rollback()
Isto está certo ?

  1. Também não sugere:
if err := db.Error; err != nil {
        return nil, err
    } else if itemDb, err := GetItem(c, entity, entityView, ItemRequest{recNo}); err != nil {
        return nil, err
    } else {
        return itemDb, nil
    }

ou

    if err := db.Error; err != nil {
        return nil, err
    } else {
            if itemDb, err := GetItem(c, entity, entityView, ItemRequest{recNo}); err != nil {
                return nil, err
            } else {
                return itemDb, nil
            }
        return result, nil
    }

Isso é por causa do sombreamento ou a tentativa de aninhamento se traduziria em ? significado - este uso deve ser try ou sugerido para ser deixado como err := ... return err ?

@guybrand Re: os dois padrões que você encontrou:

1) sim, tryhard não se esforça muito. a verificação de tipo é necessária para casos mais complexos. Se tx.Rollback() deve ser feito em todos os caminhos, defer pode ser a abordagem correta. Caso contrário, manter o if pode ser a abordagem correta. Depende do código específico.

2) O mesmo aqui: tryhard não procura por este padrão mais complexo. Talvez pudesse.

Novamente, esta é uma ferramenta experimental para obter algumas respostas rápidas. Fazer certo requer um pouco mais de trabalho.

@alexbrainman

Como lidaríamos com erros de compilação quando as pessoas usam go1.13 e antes para construir código com try?

Meu entendimento é que a versão da linguagem em si será controlada pela diretiva de versão da linguagem go go.mod para cada pedaço de código sendo compilado.

A documentação em andamento go.mod descreve a diretiva de versão de idioma go assim:

A versão de idioma esperada, definida pela diretiva go , determina
quais recursos de linguagem estão disponíveis ao compilar o módulo.
Os recursos de idioma disponíveis nessa versão estarão disponíveis para uso.
Recursos de idioma removidos em versões anteriores ou adicionados em versões posteriores,
não estará disponível. Observe que a versão do idioma não afeta
build tags, que são determinadas pela versão Go que está sendo usada.

Se hipoteticamente algo como um novo try embutido chegar em algo como Go 1.15, então, nesse ponto, alguém cujo arquivo go.modgo 1.12 não teria acesso a esse novo try embutido mesmo se compilar com a cadeia de ferramentas Go 1.15. Meu entendimento do plano atual é que eles precisariam alterar a versão do idioma Go declarada em seu go.mod de go 1.12 para ler go 1.15 se quiserem usar o novo Go Recurso de idioma 1.15 de try .

Por outro lado, se você tem um código que usa try e esse código reside em um módulo cujo arquivo go.mod declara sua versão do idioma Go como go 1.15 , mas então alguém tenta construa isso com a cadeia de ferramentas Go 1.12, nesse ponto a cadeia de ferramentas Go 1.12 falhará com um erro de compilação. A cadeia de ferramentas Go 1.12 não sabe nada sobre try , mas sabe o suficiente para imprimir uma mensagem adicional de que o código que falhou ao compilar alegou exigir Go 1.15 com base no que está no arquivo go.mod . Você pode tentar esse experimento agora mesmo usando a cadeia de ferramentas Go 1.12 de hoje e ver a mensagem de erro resultante:

.\hello.go:3:16: undefined: try
note: module requires Go 1.15

Há uma discussão muito mais longa no documento de proposta de transição do Go2 .

Dito isso, os detalhes exatos disso podem ser melhor discutidos em outro lugar (por exemplo, talvez em #30791, ou neste recente tópico golang-nuts ).

@griesemer , desculpe se perdi uma solicitação mais específica para um formato, mas adoraria compartilhar alguns resultados e ter acesso (uma possível permissão) ao código-fonte de algumas empresas.
Abaixo está um exemplo real para um pequeno projeto, acho que os resultados em anexo dão uma boa amostra, se assim for, provavelmente podemos compartilhar alguma tabela com resultados semelhantes:

Total = Número de linhas de código
$find /path/to/repo -name '*.go' -exec cat {} \; | wc -l
Errs = número de linhas com err := (isso provavelmente perde err = e myerr := , mas acho que na maioria dos casos cobre)
$find /path/to/repo -name '*.go' -exec cat {} \; | grep "err :=" | wc -l
tryhard = número de linhas tryhard encontradas

o primeiro caso que testei para estudar retornou:
Total = 5106
Erros = 111
tentar = 16

base de código maior
Total = 131777
Erros = 3289
tentar = 265

Se este formato for aceitável, deixe-nos saber como você deseja obter os resultados, suponho que apenas jogá-lo aqui não seria o formato correto
Além disso, provavelmente seria uma rapidinha tentar contar as linhas, ocasiões de err := (e provavelmente err = , apenas 4 na base de código que tentei aprender)

Obrigado.

De @griesemer em https://github.com/golang/go/issues/32437#issuecomment -503276339

Exorto-vos a olhar para este código para um exemplo.

Em relação a esse código, notei que o arquivo out criado aqui nunca parece ser fechado. Além disso, é importante verificar os erros ao fechar os arquivos nos quais você gravou, porque essa pode ser a única vez que você é informado de que houve um problema com uma gravação.

Estou trazendo isso não como um relatório de bug (embora talvez devesse ser?), mas como uma chance de ver se try tem um efeito sobre como alguém pode corrigi-lo. Vou enumerar todas as maneiras que posso pensar para corrigi-lo e considerar se a adição de try ajudaria ou prejudicaria. Aqui estão algumas maneiras:

  1. Adicione chamadas explícitas para outf.Close() antes de qualquer retorno de erro.
  2. Nomeie o valor de retorno e adicione um adiamento para fechar o arquivo, registrando o erro se ainda não estiver presente. por exemplo
func foo() (err error) {
    outf := try(os.Create())
    defer func() {
        cerr := outf.Close()
        if err == nil {
            err = cerr
        }
    }()

    ...
}
  1. O padrão de "fechamento duplo" em que se faz defer outf.Close() para garantir a limpeza do recurso e try(outf.Close()) antes de retornar para garantir que não haja erros.
  2. Refatore para que uma função auxiliar pegue o arquivo aberto em vez de um caminho para que o chamador possa garantir que o arquivo seja fechado adequadamente. por exemplo
func foo() error {
    outf := try(os.Create())
    if err := helper(outf); err != nil {
        outf.Close()
        return err
    }
    try(outf.Close())
    return nil
}

Acho que em todos os casos, exceto no caso número 1, try é na pior das hipóteses neutro e geralmente positivo. E eu consideraria o número 1 como a opção menos palatável dado o tamanho e o número de possibilidades de erro nessa função, então adicionar try reduziria o apelo de uma escolha negativa.

Espero que esta análise tenha sido útil.

Se hipoteticamente algo como um novo try embutido chegar em algo como Go 1.15, então, nesse ponto, alguém cujo arquivo go.modgo 1.12 não teria acesso

@thepudds obrigado por explicar. Mas eu não uso módulos. Então sua explicação está muito acima da minha cabeça.

Alex

@alexbrainman

Como lidaríamos com erros de compilação quando as pessoas usam go1.13 e antes para construir código com try?

Se try hipoteticamente pousar em algo como Go 1.15, então a resposta muito curta para sua pergunta é que alguém usando Go 1.13 para construir código com try veria um erro de compilação como este:

.\hello.go:3:16: undefined: try
note: module requires Go 1.15

(Pelo menos até onde eu entendo o que foi dito sobre a proposta de transição).

@alexbrainman Obrigado pelo seu feedback.

Um grande número de comentários neste tópico são do tipo "isso não se parece com o Go", ou "O Go não funciona assim" ou "Não estou esperando que isso aconteça aqui". Está tudo correto, _existing_ Go não funciona assim.

Esta é talvez a primeira mudança de linguagem sugerida que afeta a sensação da linguagem de maneiras mais substanciais. Estamos cientes disso, e é por isso que o mantivemos tão mínimo. (Tenho dificuldade em imaginar o alvoroço que uma proposta concreta de genéricos pode causar - falando sobre uma mudança de linguagem).

Mas voltando ao seu ponto: os programadores se acostumam com a forma como uma linguagem de programação funciona e se sente. Se aprendi alguma coisa ao longo de uns 35 anos de programação é que se acostuma com quase qualquer linguagem, e isso acontece muito rápido. Depois de ter aprendido o Pascal original como minha primeira linguagem de alto nível, era _inconcebível_ que uma linguagem de programação não capitalizasse todas as suas palavras-chave. Mas levou apenas uma semana para se acostumar com o "mar de palavras" que era C, onde "não se podia ver a estrutura do código porque é tudo minúsculo". Depois daqueles dias iniciais com C, o código Pascal parecia terrivelmente barulhento, e todo o código real parecia enterrado em uma confusão de palavras-chave gritantes. Avançando para Go, quando introduzimos a capitalização para marcar identificadores exportados, foi uma mudança chocante em relação à abordagem anterior, se bem me lembro, baseada em palavras-chave (isso foi antes de Go ser público). Agora achamos que é uma das melhores decisões de design (com a ideia concreta vindo de fora da Go Team). Ou considere o seguinte experimento mental: Imagine que Go não tivesse uma declaração defer e agora alguém faz um forte argumento para defer . defer não tem semântica como qualquer outra coisa na linguagem, a nova linguagem não se parece com isso pré- defer Vá mais. No entanto, depois de viver com isso por uma década, parece totalmente "Go-like".

O ponto é que a reação inicial em relação a uma mudança de linguagem é quase sem sentido sem realmente tentar o mecanismo em código real e coletar feedback concreto. Claro, o código de tratamento de erros existente é bom e parece mais claro do que a substituição usando try - fomos treinados para pensar nessas instruções if há uma década. E é claro que o código try parece estranho e tem semântica "estranha", nunca o usamos antes e não o reconhecemos imediatamente como parte da linguagem.

É por isso que estamos pedindo às pessoas que realmente se envolvam com a mudança experimentando-a em seu próprio código; isto é, realmente escrevendo, ou ter tryhard rodando sobre o código existente, e considere o resultado. Eu recomendo deixá-lo descansar por um tempo, talvez uma semana ou mais. Veja de novo e dê um retorno.

Por fim, concordo com sua avaliação de que a maioria das pessoas não conhece essa proposta ou não se engajou nela. É bastante claro que esta discussão é dominada por talvez uma dúzia de pessoas. Mas ainda é cedo, esta proposta só saiu há duas semanas, e nenhuma decisão foi tomada. Há muito tempo para mais e diferentes pessoas se envolverem com isso.

https://github.com/golang/go/issues/32437#issuecomment -503297387 praticamente diz que se você está envolvendo erros de mais de uma maneira em uma única função, aparentemente você está fazendo isso errado. Enquanto isso, eu tenho um monte de código que se parece com isso:

        if err := gen.Execute(tmp, s); err != nil {
                return fmt.Errorf("template error: %v", err)
        }

        if err := tmp.Close(); err != nil {
                return fmt.Errorf("cannot write temp file: %v", err)
        }
        closed = true

        if err := os.Rename(tmp.Name(), *genOutput); err != nil {
                return fmt.Errorf("cannot finalize file: %v", err)
        }
        removed = true

( closed e removed são usados ​​por adiados para limpar, conforme apropriado)

Eu realmente não acho que tudo isso deva receber o mesmo contexto descrevendo a missão de nível superior dessa função. Eu realmente não acho que o usuário deve apenas ver

processing path/to/dir: template: gen:42:17: executing "gen" at <.Broken>: can't evaluate field Broken in type main.state

quando o modelo está estragado, acho que é responsabilidade do meu manipulador de erros que a chamada Execute do modelo adicione "modelo em execução" ou algum pouco extra. (Esse não é o melhor contexto, mas eu queria copiar e colar o código real em vez de um exemplo inventado.)

Eu não acho que o usuário deve ver

processing path/to/dir: rename /tmp/blahDs3x42aD commands.gen.go: No such file or directory

sem nenhuma pista de _por que_ meu programa está tentando fazer essa renomeação acontecer, qual é a semântica, qual é a intenção. Acredito que adicionar um pouco de "não é possível finalizar o arquivo:" realmente ajuda.

Se esses exemplos não o convencerem o suficiente, imagine esta saída de erro de um aplicativo de linha de comando:

processing path/to/dir: open /some/path/here: No such file or directory

O que isso significa? Eu quero adicionar um motivo pelo qual o aplicativo tentou criar um arquivo lá (você nem sabia que era um create, não apenas os.Open! É ENOENT porque um caminho intermediário não existe.). Isso não é algo que deve ser adicionado ao retorno de erro _every_ desta função.

Então, o que estou perdendo. Estou "segurando errado"? Devo colocar cada uma dessas coisas em uma pequena função separada que usa um adiamento para agrupar todos os seus erros?

@guybrand Obrigado por esses números . Seria bom ter alguns insights sobre por que os números tryhard são o que são. Talvez haja muita decoração de erro específica acontecendo? Se sim, isso é ótimo e as declarações if são a escolha certa.

Vou melhorar a ferramenta quando chegar a ela.

Obrigado, @zeebo por sua análise . Eu não sei sobre esse código especificamente, mas parece que outf faz parte de um loadCmdReader (linha 173) que é passado na linha 204. Talvez seja esse o motivo outf não está fechado. (Desculpe, eu não escrevi este código).

@tv42 A partir dos exemplos em seu https://github.com/golang/go/issues/32437#issuecomment -503340426, supondo que você não esteja fazendo isso "errado", parece usar uma instrução if é a maneira de lidar com esses casos se todos eles exigirem respostas diferentes. try não vai ajudar, e defer só vai dificultar (qualquer outra proposta de mudança de idioma neste tópico que está tentando tornar este código mais simples de escrever está tão próximo do if declaração de que não vale a pena introduzir um novo mecanismo). Veja também o FAQ da proposta detalhada.

@griesemer Então tudo o que consigo pensar é que você e @rsc discordam. Ou que estou, de fato, "fazendo errado", e gostaria de ter uma conversa sobre isso.

É um objetivo explícito, tanto naquela época quanto hoje, que qualquer solução torne mais provável que os usuários adicionem o contexto apropriado aos erros. E achamos que tentar pode fazer isso, ou não teríamos proposto.

@tv42 @rsc post é sobre a estrutura geral de tratamento de erros de um bom código, com o qual concordo. Se você tiver um trecho de código existente que não se encaixa exatamente nesse padrão e estiver satisfeito com o código, deixe-o em paz.

Difere

A principal mudança do rascunho de verificação/manuseio do Gophercon para esta proposta foi descartar handle em favor da reutilização de defer . Agora o contexto de erro seria adicionado por código como esta chamada adiada (veja meu comentário anterior sobre o contexto de erro):

func CopyFile(src, dst string) (err error) {
    defer HelperToBeNamedLater(&err, "copy %s %s", src, dst)

    r := check os.Open(src)
    defer r.Close()

    w := check os.Create(dst)
    ...
}

A viabilidade de adiar como o mecanismo de anotação de erro neste exemplo depende de algumas coisas.

  1. _Resultados de erros nomeados._ Tem havido muita preocupação em adicionar resultados de erros nomeados. É verdade que desencorajamos que no passado não fosse necessário para fins de documentação, mas essa é uma convenção que escolhemos na ausência de qualquer fator decisivo mais forte. E mesmo no passado, um fator decisivo mais forte, como referir-se a resultados específicos na documentação, superava a convenção geral para resultados não nomeados. Agora há um segundo fator decisivo mais forte, ou seja, querer se referir ao erro em um adiamento. Parece que não deveria ser mais censurável do que nomear resultados para uso na documentação. Várias pessoas reagiram de forma bastante negativa a isso, e eu honestamente não entendo o porquê. Quase parece que as pessoas estão confundindo retornos sem listas de expressões (os chamados “retornos nus”) com resultados nomeados. É verdade que retornos sem listas de expressões podem causar confusão em funções maiores. Evitar essa confusão evitando esses retornos em funções longas geralmente faz sentido. Pintar resultados nomeados com o mesmo pincel não.

  2. _Expressões de endereço._ Algumas pessoas levantaram preocupações de que usar esse padrão exigirá que os desenvolvedores Go entendam as expressões de endereço. Armazenar qualquer valor com métodos de ponteiro em uma interface já requer isso, então isso não parece uma desvantagem significativa.

  3. _Defer em si._ Algumas pessoas levantaram preocupações sobre o uso de defer como um conceito de linguagem, novamente porque novos usuários podem não estar familiarizados com ele. Assim como nas expressões de endereço, o defer é um conceito de linguagem central que deve ser aprendido eventualmente. As expressões idiomáticas padrão em torno de coisas como defer f.Close() e defer l.mu.Unlock() são tão comuns que é difícil justificar evitar defer como um canto obscuro da linguagem.

  4. _Desempenho._ Temos discutido por anos trabalhando em fazer padrões de defer comuns, como um defer no topo de uma função, com zero sobrecarga em comparação com a inserção manual dessa chamada em cada retorno. Achamos que sabemos como fazer isso e vamos explorá-lo para a próxima versão do Go. Mesmo que não, a sobrecarga atual de aproximadamente 50 ns não deve ser proibitiva para a maioria das chamadas que precisam adicionar contexto de erro. E as poucas chamadas sensíveis ao desempenho podem continuar a usar instruções if até que o defer seja mais rápido.

As três primeiras preocupações são todas objeções à reutilização de recursos de linguagem existentes. Mas a reutilização de recursos de linguagem existentes é exatamente o avanço dessa proposta em relação ao check/handle: há menos a acrescentar à linguagem principal, menos peças novas a aprender e menos interações surpreendentes.

Ainda assim, entendemos que usar defer dessa maneira é novo e que precisamos dar tempo às pessoas para avaliar se o defer funciona bem o suficiente na prática para os idiomas de tratamento de erros de que precisam.

Desde que iniciamos esta discussão em agosto passado, tenho feito o exercício mental de “como ficaria esse código com check/handle?” e mais recentemente "com try/defer?" cada vez que escrevo um novo código. Normalmente, a resposta significa que escrevo um código diferente e melhor, com o contexto adicionado em um lugar (o adiamento) em vez de em cada retorno ou omitido completamente.

Dada a ideia de usar um manipulador adiado para agir em caso de erros, há uma variedade de padrões que podemos habilitar com um pacote de biblioteca simples. Eu arquivei #32676 para pensar mais sobre isso, mas usando a API do pacote nesse problema, nosso código ficaria assim:

func CopyFile(src, dst string) (err error) {
    defer errd.Add(&err, "copy %s %s", src, dst)

    r := check os.Open(src)
    defer r.Close()

    w := check os.Create(dst)
    ...
}

Se estivéssemos depurando CopyFile e quiséssemos ver qualquer erro retornado e rastreamento de pilha (semelhante a querer inserir uma impressão de depuração), poderíamos usar:

func CopyFile(src, dst string) (err error) {
    defer errd.Trace(&err)
    defer errd.Add(&err, "copy %s %s", src, dst)

    r := check os.Open(src)
    defer r.Close()

    w := check os.Create(dst)
    ...
}

e assim por diante.

Usar defer dessa maneira acaba sendo bastante poderoso e mantém a vantagem de check/handle que você pode escrever “faça isso em qualquer erro” uma vez no topo da função e depois não se preocupe com isso pelo resto da o corpo. Isso melhora a legibilidade da mesma forma que as saídas rápidas iniciais .

Isso funcionará na prática? Queremos descobrir.

Tendo feito o experimento mental de como seria o adiamento em meu próprio código por alguns meses, acho que provavelmente funcionará. Mas é claro que conseguir usá-lo em código real nem sempre é o mesmo. Teremos de experimentar para descobrir.

As pessoas podem experimentar essa abordagem hoje, continuando a escrever instruções if err != nil , mas copiando os auxiliares de diferimento e fazendo uso deles conforme apropriado. Se você está inclinado a fazer isso, por favor, deixe-nos saber o que você aprendeu.

@tv42 , concordo com @griesemer. Se você achar que é necessário um contexto adicional para suavizar uma conexão como a renomeação sendo uma etapa de "finalização", não há nada de errado em usar instruções if para adicionar contexto adicional. Em muitas funções, no entanto, há pouca necessidade de tal contexto adicional.

@guybrand , os números tryhard são ótimos, mas ainda melhores seriam as descrições de por que exemplos específicos não foram convertidos e, além disso, seria inadequado reescrever para ser possível converter. O exemplo e a explicação de @ tv42 são uma instância disso.

@griesemer sobre sua preocupação com o adiamento . Eu estava indo para aquele emit ou na proposta inicial handle . O emit/handle seria chamado se o err não fosse nulo. E iniciará nesse momento em vez de no final da função. O adiamento é chamado no final. emit/handle terminaria a função com base em se err é nil ou não. É por isso que adiar não funcionaria.

alguns dados:

fora de um projeto de ~ 70k LOC que eu vendi para eliminar religiosamente "retornos de erro nu" ainda temos 612 retornos de erro nu. principalmente lidando com um caso em que um erro é registrado, mas a mensagem é importante apenas internamente (a mensagem para o usuário é predefinida). try() terá uma economia maior do que apenas duas linhas por cada retorno nu, porque com erros predefinidos podemos adiar um manipulador e usar try em mais lugares.

mais interessante, no diretório do fornecedor, de ~620k+ LOC, temos apenas 1600 retornos de erro nu. bibliotecas que escolhemos tendem a decorar erros ainda mais religiosamente do que nós.

@rsc se, posteriormente, os manipuladores forem adicionados a try haverá um pacote errors/errc com funções como func Wrap(msg string) func(error) error para que você possa fazer try(f(), errc.Wrap("f failed")) ?

@damienfamed75 Obrigado por suas explicações . Então o emit será chamado quando try encontrar um erro, e será chamado com esse erro. Isso parece bastante claro.

Você também está dizendo que o emit terminaria a função se houvesse um erro, e não se o erro fosse tratado de alguma forma. Se você não encerrar a função, onde o código continua? Presumivelmente com o retorno de try (caso contrário, não entendo o emit que não encerra a função). Não seria mais fácil e claro nesse caso usar apenas if em vez de try ? Usar um emit ou handle obscureceria tremendamente o fluxo de controle nesses casos, especialmente porque a cláusula emit pode estar em uma parte completamente diferente (presumivelmente anterior) na função. (Nessa nota, pode-se ter mais de um emit ? Se não, por que não? O que acontece se não houver um emit ? Muitas das mesmas perguntas que atormentaram o check original handle projeto de rascunho.)

Somente se alguém quiser retornar de uma função sem muito trabalho extra além da decoração de erros, ou sempre com o mesmo trabalho, faz sentido usar try , e algum tipo de manipulador. E esse mecanismo de manipulação, que é executado antes de uma função retornar, já existe em defer .

@guybrand (e @griesemer) em relação ao seu segundo padrão não reconhecido, consulte https://github.com/griesemer/tryhard/issues/2

@daved

Como o valor será derivado dos dados quando grande parte do processo público é impulsionado pelo sentimento?

Talvez outros possam ter uma experiência como a minha relatada aqui . Eu esperava percorrer algumas instâncias de try inseridas por tryhard , descobrir que elas se pareciam mais ou menos com o que já existia neste tópico e seguir em frente. Em vez disso, fiquei surpreso ao encontrar um caso em que try levou a um código claramente melhor, de uma maneira que não havia sido discutida antes.

Portanto, há pelo menos esperança. :)

Para as pessoas que estão testando tryhard , se você ainda não o fez, eu o encorajo não apenas a ver quais mudanças a ferramenta fez, mas também a procurar por instâncias restantes de err != nil e olhar para o que deixou sozinho, e por quê.

(E observe também que há alguns problemas e PRs em https://github.com/griesemer/tryhard/.)

@rsc aqui está minha visão de por que eu pessoalmente não gosto do padrão defer HandleFunc(&err, ...) . Não é porque eu associo isso com retornos nus ou algo assim, apenas parece muito "inteligente".

Houve uma proposta de tratamento de erros há alguns meses (talvez um ano?) atrás, no entanto, perdi o controle agora. Eu esqueci o que estava solicitando, no entanto, alguém respondeu com algo como:

func myFunction() (i int, err error) {
    defer func() {
        if err != nil {
            err = fmt.Errorf("wrapping the error: %s", err)
        }
    }()

    // ...
    return 0, err

    // ...
    return someInt, nil
}

Foi interessante ver para dizer o mínimo. Foi a primeira vez que vi defer usado para tratamento de erros, e agora está sendo mostrado aqui. Eu vejo isso como "inteligente" e "hacky", e, pelo menos no exemplo que trago, não parece Go. No entanto, envolvê-lo em uma chamada de função adequada com algo como fmt.HandleErrorf o ajuda a se sentir muito melhor. Eu ainda me sinto negativamente em relação a isso, no entanto.

Outra razão pela qual vejo as pessoas não gostando é que quando se escreve return ..., err , parece que err deve ser retornado. Mas não é devolvido, em vez disso, o valor é modificado antes do envio. Eu disse antes que return sempre pareceu uma operação "sagra" em Go, e encorajar o código que modifica um valor retornado antes de retornar realmente parece errado.

OK, números e dados é então. :)

Eu corri tryhard nas fontes de vários serviços da nossa plataforma de microsserviços, e comparei com os resultados de loccount e grep 'if err'. Eu obtive os seguintes resultados na ordem loccount / grep 'if err' | wc / tryhard:

1382/64/14
108554/66/5
58401/22/5
2052/247/39
12024/1655/1

Alguns de nossos microsserviços fazem muito tratamento de erros e alguns fazem pouco, mas, infelizmente, a tryhard só conseguiu melhorar automaticamente o código, na melhor das hipóteses, 22% dos casos, na pior das hipóteses, menos de 1%. Agora, não vamos reescrever manualmente nosso tratamento de erros, então uma ferramenta como tryhard será essencial para introduzir try() em nossa base de código. Eu aprecio que esta é uma ferramenta preliminar simples, mas fiquei surpreso com o quão raramente ela foi capaz de ajudar.

Mas acho que agora, com o número em mãos, posso dizer que para nosso uso, try() não está realmente resolvendo nenhum problema, ou pelo menos não até que tryhard se torne muito melhor.

Também descobri em nossas bases de código que o caso de uso if err != nil { return err } de try() é realmente muito raro, ao contrário do compilador go, onde é comum. Com todo o respeito, mas acho que os designers de Go, que estão analisando o código-fonte do compilador Go com muito mais frequência do que em outras bases de código, estão superestimando a utilidade de try() por causa disso.

@beoran tryhard é muito rudimentar no momento. Você tem noção das razões mais comuns pelas quais try seria raro em sua base de código? Por exemplo, porque você decora os erros? Porque você faz outro trabalho extra antes de retornar? Algo mais?

@rsc , @griesemer

Quanto aos exemplos , dei duas amostras repetidas aqui que tryHard perdeu, uma provavelmente ficará como "if Err :=", a outra pode ser resolvida

quanto à decoração de erro , dois padrões recorrentes que vejo no código são (coloco os dois em um trecho de código):

if v, err := someFunction(vars...) ; err != nil {
        return fmt.Errorf("extra data to help with where did error occur and params are %s , %d , err : %v",
            strParam, intParam, err)
    } else if v2, err := passToAnotherFunc(v,vars ...);err != nil {
        extraData := DoSomethingAccordingTo(v2,err)
        return formatError(err,extraData)
    } else {

    }

E muitas vezes o formatError é algum padrão para o aplicativo ou repositórios cruzados, mais repetido é a formatação DbError (uma função em todos os aplicativos/aplicativos, usada em dezenas de locais), em alguns casos (sem entrar em "isso é um padrão correto") salvando alguns dados para log (falha na consulta sql que você não gostaria de passar para a pilha) e algum outro texto para o erro.

Em outras palavras, se eu quiser "fazer algo inteligente com dados extras, como registrar o erro A e gerar o erro B, além de mencionar essas duas opções para estender o tratamento de erros
Esta é outra opção para "mais do que apenas retornar o erro e deixar 'alguém' ou 'alguma outra função' lidar com isso"

O que significa que provavelmente há mais uso para try() em "bibliotecas" do que em "programas executáveis", talvez eu tente executar a comparação Total/Errs/tryHard diferenciando libs de executáveis ​​("apps").

Eu me encontrei exatamente na situação descrita em https://github.com/golang/go/issues/32437#issuecomment -503297387
Em algum nível eu envolvo os erros individualmente, não vou mudar isso com try , tudo bem com if err!=nil .
Em outro nível, eu apenas return err é difícil adicionar o mesmo contexto para todos os retornos, então usarei try e defer .
Eu até já faço isso com um logger específico que uso no início da função apenas em caso de erro. Pra mim try e a decoração por função já é uma bosta.

@thepudds

Se try hipoteticamente pousar em algo como Go 1.15, então a resposta muito curta para sua pergunta é que alguém usando Go 1.13

O Go 1.13 ainda nem foi lançado, então não posso usá-lo. E, como meu projeto não usa módulos Go, não poderei atualizar para Go 1.13. (Acredito que o Go 1.13 exigirá que todos usem os módulos Go)

para construir código com try veria um erro de compilação como este:

.\hello.go:3:16: undefined: try
note: module requires Go 1.15

(Pelo menos até onde eu entendo o que foi dito sobre a proposta de transição).

Isso é tudo hipotético. É difícil para mim comentar sobre coisas fictícias. E, talvez você goste desse erro, mas acho confuso e inútil.

Se try for indefinido, eu faria grep por ele. E não encontrarei nada. O que devo fazer então?

E o note: module requires Go 1.15 é a pior ajuda nesta situação. Por que module ? Por que Go 1.15 ?

@griesemer

Esta é talvez a primeira mudança de linguagem sugerida que afeta a sensação da linguagem de maneiras mais substanciais. Estamos cientes disso, e é por isso que o mantivemos tão mínimo. (Tenho dificuldade em imaginar o alvoroço que uma proposta concreta de genéricos pode causar - falando sobre uma mudança de linguagem).

Eu preferiria que você gastasse tempo com genéricos, em vez de tentar. Talvez haja um benefício em ter genéricos em Go.

Mas voltando ao seu ponto: os programadores se acostumam com a forma como uma linguagem de programação funciona e se sente. ...

Concordo com todos os seus pontos. Mas estamos falando sobre a substituição de uma forma particular da instrução if por uma chamada de função try. Isso está na linguagem que se orgulha da simplicidade e da ortogonalidade. Eu posso me acostumar com tudo, mas qual é o ponto? Para salvar algumas linhas de código?

Ou considere o seguinte experimento mental: Imagine que Go não tivesse uma declaração defer e agora alguém faz um forte argumento para defer . defer não tem semântica como qualquer outra coisa na linguagem, a nova linguagem não se parece com isso pré- defer Vá mais. No entanto, depois de viver com isso por uma década, parece totalmente "Go-like".

Depois de muitos anos, ainda sou enganado pelo corpo defer e fechado sobre variáveis. Mas defer paga seu preço em espadas quando se trata de gerenciamento de recursos. Não consigo imaginar ir sem defer . Mas não estou preparado para pagar um preço semelhante por try , porque não vejo benefícios aqui.

É por isso que estamos pedindo às pessoas que realmente se envolvam com a mudança experimentando-a em seu próprio código; isto é, realmente escrevendo, ou ter tryhard rodando sobre o código existente, e considere o resultado. Eu recomendo deixá-lo descansar por um tempo, talvez uma semana ou mais. Veja de novo e dê um retorno.

Tentei alterar um pequeno projeto meu (cerca de 1200 linhas de código). E parece semelhante à sua alteração em https://go-review.googlesource.com/c/go/+/182717/1/src/cmd/link/internal/ld/macho_combine_dwarf.go Não vejo minha opinião mudar sobre isso depois de uma semana. Minha mente está sempre ocupada com alguma coisa, e eu vou esquecer.

... Mas ainda é cedo, esta proposta só saiu há duas semanas, ...

E posso ver que já existem 504 mensagens sobre esta proposta apenas neste tópico. Se eu estiver interessado em levar essa mudança adiante, levarei dias, se não semanas, apenas para ler e compreender tudo isso. Eu não invejo seu trabalho.

Obrigado por ter tempo para responder à minha mensagem. Desculpe, se eu não responder a este tópico - é muito grande para eu monitorar, se a mensagem é endereçada a mim ou não.

Alex

@griesemer Obrigado pela proposta maravilhosa e tryhard parece ser mais útil do que eu esperava. Eu também vou querer apreciar.

@rsc obrigado pela resposta e ferramenta bem articuladas.

Tenho acompanhado este tópico por um tempo e os seguintes comentários de @beoran me dão calafrios

Ocultar a variável de erro e o retorno não ajuda a tornar as coisas mais fáceis de entender

Já gerenciei vários bad written code antes e posso testemunhar que é o pior pesadelo para todo desenvolvedor.

O fato de a documentação dizer para usar A likes não significa que seria seguido, o fato permanece se for possível usar AA , AB então não há limite de como isso pode ser usado.

To my surprise, people already think the code below is cool ... Acho que it's an abomination com todo respeito peço desculpas a quem se ofender.

parentCommitOne := try(try(try(r.Head()).Peel(git.ObjectCommit)).AsCommit())
parentCommitTwo := try(try(remoteBranch.Reference.Peel(git.ObjectCommit)).AsCommit())

Espere até que você verifique AsCommit e veja

func AsCommit() error(){
    return try(try(try(tail()).find()).auth())
}

A loucura continua e honestamente eu não quero acreditar que essa é a definição de @robpike simplicity is complicated (Humor)

Com base no exemplo @rsc

// Example 1
headRef := try(r.Head())
parentObjOne := try(headRef.Peel(git.ObjectCommit))
parentObjTwo := try(remoteBranch.Reference.Peel(git.ObjectCommit))
parentCommitOne := parentObjOne.AsCommit()
parentCommitTwo := parentObjTwo.AsCommit()
treeOid := try(index.WriteTree())
tree := try(r.LookupTree(treeOid))

// Example 2 
try headRef := r.Head()
try parentObjOne := headRef.Peel(git.ObjectCommit)
try parentObjTwo := remoteBranch.Reference.Peel(git.ObjectCommit)
parentCommitOne := parentObjOne.AsCommit()
parentCommitTwo := parentObjTwo.AsCommit()
try treeOid := index.WriteTree()
try tree := r.LookupTree(treeOid)

// Example 3 
try (
    headRef := r.Head()
    parentObjOne := headRef.Peel(git.ObjectCommit)
    parentObjTwo := remoteBranch.Reference.Peel(git.ObjectCommit)
)
parentCommitOne := parentObjOne.AsCommit()
parentCommitTwo := parentObjTwo.AsCommit()
try (
    treeOid := index.WriteTree()
    tree := r.LookupTree(treeOid)
)

Sou a favor de Example 2 com um pouco else , Por favor, note que esta pode não ser a melhor abordagem no entanto

  • É fácil ver claramente o erro
  • Menos possível de se transformar no abomination que os outros podem dar à luz
  • try não se comporta como uma função normal. dar-lhe uma sintaxe de função é pouco. go usa if e se eu puder mudar para try tree := r.LookupTree(treeOid) else { fica mais natural
  • Erros podem ser muito, muito caros, eles precisam de tanta visibilidade quanto possível e acho que é por isso que o go não suporta o tradicional try & catch
try headRef := r.Head()
try parentObjOne := headRef.Peel(git.ObjectCommit)
try parentObjTwo := remoteBranch.Reference.Peel(git.ObjectCommit)

parentCommitOne := parentObjOne.AsCommit()
parentCommitTwo := parentObjTwo.AsCommit()

try treeOid := index.WriteTree()
try tree := r.LookupTree(treeOid) else { 
    // Heal the world 
   // I may return with return keyword 
   // I may not return but set some values to 0 
   // I may remember I need to log only this 
   // I may send a mail to let the cute monkeys know the server is on fire 
}

Mais uma vez quero me desculpar por ser um pouco egoísta.

@josharian Não posso divulgar muito aqui, porém, os motivos são bem diversos. Como você diz, nós decoramos os erros, e ou também fazemos processamentos diferentes, e também, um caso de uso importante é que nós os registramos, onde a mensagem de log difere para cada erro que uma função pode retornar, ou porque usamos o if err := foo() ; err != nil { /* various handling*/ ; return err } formulário, ou outros motivos.

O que quero enfatizar é o seguinte: o caso de uso simples para o qual try() foi projetado ocorre muito raramente em nossa base de código. Portanto, para nós não há muito a ganhar em adicionar 'try()' à linguagem.

EDIT: Se try() for implementado, acho que o próximo passo deve ser tornar o tryhard muito melhor, para que possa ser usado amplamente para atualizar as bases de código existentes.

@griesemer Vou tentar resolver todas as suas preocupações uma a uma da sua última resposta .
Primeiro você perguntou se o manipulador não retorna ou sai da função de alguma forma, então o que aconteceria. Sim, pode haver casos em que a cláusula emit / handle não retornará ou sairá de uma função, mas continuará de onde parou. Por exemplo, no caso de estarmos tentando encontrar um delimitador ou algo simples usando um leitor e chegarmos ao EOF , podemos não querer retornar um erro quando o encontramos. Então eu construí este exemplo rápido de como isso poderia ser:

func findDelimiter(r io.Reader) ([]byte, error) {
    emit err {
        // if this doesn't return then continue from where we left off
        // at the try function that was called last.
        if err != io.EOF {
            return nil, err
        }
    }

    bufReader := bufio.NewReader(r)

    token := try(bufReader.ReadSlice('|'))

    return token, nil
}

Ou ainda poderia ser mais simplificado para isso:

func findDelimiter(r io.Reader) ([]byte, error) {
    emit err != io.EOF {
        return nil, err
    }

    bufReader := bufio.NewReader(r)

    token := try(bufReader.ReadSlice('|'))

    return token, nil
}

A segunda preocupação era com a interrupção do fluxo de controle. E sim, isso atrapalharia o fluxo, mas para ser justo, a maioria das propostas está atrapalhando um pouco o fluxo de ter uma função central de tratamento de erros e tal. Isso não é diferente, eu acredito.
Em seguida, você perguntou se usamos emit / handle mais de uma vez em que digo que é redefinido.
Se você usar emit mais de uma vez, ele substituirá o último e assim por diante. Se você não tiver nenhum, o try terá um manipulador padrão que apenas retornará valores nulos e o erro. Isso significa que este exemplo aqui:

func writeStuff(filename string) (io.ReadCloser, error) {
    emit err {
        return nil, err
    }

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

    try(fmt.Fprintf(f, "stuff\n"))

    return f, nil
}

Faria a mesma coisa que este exemplo:

func writeStuff(filename string) (io.ReadCloser, error) {
    // when not defining a handler then try's default handler kicks in to
    // return nil valued then error as usual.

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

    try(fmt.Fprintf(f, "stuff\n"))

    return f, nil
}

Sua última pergunta foi sobre declarar uma função de manipulador que é chamada em um defer com suponho uma referência a um error . Este design não funciona da mesma forma que esta proposta funciona porque um defer não pode parar imediatamente uma função dada uma condição em si.

Acredito que abordei tudo em sua resposta e espero que isso esclareça um pouco mais minha proposta. Se houver mais preocupações, me avise porque acho que toda essa discussão com todos é muito divertida para refletir sobre novas ideias. Continuem com o ótimo trabalho, pessoal!

@velovix , re https://github.com/golang/go/issues/32437#issuecomment -503314834:

Novamente, isso significa que try aposta em reagir a erros, mas não em adicionar contexto a eles. Essa é uma distinção que me fez alusão e talvez a outros. Isso faz sentido porque a maneira como uma função adiciona contexto a um erro não é de interesse particular para o leitor, mas a maneira como uma função reage a erros é importante. Devemos tornar as partes menos interessantes do nosso código menos detalhadas, e é isso que try faz.

Esta é uma maneira muito bonita de colocá-lo. Obrigado.

@olekukonko , re https://github.com/golang/go/issues/32437#issuecomment -503508478:

To my surprise, people already think the code below is cool ... Acho que it's an abomination com todo respeito peço desculpas a quem se ofender.

parentCommitOne := try(try(try(r.Head()).Peel(git.ObjectCommit)).AsCommit())
parentCommitTwo := try(try(remoteBranch.Reference.Peel(git.ObjectCommit)).AsCommit())

Grepping https://swtch.com/try.html , essa expressão ocorreu três vezes neste tópico.
@goodwine trouxe isso como código ruim, eu concordei, e @velovix disse "apesar de sua feiúra ... é melhor do que você costuma ver em linguagens try-catch ... porque você ainda pode dizer quais partes do código podem desviar controlar o fluxo devido a um erro e que não pode."

Ninguém disse que era "legal" ou algo para apresentar como um ótimo código. Novamente, é sempre possível escrever código ruim .

Eu também diria apenas re

Erros podem ser muito, muito caros, eles precisam de tanta visibilidade quanto possível

Erros em Go não são caros. São ocorrências cotidianas, comuns e destinadas a serem leves. (Isso contrasta com algumas implementações de exceções em particular. Certa vez tivemos um servidor que gastou muito tempo de CPU preparando e descartando objetos de exceção contendo rastreamentos de pilha para chamadas "abertura de arquivo" com falha em um loop verificando uma lista de locais para um determinado arquivo.)

@alexbrainman , desculpe a confusão sobre o que acontece se versões mais antigas do código de compilação Go contendo try. A resposta curta é que é como qualquer outra vez que mudamos o idioma: o compilador antigo rejeitará o novo código com uma mensagem quase inútil (neste caso "undefined: try"). A mensagem não ajuda porque o compilador antigo não conhece a nova sintaxe e não pode ser mais útil. As pessoas provavelmente fariam uma pesquisa na web por "go undefined try" e descobririam sobre o novo recurso.

No exemplo do @thepudds , o código usando try tem um go.mod que contém a linha 'go 1.15', o que significa que o autor do módulo diz que o código é escrito em relação à versão da linguagem Go. Isso serve como um sinal para comandos go mais antigos para sugerir após um erro de compilação que talvez a mensagem inútil seja devido a uma versão muito antiga do Go. Isso é explicitamente uma tentativa de tornar a mensagem um pouco mais útil sem forçar os usuários a recorrer a pesquisas na web. Se ajudar, ótimo; se não, as pesquisas na web parecem bastante eficazes de qualquer maneira.

@guybrand , re https://github.com/golang/go/issues/32437#issuecomment -503287670 e com desculpas por provavelmente chegar tarde demais para o seu encontro:

Um problema em geral com funções que retornam tipos não exatamente de erro é que, para não interfaces, a conversão para erro não preserva a nulidade. Então, por exemplo, se você tiver seu próprio tipo concreto *MyError personalizado (digamos, um ponteiro para uma estrutura) e usar err == nil como o sinal de sucesso, isso é ótimo até que você tenha

func f() (int, *MyError)
func g() (int, error) { x, err := f(); return x, err }

Se f retornar um nil *MyError, g retornará o mesmo valor como um erro não-nil, o que provavelmente não é o que se pretendia. Se *MyError for uma interface em vez de um ponteiro de estrutura, a conversão preserva a nulidade, mas mesmo assim é uma sutileza.

Para try, você pode pensar que, como try só seria acionado para valores não nulos, não há problema. Por exemplo, na verdade, isso está OK no que diz respeito a retornar um erro não nulo quando f falha, e também está OK na medida em que retorna um erro nil quando f é bem-sucedido:

func g() (int, error) {
    return try(f()), nil
}

Na verdade, tudo bem, mas você pode ver isso e pensar em reescrevê-lo para

func g() (int, error) {
    return f()
}

que parece que deveria ser o mesmo, mas não é.

Há tantos outros detalhes da proposta de tentativa que precisam de exame cuidadoso e avaliação na experiência real que parecia que decidir sobre essa sutileza em particular seria melhor adiar.

Obrigado a todos por todos os comentários até agora . Neste ponto, parece que identificamos os principais benefícios, preocupações e possíveis implicações boas e ruins de try . Para progredir, eles precisam ser avaliados ainda mais, analisando o que try significaria para bases de código reais. A discussão neste momento está circulando e repetindo esses mesmos pontos.

A experiência é agora mais valiosa do que a discussão contínua. Queremos incentivar as pessoas a reservar um tempo para experimentar como try ficaria em suas próprias bases de código e escrever e vincular relatórios de experiência na página de feedback .

Para dar a todos algum tempo para respirar e experimentar, vamos pausar esta conversa e bloquear o problema para a próxima semana e meia.

O bloqueio começará por volta de 1p PDT/4p EDT (em cerca de 3h a partir de agora) para dar às pessoas a chance de enviar uma postagem pendente. Reabriremos a questão para mais discussão em 1º de julho.

Tenha certeza de que não temos intenção de apressar quaisquer novos recursos de linguagem sem dedicar um tempo para entendê-los bem e garantir que eles estejam resolvendo problemas reais em código real. Levaremos o tempo necessário para acertar isso, assim como fizemos no passado.

Essa página wiki está cheia de respostas para verificar/tratar. Sugiro que você inicie uma nova página.

De qualquer forma, não terei tempo para continuar a jardinagem na wiki.

@networkimprov , obrigado por sua ajuda na jardinagem. Eu criei uma nova seção superior em https://github.com/golang/go/wiki/Go2ErrorHandlingFeedback. Acho que deveria ser melhor do que uma página totalmente nova.

Eu também perdi a nota 1p PDT / 4p EDT de Robert para o bloqueio, então bloqueei brevemente um pouco cedo demais. Está aberto novamente, por mais um pouco.

Eu estava planejando escrever isso, e só queria completá-lo antes de ser bloqueado.

Espero que a equipe go não veja as críticas e sinta que isso é indicativo do sentimento da maioria. Há sempre a tendência de a minoria vocal sobrecarregar a conversa, e eu sinto que isso pode ter acontecido aqui. Quando todo mundo está indo pela tangente, isso desencoraja outros que só querem falar sobre a proposta COMO ESTÁ.

Então - eu gostaria de articular minha posição positiva para o que vale a pena.

Eu tenho um código que já usa defer para decorar/anotar erros, até mesmo para cuspir rastreamentos de pilha, exatamente por esse motivo.

Ver:
https://github.com/ugorji/go-ndb/blob/master/ndb/ndb.go#L331
https://github.com/ugorji/go-serverapp/blob/master/app/baseapp.go#L129
https://github.com/ugorji/go-serverapp/blob/master/app/webrouter.go#L180

que todos chamam errorutil.OnError(*error)

https://github.com/ugorji/go-common/blob/master/errorutil/errors.go#L193

Isso está na linha dos ajudantes de diferimento que Russ/Robert mencionam anteriormente.

É um padrão que eu já uso, FWIW. Não é mágica. É completamente como IMHO.

Eu também o uso com parâmetros nomeados e funciona de maneira excelente.

Digo isso para contestar a noção de que qualquer coisa recomendada aqui é mágica.

Em segundo lugar, eu queria adicionar alguns comentários sobre try(...) como uma função.
Ele tem uma clara vantagem sobre uma palavra-chave, pois pode ser estendido para receber parâmetros.

Existem 2 modos de extensão que foram discutidos aqui:

  • estender tente pegar um rótulo para pular para
  • extend tenta pegar um manipulador do formulário func(error) error

Para cada um deles, é necessário que try como função receba um único parâmetro, podendo ser estendido posteriormente para receber um segundo parâmetro, se necessário.

A decisão não foi tomada sobre a necessidade de estender a tentativa e, em caso afirmativo, qual direção tomar. Conseqüentemente, a primeira direção é tentar eliminar a maior parte da gagueira "if err != nil { return err }" que eu sempre detestei, mas tomei como o custo de fazer negócios em andamento.

Pessoalmente, estou feliz que try seja uma função, que eu possa chamar inline, por exemplo, posso escrever

var u User = db.loadUser(try(strconv.Atoi(stringId)))

EM oposição a:

var id int // i have to define this on its own if err is already defined in an enclosing block
id, err = strconv.Atoi(stringId)
if err != nil {
  return
}
var u User = db.loadUser(id)

Como você pode ver, eu apenas reduzi 6 linhas para 1. E 5 dessas linhas são verdadeiramente clichê.
Isso é algo com o qual lidei muitas vezes, e escrevi muitos códigos e pacotes go - você pode verificar meu github para ver alguns dos que publiquei on-line ou minha biblioteca go-codec.

Finalmente, muitos dos comentários aqui não mostraram realmente problemas com a proposta, por mais que tenham postulado sua própria maneira preferida de resolver o problema.

Pessoalmente, estou emocionado que try(...) está chegando. E aprecio as razões pelas quais try como uma função é a solução preferida. Eu claramente gosto que o adiamento esteja sendo usado aqui, pois apenas faz sentido.

Vamos relembrar um dos princípios fundamentais do go - conceitos ortogonais que podem ser bem combinados. Esta proposta aproveita vários conceitos ortogonais do go (defer, parâmetros de retorno nomeados, funções internas para fazer o que não é possível via código do usuário, etc.)
go usuários têm solicitado universalmente por anos, ou seja, reduzindo/eliminando o clichê if err != nil { return err }. As pesquisas com usuários do Go mostram que esse é um problema real. A equipe go está ciente de que é um problema real. Estou feliz que as vozes altas de alguns não estão distorcendo muito a posição da equipe de go.

Eu tinha uma pergunta sobre tentar como um goto implícito if err != nil.

Se decidirmos que essa é a direção, será difícil adaptar "tentar faz um retorno" para "tentar faz um goto",
dado que goto definiu semântica que você não pode passar por variáveis ​​não alocadas?

Obrigado por sua nota, @ugorji.

Eu tinha uma pergunta sobre tentar como um goto implícito if err != nil.

Se decidirmos que essa é a direção, será difícil adaptar "tentar faz um retorno" para "tentar faz um goto",
dado que goto definiu semântica que você não pode passar por variáveis ​​não alocadas?

Sim, exatamente certo. Há alguma discussão sobre #26058.
Eu acho que 'try-goto' tem pelo menos três strikes contra ele:
(1) você tem que responder a variáveis ​​não alocadas,
(2) você perde informações da pilha sobre qual tentativa falhou, que, em contraste, você ainda pode capturar no caso return+defer, e
(3) todo mundo adora odiar no goto.

Sim, try é o caminho a percorrer.
Tentei adicionar try uma vez e gostei.
Patch - https://github.com/ascheglov/go/pull/1
Tópico no Reddit - https://www.reddit.com/r/golang/comments/6vt3el/the_try_keyword_proofofconcept/

@griesemer

Continuando em https://github.com/golang/go/issues/32825#issuecomment -507120860 ...

Seguindo a premissa de que o abuso de try será mitigado pela revisão de código, verificação e/ou padrões da comunidade, posso ver a sabedoria em evitar alterar o idioma para restringir a flexibilidade de try . Não vejo a sabedoria de fornecer instalações adicionais que encorajem fortemente as manifestações mais difíceis/desagradáveis ​​de consumir.

Ao dividir isso, parece haver duas formas de fluxo de controle de caminho de erro sendo expressas: Manual e Automático. Em relação ao encapsulamento de erros, parece haver três formas sendo expressas: Direta, Indireta e Pass-through. Isso resulta em seis "modos" totais de tratamento de erros.

Os modos Direto Manual e Direto Automático parecem agradáveis:

wrap := func(err error) error {
  return fmt.Errorf("failed to process %s: %v", filename, err)
}

f, err := os.Open(filename)
if err != nil {
    return nil, wrap(err)
}
defer f.Close()

info, err := f.Stat()
if err != nil {
    return nil, wrap(err)
}
// in errors, named better, and optimized
WrapfFunc := func(format string, args ...interface{}) func(error) error {
  return func(err error) error {
    if err == nil {
      return nil
    }
    s := fmt.Sprintf(format, args...)
    return errors.Errorf(s+": %w", err)
  }
}

```vai
wrap := errors.WrapfFunc("falha ao processar %s", filename)

f, err := os.Open(nome do arquivo)
tente(enrole(err))
adiar f.Fechar()

info, err := f.Stat()
tente(enrole(err))

Manual Pass-through, and Automatic Pass-through modes are also simple enough to be agreeable (despite often being a code smell):
```go
f, err := os.Open(filename)
if err != nil {
    return nil, err
}
defer f.Close()

info, err := f.Stat()
if err != nil {
    return nil, err
}
f := try(os.Open(filename))
defer f.Close()

info := try(f.Stat())

No entanto, os modos Indireto Manual e Indireto Automático são bastante desagradáveis ​​devido à alta probabilidade de erros sutis:

defer errd.Wrap(&err, "failed to do X for %s", filename)

var f *os.File
f, err = os.Open(filename)
if err != nil {
    return
}
defer f.Close()

var info os.FileInfo
info, err = f.Stat()
if err != nil {
    return
}
defer errd.Wrap(&err, "failed to do X for %s", filename)

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

info := try(f.Stat())

Novamente, posso entender não proibi-los, mas facilitar/abençoar os modos indiretos é onde isso ainda está levantando bandeiras vermelhas claras para mim. O suficiente, neste momento, para eu permanecer enfaticamente cético em relação a toda a premissa.

Try não deve ser uma função para evitar aquele maldito

info := try(try(os.Open(filename)).Stat())

vazamento de arquivo.

Quero dizer try declaração não permitirá encadeamento. E é mais bonito como um bônus. Existem problemas de compatibilidade embora.

@sirkon Como try é especial, a linguagem pode não permitir try aninhados se isso for importante - mesmo que try pareça uma função. Novamente, se este for o único obstáculo para try , isso pode ser facilmente resolvido de várias maneiras ( go vet ou restrição de idioma). Vamos seguir em frente - já ouvimos isso muitas vezes. Obrigado.

Vamos seguir em frente - já ouvimos isso muitas vezes antes

“Isso é tão chato, vamos seguir em frente”

Há outro bom análogo:

- Sua teoria contradiz os fatos!
- O pior para os fatos!

Por Hegel

Quero dizer que você está resolvendo um problema que não existe de fato. E o jeito feio disso.

Vamos dar uma olhada onde esse problema realmente aparece: lidar com efeitos colaterais do mundo exterior, é isso. E esta é realmente uma das partes mais fáceis logicamente na engenharia de software. E o mais importante nisso. Não consigo entender por que precisamos de uma simplificação para a coisa mais fácil que nos custará menos confiabilidade.

IMO o problema mais difícil desse tipo é a preservação da consistência de dados em sistemas distribuídos (e não tão distribuídos de fato). E o tratamento de erros não era um problema com o qual eu estava lutando em Go ao resolvê-los. A falta de compreensão de fatias e mapas, falta de soma/algébrica/variância/qualquer tipo era MUITO mais irritante.

Como o debate aqui parece continuar inabalável, deixe-me repetir novamente:

A experiência é agora mais valiosa do que a discussão contínua. Queremos incentivar as pessoas a reservar um tempo para experimentar como try ficaria em suas próprias bases de código e escrever e vincular relatórios de experiência na página de feedback.

Se a experiência concreta fornecer evidências significativas a favor ou contra esta proposta, gostaríamos de ouvir isso aqui. Aborrecimentos pessoais, cenários hipotéticos, designs alternativos, etc. podemos reconhecer, mas eles são menos acionáveis.

Obrigado.

Não quero ser rude aqui e agradeço toda a sua moderação, mas a comunidade falou muito fortemente sobre a mudança no tratamento de erros. Mudar as coisas, ou adicionar novo código, vai incomodar _todas_ as pessoas que preferem o sistema atual. Você não pode fazer todo mundo feliz, então vamos nos concentrar nos 88% que podemos fazer felizes (número derivado da proporção de votos abaixo).

No momento da redação deste artigo, o segmento "deixe em paz" está com 1322 votos para cima e 158 para baixo. Este segmento está em 158 para cima e 255 para baixo. Se isso não for um fim direto deste tópico sobre tratamento de erros, devemos ter uma boa razão para continuar empurrando o problema.

É possível sempre fazer o que sua comunidade pede e destruir seu produto ao mesmo tempo.

No mínimo, acho que esta proposta específica deve ser considerada como fracassada.

Felizmente, go não foi projetado por um comitê. Precisamos confiar que os guardiões da língua que todos amamos continuarão a tomar a melhor decisão, considerando todos os dados disponíveis, e não tomarão uma decisão baseada na opinião popular das massas. Lembre-se - eles usam go também, assim como nós. Eles sentem os pontos de dor, assim como nós.

Se você tem uma posição, reserve um tempo para defendê-la da mesma forma que a equipe Go defende suas propostas. Caso contrário, você está apenas afogando a conversa com sentimentos inconstantes que não são acionáveis ​​e não levam as conversas adiante. E isso torna mais difícil para as pessoas que querem se envolver, como disseram as pessoas podem apenas querer esperar até que o barulho diminua.

Quando o processo de proposta começou, Russ fez questão de evangelizar a necessidade de relatos de experiência como forma de influenciar uma proposta ou fazer seu pedido ser ouvido. Vamos pelo menos tentar honrar isso.

A equipe go tem levado em consideração todos os comentários acionáveis. Eles ainda não falharam conosco. Veja os documentos detalhados produzidos para alias, para módulos, etc. Vamos pelo menos dar a eles a mesma consideração e gastar tempo para refletir sobre nossas objeções, responder à posição deles sobre suas objeções e dificultar que sua objeção seja ignorada.

O benefício de Go sempre foi que é uma linguagem pequena e simples com construções ortogonais projetadas por um pequeno grupo de pessoas que pensariam criticamente no espaço antes de se comprometer com uma posição. Vamos ajudá-los onde pudermos, em vez de apenas dizer "veja, o voto popular diz não" - onde muitas pessoas votando podem nem ter muita experiência em ir ou entender o ir totalmente. Já li pôsteres em série que admitiram não conhecer alguns conceitos fundamentais dessa linguagem reconhecidamente pequena e simples. Isso torna difícil levar seu feedback a sério.

De qualquer forma, é uma merda que estou fazendo isso aqui - sinta-se à vontade para remover este comentário. Eu não vou ficar ofendido. Mas alguém tem que dizer isso sem rodeios!

Essa coisa toda da 2ª proposta parece muito com influenciadores digitais organizando um comício para mim. Concursos de popularidade não avaliam méritos técnicos.

As pessoas podem ficar em silêncio, mas ainda esperam o Go 2. Pessoalmente, estou ansioso por isso e pelo resto do Go 2. O Go 1 é uma ótima linguagem e bem adequada para diferentes tipos de programas. Espero que Go 2 expanda isso.

Por fim, também reverterei minha preferência por ter try como uma declaração. Agora apoio a proposta tal como está. Depois de tantos anos sob a promessa de compatibilidade "Go 1", as pessoas pensam que o Go foi esculpido em pedra. Devido a essa suposição problemática, não alterar a sintaxe da linguagem neste caso parece um compromisso muito melhor aos meus olhos agora. Edit: Também estou ansioso para ver os relatórios de experiência para verificação de fatos.

PS: Eu me pergunto que tipo de oposição acontecerá quando os genéricos forem propostos.

Temos cerca de uma dúzia de ferramentas escritas em nossa empresa. Executei a ferramenta tryhard em nossa base de código e encontrei 933 potenciais candidatos a try(). Pessoalmente, acredito que a função try() é uma ideia brilhante porque resolve mais do que apenas um problema de código clichê.

Ele impõe o chamador e a função/método chamado para retornar o erro como o último parâmetro. Não será permitido:

var file= try(parse())

func parse()(err, result) {
}

Ele impõe uma maneira de lidar com erros em vez de declarar a variável de erro e permitir livremente o padrão err!=nil err==nil, o que dificulta a legibilidade, aumenta o risco de código propenso a erros no IMO:

func Foo() (err error) {
    var file, ferr = os.Open("file1.txt")
    if ferr == nil {
               defer file.Close()
        var parsed, perr = parseFile(file)
        if perr != nil {
            return
        }
        fmt.Printf("%s", parsed)
    }
    return nil
}

Com try(), o código é mais legível, consistente e seguro na minha opinião:

func Foo() (err error) {
    var file = try(os.Open("file.txt"))
        defer file.Close()
    var parsed = try(parseFile(file))
    fmt.Printf(parsed)
    return
}

Executei alguns experimentos semelhantes ao que @lpar fez em todos os repositórios Go não arquivados do Heroku (públicos e privados).

Os resultados estão nesta essência: https://gist.github.com/freeformz/55abbe5da61a28ab94dbb662bfc7f763

cc @davecheney

@ubikenobi Sua função mais segura ~is~ estava vazando.

Além disso, nunca vi um valor retornado após um erro. No entanto, eu poderia imaginar que faz sentido quando uma função é sobre o erro e os outros valores retornados não são contingentes ao erro em si (talvez levando a dois retornos de erro com o segundo "guardando" os valores anteriores).

Por último, embora não seja comum, err == nil fornece um teste legítimo para alguns retornos antecipados.

@Daved

Obrigado por apontar sobre o vazamento, esqueci de adicionar defer.Close() em ambos os exemplos. (atualizado agora).

Eu raramente vejo err retornar nessa ordem também, mas ainda é bom poder pegá-los em tempo de compilação se forem erros do que por design.

Eu vejo err==nil case como uma exceção do que uma norma na maioria dos casos. Pode ser útil em alguns casos, como você mencionou, mas o que eu não gosto é que os desenvolvedores escolham de forma inconsistente sem um motivo válido. Felizmente, em nossa base de código, a grande maioria das declarações são err!=nil, que podem facilmente se beneficiar da função try().

  • Corri tryhard em uma grande API Go que mantenho com uma equipe de quatro outros engenheiros em tempo integral. Em 45580 linhas de código Go, tryhard identificou 301 erros para reescrever (portanto, seria uma alteração de +301/-903), ou reescreveria cerca de 2% do código assumindo que cada erro leva aproximadamente 3 linhas. Levando em conta comentários, espaços em branco, importações, etc., isso parece substancial para mim.
  • Eu tenho usado a ferramenta de linha do tryhard para explorar como try mudaria meu trabalho, e subjetivamente isso flui muito bem para mim! O verbo tentar parece mais claro para mim que algo pode dar errado na função de chamada e o realiza de forma compacta. Estou muito acostumado a escrever if err != nil , e não me importo muito, mas também não me importaria de mudar. Escrever e refatorar a variável vazia que precede o erro (ou seja, fazer a fatia/mapa/variável vazia retornar) repetidamente é provavelmente mais tedioso do que o próprio err .
  • É um pouco difícil seguir todos os tópicos de discussão, mas estou curioso para saber o que isso significa para erros de encapsulamento. Seria bom se try fosse variável se você quisesse adicionar opcionalmente contexto como try(json.Unmarshal(b, &accountBalance), "failed to decode bank account info for user %s", user) . Edit: este ponto provavelmente fora do tópico; de olhar para não tentar reescrever, é aqui que isso acontece.
  • Eu realmente aprecio o pensamento e o cuidado que estão sendo colocados nisso! A compatibilidade com versões anteriores e a estabilidade são realmente importantes para nós e o esforço do Go 2 até o momento tem sido muito bom para manter os projetos. Obrigado!

Isso não deveria ser feito na fonte que foi examinada por Esquilos experientes para garantir que as substituições sejam racionais? Quanto dessa reescrita de "2%" deveria ter sido reescrita com manipulação explícita? Se não soubermos disso, a LOC continua sendo uma métrica relativamente inútil.

*É exatamente por isso que meu post esta manhã se concentrou em "modos" de tratamento de erros. É mais fácil e mais substantivo discutir os modos de manipulação de erros que try facilita e depois lutar com os perigos potenciais do código que provavelmente escreveremos do que executar um contador de linhas bastante arbitrário.

@kingishb Quantos pontos _try_ encontrados estão em funções públicas de pacotes não principais? Normalmente, funções públicas devem retornar erros nativos do pacote (ou seja, encapsulados ou decorados)....

@networkimprov Essa é uma fórmula excessivamente simplista para minhas sensibilidades. Onde isso soa verdadeiro é em termos de superfícies de API que retornam erros inspecionáveis. Normalmente, é apropriado adicionar contexto a uma mensagem de erro com base na relevância do contexto, não em sua posição na pilha de chamadas.

Muitos falsos positivos provavelmente estão passando nas métricas atuais. E as falhas que ocorrem devido a seguir as práticas sugeridas (https://blog.golang.org/errors-are-values)? try provavelmente reduziria o uso de tais práticas e, nesse sentido, elas são os principais alvos para substituição (provavelmente um dos únicos casos de uso realmente intrigantes para mim). Então, novamente, isso parece inútil para raspar a fonte existente sem muito mais diligência.

Obrigado @ubikenobi , @freeformz e @kingishb por coletar seus dados, muito apreciado! Como um aparte, se você executar tryhard com a opção -err="" if também tentará trabalhar com código onde a variável de erro é chamada de algo diferente de err (como e ). Isso pode gerar mais alguns casos, dependendo da base do código (mas também possivelmente aumentar a chance de falsos positivos).

@griesemer caso você esteja procurando mais pontos de dados. Corri tryhard em dois de nossos microsserviços, com estes resultados:

cloc v 1.82 / tryhard
13280 linhas de código Go / 148 identificadas para try (1%)

Outro serviço:
9768 linhas de código Go / 50 identificadas para try (0,5%)

Subsequentemente tryhard inspecionou um conjunto mais amplo de vários microsserviços:

314343 Linhas de código Go / 1563 identificadas para tentativa (0,5%)

Fazendo uma inspeção rápida. Os tipos de pacotes que try podem otimizar normalmente são adaptadores/empacotadores de serviço que retornam de forma transparente o erro (GRPC) retornado do serviço encapsulado.

Espero que isto ajude.

É uma ideia absolutamente ruim.

  • Quando err var aparece para defer ? Que tal "explícito melhor que implícito"?
  • Usamos uma regra simples: você deve encontrar rapidamente exatamente um lugar onde o erro retornou. Cada erro é envolvido com contexto para entender o que e onde está errado. defer criará muito código feio e difícil de entender.
  • @davecheney escreveu um ótimo post sobre erros e a proposta é totalmente contra tudo nesse post.
  • Por fim, se você usar os.Exit , seus erros serão desmarcados.

Acabei de executar tryhard em um pacote (com fornecedor) e ele relatou 2478 com a contagem de códigos caindo de 873934 para 851178 mas não tenho certeza como interpretar isso porque não sei quanto disso é devido ao over-wrapping (com o stdlib sem suporte para encapsulamento de erros de rastreamento de pilha) ou quanto desse código é sobre tratamento de erros.

O que eu sei, no entanto, é que só esta semana eu perdi uma quantidade embaraçosa de tempo devido a copiar-massas como if err != nil { return nil } e erros que parecem error: cannot process ....file: cannot parse ...file: cannot open ...file .

\ Eu não colocaria muito peso no número de votos, a menos que você pense que existem apenas cerca de 3.000 desenvolvedores Go por aí. A alta contagem de votos na outra não proposta se deve simplesmente ao fato de que a questão chegou ao topo do HN e do Reddit - a comunidade Go não é exatamente conhecida por sua falta de dogma e/ou negação, então não -deve-se ficar surpreso com a contagem de votos.

Eu também não levaria muito a sério as tentativas de apelar à autoridade, porque essas mesmas autoridades são conhecidas por rejeitar novas idéias e propostas mesmo depois que sua própria ignorância e/ou mal-entendido é apontada.
\

Executamos tryhard -err="" em nosso maior serviço (±163k linhas de código incluindo testes) - ele encontrou 566 ocorrências. Eu suspeito que seria ainda mais na prática, já que parte do código foi escrito com if err != nil em mente, então foi projetado em torno disso (o artigo "erros são valores" de Rob Pike sobre como evitar a repetição vem à mente).

@griesemer Adicionei um novo arquivo à essência. Foi gerado com -err="". Verifiquei no local e há algumas alterações. Também atualizei o tryhard esta manhã, então a versão mais recente também foi usada.

@griesemer Acho que tryhard seria mais útil se pudesse contar:

a) o número de sites de chamada que geram um erro
b) o número de manipuladores de declaração única if err != nil [&& ...] (candidatos a on err #32611)
c) o número daqueles que devolvem alguma coisa (candidatos a defer #32676)
d) o número daqueles que retornam err (candidatos a try() )
e) o número daqueles que estão em funções exportadas de pacotes não principais (provavelmente falso positivo)

Comparando o LoC total com instâncias de return err sorta falta contexto, IMO.

@networkimprov Concordo - sugestões semelhantes foram apresentadas antes. Vou tentar encontrar algum tempo nos próximos dias para melhorar isso.

Aqui estão as estatísticas de execução do tryhard em nossa base de código interna (somente nosso código, não dependências):

Antes de:

  • 882 arquivos .go
  • 352434 loc
  • 329909 local não vazio

Depois de tentar:

  • 2701 substituições (média 3.1 substituições/arquivo)
  • 345364 loc (-2,0%)
  • 322838 local não vazio (-2,1%)

Edit: Agora que @griesemer atualizou o tryhard para incluir estatísticas resumidas, aqui estão mais algumas:

  • 39,2% das declarações if são if <err> != nil
  • 69,6% destes são try candidatos

Examinando as substituições que o tryhard encontrou, certamente existem tipos de código em que o uso de try seria muito predominante, e outros tipos em que raramente seria usado.

Também notei alguns lugares que o tryhard não poderia transformar, mas se beneficiariam muito com o try. Por exemplo, aqui está algum código que temos para decodificar mensagens de acordo com um protocolo de fio simples (editado para simplicidade/claridade):

func (req *Request) Decode(r Reader) error {
    typ, err := readByte(r)
    if err != nil {
        return err
    }
    req.Type = typ
    req.Body, err = readString(r)
    if err != nil {
        return unexpected(err)
    }

    req.ID, err = readID(r)
    if err != nil {
        return unexpected(err)
    }
    n, err := binary.ReadUvarint(r)
    if err != nil {
        return unexpected(err)
    }
    req.SubIDs = make([]ID, n)
    for i := range req.SubIDs {
        req.SubIDs[i], err = readID(r)
        if err != nil {
            return unexpected(err)
        }
    }
    return nil
}

// unexpected turns any io.EOF into an io.ErrUnexpectedEOF.
func unexpected(err error) error {
    if err == io.EOF {
        return io.ErrUnexpectedEOF
    }
    return err
}

Sem try , nós apenas escrevemos unexpected nos pontos de retorno onde é necessário, pois não há grande melhoria ao manuseá-lo em um só lugar. No entanto, com try , podemos aplicar a transformação de erro unexpected com um adiamento e, em seguida, reduzir drasticamente o código, tornando-o mais claro e fácil de percorrer:

func (req *Request) Decode(r Reader) (err error) {
    defer func() { err = unexpected(err) }()

    req.Type = try(readByte(r))
    req.Body = try(readString(r))
    req.ID = try(readID(r))

    n := try(binary.ReadUvarint(r))
    req.SubIDs = make([]ID, n)
    for i := range req.SubIDs {
        req.SubIDs[i] = try(readID(r))
    }
    return nil
}

@cespare Fantástica reportagem!

O trecho totalmente reduzido é geralmente melhor, mas os parênteses são ainda piores do que eu esperava, e o try dentro do loop é tão ruim quanto eu esperava.

Uma palavra-chave é muito mais legível e é um pouco surreal que esse seja um ponto em que muitos outros diferem. O seguinte é legível e não me preocupa com sutilezas devido a apenas um valor ser retornado (embora ainda possa aparecer em funções mais longas e/ou com muito aninhamento):

func (req *Request) Decode(r Reader) (err error) {
    defer func() { err = wrapEOF(err) }()

    req.Type = try readByte(r)
    req.Body = try readString(r)
    req.ID = try readID(r)

    n := try binary.ReadUvarint(r)
    req.SubIDs = make([]ID, n)
    for i := range req.SubIDs {
        req.SubIDs[i], err = readID(r)
        try err
    }
    return nil
}

* Sendo justo sobre isso, o destaque de código ajudaria muito, mas isso parece um batom barato.

Você entende que a maior vantagem que você obtém em caso de código muito ruim?

Se você usar unexpected() ou retornar o erro como está, você não sabe nada sobre seu código e seu aplicativo.

try não pode ajudá-lo a escrever um código melhor, mas pode produzir mais código ruim.

@cespare Um decodificador também pode ser um struct com um tipo de erro dentro dele, com os métodos verificando err == nil antes de cada operação e retornando um booleano ok.

Como este é o processo que usamos para codecs, try é absolutamente inútil porque pode-se facilmente criar um idioma não mágico, mais curto e mais sucinto para lidar com erros para esse caso específico.

@makhov Por "código muito ruim", suponho que você queira dizer código que não envolve erros.

Se sim, então você pode pegar um código que se parece com isso:

a, b, c, err := someFn()
if err != nil {
  return ..., errors.Wrap(err, ...)
}

E transformá-lo em código semanticamente idêntico[1] que se parece com isso:

a, b, c, err := someFn()
try(errors.Wrap(err, ...))

A proposta não está dizendo que você deve usar defer para encapsulamento de erros, apenas explicando por que a palavra-chave handle da iteração anterior da proposta não é necessária, pois ela pode ser implementada em termos de defer sem nenhuma alteração de idioma.

(Seu outro comentário também parece ser baseado em exemplos ou pseudocódigo na proposta, em oposição ao núcleo do que está sendo proposto)

Eu executei tryhard na minha base de código com 54K LOC, 1116 instâncias foram encontradas.
Eu vi o diff, e devo dizer que tenho muito pouca construção que se beneficiaria muito de tentar, porque quase todo o meu uso do tipo de construção if err != nil é um bloco simples de nível único que apenas retorna o erro com contexto adicionado. Acho que encontrei apenas algumas instâncias em que try realmente alteraria a construção do código.

Em outras palavras, minha opinião é que try em sua forma atual me dá:

  • menos digitação (uma redução de aproximadamente 30 caracteres por ocorrência, indicada pelos "**" abaixo)
-       **if err := **json.NewEncoder(&buf).Encode(in)**; err != nil {**
-               **return err**
-       **}**
+       try(json.NewEncoder(&buf).Encode(in))

enquanto ele apresenta esses problemas para mim:

  • Mais uma maneira de lidar com erros
  • indicação visual ausente para divisão do caminho de execução

Como escrevi anteriormente neste tópico, posso viver com try , mas depois de experimentá-lo no meu código, acho que pessoalmente prefiro não ter isso introduzido na linguagem. meus $.02

recurso inútil, economiza digitação, mas não é grande coisa.
Prefiro escolher o caminho antigo.
escreva mais manipulador de erros para programar fácil de solucionar problemas.

Apenas alguns pensamentos...

Esse idioma é útil em go, mas é apenas isso: um idioma que você deve
ensinar aos recém-chegados. Um programador novo tem que aprender isso, caso contrário ele
pode até ser tentado a refatorar o tratamento de erros "oculto". Também o
código não é mais curto usando esse idioma (muito pelo contrário), a menos que você esqueça
para contar os métodos.

Agora vamos imaginar que try está implementado, quão útil será essa expressão para
esse caso de uso? Considerando:

  • Try mantém a implementação mais próxima em vez de se espalhar pelos métodos.
  • Os programadores lerão e escreverão código com try com muito mais frequência do que isso
    idioma específico (que raramente é usado, exceto para todas as tarefas específicas). UMA
    idioma mais usado torna-se mais natural e legível, a menos que haja uma clara
    desvantagem, o que claramente não é o caso aqui se compararmos ambos com um
    mente aberta.

Então talvez esse idioma seja considerado substituído por try.

Em ter, 2 de julho de 2019 18:06, as [email protected] escreveu:

@cespare https://github.com/cespare Um decodificador também pode ser um struct com
um tipo de erro dentro dele, com os métodos verificando err == nil antes
cada operação e retornando um booleano ok.

Porque este é o processo que usamos para codecs, tentar é absolutamente inútil
porque pode-se facilmente fazer um idioma não mágico, mais curto e mais sucinto
para tratamento de erros para este caso específico.


Você está recebendo isso porque foi mencionado.
Responda a este e-mail diretamente, visualize-o no GitHub
https://github.com/golang/go/issues/32437?email_source=notifications&email_token=AAT5WM3YDDRZXVXOLDQXKH3P5O7L5A5CNFSM4HTGCZ72YY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWS5GODZ,CRHXA#issuecomment-
ou silenciar o thread
https://github.com/notifications/unsubscribe-auth/AAT5WMYXLLO74CIM6H4Y2RLP5O7L5ANCNFSM4HTGCZ7Q
.

Verbosidade no tratamento de erros é uma coisa boa na minha opinião. Em outras palavras, não vejo um caso de uso forte para tentar.

Estou aberto a essa ideia, mas acho que deveria incluir algum mecanismo para determinar onde ocorreu a divisão da execução. Xerror/Is seria bom para alguns casos (por exemplo, se o erro for um ErrNotExists, você pode inferir que aconteceu em um Open), mas para outros - incluindo erros herdados em bibliotecas - não há substituto.

Um similar para recuperação integrado poderia ser incluído para fornecer informações de contexto sobre onde o fluxo de controle foi alterado? Possivelmente, para mantê-lo barato, uma função separada usada no lugar de try().

Ou talvez um debug.Try com a mesma sintaxe que try() mas com as informações de depuração adicionadas? Dessa forma, try() pode ser tão útil com código usando erros antigos, sem forçá-lo a recorrer ao tratamento de erros antigo.

A alternativa seria que try() envolvesse e adicionasse contexto, mas na maioria dos casos isso reduziria o desempenho sem nenhum propósito, daí a sugestão de funções adicionais.

Edit: depois de escrever isso, ocorreu-me que o compilador poderia determinar qual variante de try() usar com base em se alguma instrução defer usa essa função de fornecimento de contexto semelhante a "recover". Não tenho certeza sobre a complexidade disso embora

@lestrat Eu não diria minha opinião neste comentário, mas se houver uma chance de explicar como "tentar" pode afetar o bem para nós, seria que dois ou mais tokens podem ser escritos na instrução if. Portanto, se você escrever 200 condições em uma instrução if, poderá reduzir muitas linhas.

if try(foo()) == 1 && try(bar()) == 2 {
  // err
}
n1, err := foo()
if err != nil {
  // err
}
n2, err := bar()
if err != nil {
  // err
}
if n1 == 1 && n2 == 2 {
  // err
}

@mattn essa é a coisa, _teoricamente_ você está absolutamente correto. Tenho certeza de que podemos criar casos em que try se encaixaria maravilhosamente.

Acabei de fornecer dados que, na vida real, pelo menos _eu_ não encontrei quase nenhuma ocorrência de tais construções que se beneficiariam da tradução para tentar em _meu código_.

É possível que eu escreva código de maneira diferente do resto do mundo, mas achei que valeria a pena alguém comentar que, com base na tradução de PoC, alguns de nós não ganham muito com a introdução de try no idioma.

Como um aparte, eu ainda não usaria seu estilo no meu código. eu escreveria como

n1 := try(foo())
n2 := try(bar())
if n1 == 1 && n2 == 2 {
   return errors.New(`boo`)
}

então eu ainda estaria economizando aproximadamente a mesma quantidade de digitação por instância daqueles n1/n2/....n(n)s

Por que ter uma palavra-chave (ou função)?

Se o contexto de chamada espera valores n+1, então tudo é como antes.

Se o contexto de chamada espera n valores, o comportamento try é ativado.

(Isso é particularmente útil no caso n = 1, de onde vem toda a confusão terrível.)

Meu ide já destaca valores de retorno ignorados; seria trivial oferecer dicas visuais para isso, se necessário.

@balasanjay Sim, erros de encapsulamento são o caso. Mas também temos registro, reações diferentes em erros diferentes (o que devemos fazer com variáveis ​​de erro, por exemplo sql.NoRows ?), código legível e assim por diante. Escrevemos defer f.Close() imediatamente após abrir um arquivo para deixar claro para os leitores. Verificamos os erros imediatamente pelo mesmo motivo.

Mais importante ainda, esta proposta viola a regra “ erros são valores ”. É assim que o Go é projetado. E essa proposta vai diretamente contra a regra.

try(errors.Wrap(err, ...)) é outro pedaço de código terrível porque contradiz tanto essa proposta quanto o design atual do Go.

Eu tendo a concordar com @lestrat
Como geralmente foo() e bar() são na verdade:
SomeFunctionWithGoodName(Parm1, Parms2)

então a sintaxe @mattn sugerida seria na verdade:

if  try(SomeFunctionWithGoodName(Parm1, Parms2)) == 1 && try(package.SomeOtherFunction(Parm1, Parms2,Parm3))) == 2 {


} 

A legibilidade geralmente será uma bagunça.

considere um valor de retorno:
someRetVal, err := SomeFunctionWithGoodName(Parm1, Parms2)
está em uso com mais frequência do que apenas comparar com um const como 1 ou 2 e não fica pior, mas requer um recurso de atribuição dupla:

if  a := try(SomeFunctionWithGoodName(Parm1, Parms2)) && b:= try(package.SomeOtherFunction(Parm1, Parms2,Parm3))) {


} 

Quanto a todos os casos de uso ("quanto o tryhard me ajudou"):

  1. Eu acho que você veria uma grande diferença entre bibliotecas e executáveis, seria interessante ver de outras pessoas se elas também obtivessem essa diferença
  2. minha sugestão é não comparar o %save em linhas no código, mas sim o número de erros no código versus o número refatorado.
    (minha opinião sobre isso foi
    $find /path/to/repo -name '*.go' -exec cat {} \; | grep "err :=" | wc -l
    )

@makhov

esta proposta viola a regra "erros são valores"

Na verdade. Erros ainda são valores nesta proposta. try() está apenas simplificando o fluxo de controle sendo um atalho para if err != nil { return ...,err } . O tipo error já é de alguma forma "especial" por ser um tipo de interface embutido. Esta proposta é apenas adicionar uma função interna que complementa o tipo error . Não há nada de extraordinário aqui.

@ngrilly Simplificando? Quão?

func (req *Request) Decode(r Reader) error {
    defer func() { err = unexpected(err) }()

    req.Type = try(readByte(r))
    req.Body = try(readString(r))
    req.ID = try(readID(r))

    n := try(binary.ReadUvarint(r))
    req.SubIDs = make([]ID, n)
    for i := range req.SubIDs {
        req.SubIDs[i] = try(readID(r))
    }
    return nil
}

Como devo entender que o erro foi retornado dentro do loop? Por que é atribuído a err var, não a foo ?
É mais simples mantê-lo em mente e não mantê-lo no código?

@daved

os parênteses são ainda piores do que eu esperava [...] Uma palavra-chave é muito mais legível e é um pouco surreal que esse seja um ponto em que muitos outros divergem.

Escolher entre uma palavra-chave e uma função interna é principalmente uma questão estética e sintática. Sinceramente, não entendo por que isso é tão importante para seus olhos.

PS: A função integrada tem a vantagem de ser compatível com versões anteriores, ser extensível com outros parâmetros no futuro e evitar os problemas de precedência do operador. A palavra-chave tem a vantagem de... ser uma palavra-chave, e sinalizar que try é "especial".

@makhov

Simplificando?

OK. A palavra certa é "encurtar".

try() encurta nosso código substituindo o padrão if err != nil { return ..., err } por uma chamada para a função interna try() .

É exatamente como quando você identifica um padrão recorrente em seu código e o extrai em uma nova função.

Já temos funções embutidas como append(), que poderíamos substituir escrevendo o código "in extenso" sempre que precisarmos anexar algo a uma fatia. Mas porque fazemos isso o tempo todo, foi integrado na linguagem. try() não é diferente.

Como devo entender que o erro foi retornado dentro do loop?

O try() no loop age exatamente como o try() no resto da função, fora do loop. Se readID() retornar um erro, então a função retornará o erro (depois de ter decorado o if).

Por que é atribuído a err var, não a foo?

Não vejo nenhuma variável foo no seu exemplo de código...

@makhov Acho que o trecho está incompleto, pois o erro retornado não é nomeado (reli rapidamente a proposta, mas não consegui ver se o nome da variável err é o nome padrão, se nenhum estiver definido).

Ter que renomear os parâmetros retornados é um dos pontos que as pessoas que rejeitam essa proposta não gostam.

func (req *Request) Decode(r Reader) (err error) {
    defer func() { err = unexpected(err) }()

    req.Type = try(readByte(r))
    req.Body = try(readString(r))
    req.ID = try(readID(r))

    n := try(binary.ReadUvarint(r))
    req.SubIDs = make([]ID, n)
    for i := range req.SubIDs {
        req.SubIDs[i] = try(readID(r))
    }
    return nil
}

@pierrec talvez possamos ter uma função como recover() para recuperar o erro se não estiver no parâmetro nomeado?
defer func() {err = unexpected(tryError())}

@makhov Você pode torná-lo mais explícito:

func (req *Request) Decode(r Reader) error {
    req.Type, err := readByte(r)
        try(err) // or add annotation like try(annotate(err, ...))
    req.Body, err := readString(r)
        try(err)
    req.ID, err := readID(r)
        try(err)

    n, err := binary.ReadUvarint(r)
        try(err)
    req.SubIDs = make([]ID, n)
    for i := range req.SubIDs {
        req.SubIDs[i], err := readID(r)
                try(err)
    }
    return nil
}

@pierrec Ok, vamos mudar:

func (req *Request) Decode(r Reader) error {
        var errOne, errTwo error
    defer func() { err = unexpected(???) }()

    req.Type = try(readByte(r))
    …
}

@reusee E por que é melhor que isso?

func (req *Request) Decode(r Reader) error {
    req.Type, err := readByte(r)
        if err != nil { return err }
        …
}

Em que momento todos nós decidimos que a brevidade é melhor do que a legibilidade?

@flibustenet Obrigado por entender o problema. Parece muito melhor, mas ainda não tenho certeza de que precisamos de compatibilidade com versões anteriores para essa pequena "melhoria". É muito chato se eu tiver um aplicativo que para de compilar na nova versão do Go:

package main

func main() {
    // ...
   try("a", "b")
    // ...
}

func try(a, b string) {
    // ...
}

@makhov Concordo que isso precisa ser esclarecido: o compilador comete erros quando não consegue descobrir a variável? Eu pensei que sim.
Talvez a proposta precise esclarecer esse ponto? Ou eu perdi isso no documento?

@flibustenet sim, essa é uma maneira de usar try (), mas parece-me que não é uma maneira idiomática de usar try.

@cespare Pelo que você escreveu parece que a modificação dos valores de retorno em defer é um recurso de try mas você já pode fazer isso.

https://play.golang.com/p/ZMauFmt9ezJ

(Desculpe se interpretei mal o que você disse)

@jan-g Sobre https://github.com/golang/go/issues/32437#issuecomment -507961463: A ideia de manipular erros de forma invisível surgiu várias vezes. O problema com essa abordagem implícita é que adicionar um retorno de erro a uma função chamada pode fazer com que a função de chamada se comporte de forma silenciosa e invisível de forma diferente. Nós absolutamente queremos ser explícitos quando os erros são verificados. Uma abordagem implícita também vai contra o princípio geral em Go de que tudo é explícito.

@griesemer

Eu tentei tryhand em um dos meus projetos (https://github.com/komuw/meli) e não fez nenhuma alteração.

gobin github.com/griesemer/tryhard
     Installed github.com/griesemer/[email protected] to ~/go/bin/tryhard

```bash
~/go/bin/tryhard -err "" -r
0

most of my err handling looks like;
```Go
import "github.com/pkg/errors"

func CreateDockerVolume(volName string) (string, error) {
    volume, err := VolumeCreate(volName)
    if err != nil {
        return "", errors.Wrapf(err, "unable to create docker volume %v", volName)
    }
    return volume.Name, nil
}

@komuw Antes de tudo, certifique-se de fornecer um argumento de nome de arquivo ou diretório para tryhard , como em

tryhard -err="" -r .  // <<< note the dot
tryhard -err="" -r filename

Além disso, o código como você tem em seu comentário não será reescrito, pois trata de erros específicos no bloco if . Por favor, leia a documentação de tryhard para saber quando se aplica. Obrigado.

func CreateDockerVolume(volName string) (string, error) {
    volume, err := VolumeCreate(volName)
    if err != nil {
        return "", errors.Wrapf(err, "unable to create docker volume %v", volName)
    }
    return volume.Name, nil
}

Este é um exemplo um tanto interessante. Minha primeira reação ao olhar para ele foi perguntar se isso produziria strings de erro de gagueira como:

unable to create docker volume: VolumeName: could not create volume VolumeName: actual problem

A resposta é que não, porque a função VolumeCreate (de um repositório diferente) é:

func (cli *Client) VolumeCreate(ctx context.Context, options volumetypes.VolumeCreateBody) (types.Volume, error) {
        var volume types.Volume
        resp, err := cli.post(ctx, "/volumes/create", nil, options, nil)
        defer ensureReaderClosed(resp)
        if err != nil {
                return volume, err
        }
        err = json.NewDecoder(resp.body).Decode(&volume)
        return volume, err
}

Em outras palavras, a decoração adicional no erro é útil porque a função subjacente não decorou seu erro. Essa função subjacente pode ser ligeiramente simplificada com try .

Talvez a função VolumeCreate realmente devesse estar decorando seus erros. Nesse caso, no entanto, não está claro para mim que a função CreateDockerVolume deva adicionar uma decoração adicional, já que não há novas informações a serem fornecidas.

@neild
Mesmo que VolumeCreate decorasse os erros, ainda precisaríamos CreateDockerVolume para adicionar sua decoração, pois VolumeCreate pode ser chamado de várias outras funções, e se algo falhar (e esperamos logado) você gostaria de saber o que falhou - que neste caso é CreateDockerVolume ,
No entanto, Considerando VolumeCreate faz parte da interface APIclient.

O mesmo vale para outras bibliotecas - os.Open pode muito bem decorar o nome do arquivo, motivo do erro etc, mas
func ReadConfigFile(...
func WriteDataFile(...
etc - chamar os.Open são as partes com falha reais que você gostaria de ver para registrar, rastrear e lidar com seus erros - especialmente, mas não apenas no ambiente de produção.

@neilda obrigado.

Não quero estragar este tópico, mas...

Talvez a função VolumeCreate realmente devesse estar decorando seus erros.
Nesse caso, no entanto, não está claro para mim que o
Função CreateDockerVolume
deve adicionar decoração adicional,

O problema é que, como o autor da função CreateDockerVolume eu posso não
saber se o autor de VolumeCreate decorou seus erros, então eu
não precisa decorar o meu.
E mesmo se eu soubesse que eles tinham, eles poderiam decidir desdecorar seus
funcionar em uma versão posterior. E como essa mudança não está mudando api, eles
lançaria como um patch/versão secundária e agora minha função que era
dependente de sua função ter erros decorados não tem todas as
informações que eu preciso.
Então geralmente eu me pego decorando/embrulhando mesmo que a biblioteca que eu estou
a chamada já foi encerrada.

Eu tive um pensamento enquanto conversava sobre try com um colega de trabalho. Talvez try só deva ser habilitado para a biblioteca padrão na versão 1.14. @crawshaw e @jimmyfrasche fizeram um tour rápido por alguns casos e deram alguma perspectiva, mas, na verdade, reescrever o código da biblioteca padrão usando try o máximo possível seria valioso.

Isso dá tempo à equipe Go para reescrever um projeto não trivial usando-o, e a comunidade pode ter um relato de experiência sobre como isso funciona. Nós saberíamos com que frequência ele é usado, com que frequência ele precisa ser emparelhado com um defer , se ele altera a legibilidade do código, quão útil tryhard é, etc.

É um pouco contra o espírito da biblioteca padrão, permitindo que ela use algo que o código Go normal não pode, mas nos dá um playground para ver como try afeta uma base de código existente.

Desculpas se alguém já pensou nisso; Passei pelas várias discussões e não vi uma proposta semelhante.

@jonbodner https://go-review.googlesource.com/c/go/+/182717 dá uma boa ideia de como isso pode ser.

E esqueci de dizer: participei da sua pesquisa e votei por um melhor tratamento de erros, não isso.

Eu quis dizer que eu gostaria de ver mais rigoroso impossível esquecer o processamento de erros.

@jonbodner https://go-review.googlesource.com/c/go/+/182717 dá uma boa ideia de como isso pode ser.

Resumindo:

  1. 1 linha substitui universalmente 4 linhas (2 linhas para quem usa if ... { return err } )
  2. A avaliação do(s) resultado(s) retornado(s) pode ser otimizada - apenas no caminho da falha, no entanto.

Cerca de 6.000 substituições no total do que parece ser apenas uma mudança cosmética: não exporá os erros existentes, talvez não introduzirá novos (corrija-me se estiver errado sobre qualquer um).

Eu, na qualidade de mantenedor, me incomodaria em fazer algo assim com meu próprio código? Não, a menos que eu mesmo escreva a ferramenta de substituição. O que torna tudo certo para o repositório golang/go .

PS Um aviso interessante em CL:

... Some transformations may be incorrect due to the limitations of the tool (see https://github.com/griesemer/tryhard)...

Como xerrors , que tal dar o primeiro passo para usá-lo como um pacote de terceiros?

Por exemplo, tente usar o pacote abaixo.

https://github.com/junpayment/gotry

  • Pode ser curto para o seu caso de uso porque eu o fiz.

No entanto, acho que tentar em si é uma ótima ideia, então acho que também existe uma abordagem que realmente a usa com menos influência.

===

Como um aparte, há duas coisas que estou preocupado em tentar.

1. Há uma opinião de que a linha pode ser omitida, mas parece que não há consideração da cláusula defer(ou handler).

Por exemplo, quando o tratamento de erros é detalhado.

foo, err: = Foo ()
if err! = nil {
  if err.Error () = "AAA" {
    some action for AAA
  } else if err.Error () = "BBB" {
    some action for BBB
  } else if err.Error () = "CCC" {
    some action for CCC
  } else {
    return err
  }
}

Se você simplesmente substituir isso por tentar, será o seguinte.

handler: = func (err error) {
  if err.Error () = "AAA" {
    some action for AAA
  } else if err.Error () = "BBB" {
    some action for BBB
  } else if err.Error () = "CCC" {
    some action for CCC
  } else {
    return err
  }
}
foo: = try (Foo (), handler)

2.Pode haver outros pacotes ruins que acidentalmente implementaram a interface de erro.

type Bad struct {}
func (bad * Bad) Error () {
  return "i really do not intend to be an error"
}

@junpayment Obrigado pelo seu pacote gotry - acho que é uma maneira de ter uma ideia de try , mas será um pouco chato ter que digitar todos os Try resulta de um interface{} em uso real.

Sobre suas duas perguntas:
1) Não tenho certeza de onde você quer chegar com isso. Você está sugerindo que try deve aceitar um manipulador como no seu exemplo? (e como tínhamos em uma versão interna anterior de try ?)
2) Não estou muito preocupado com funções que implementam acidentalmente a interface de erro. Este problema não é novo e não parece ter causado problemas sérios até onde sabemos.

@jonbodner https://go-review.googlesource.com/c/go/+/182717 dá uma boa ideia de como isso pode ser.

Obrigado por fazer este exercício. No entanto, isso confirma para mim o que eu suspeitava, o próprio código-fonte go tem muitos lugares onde try() seria útil porque o erro é apenas repassado. No entanto, como posso ver nos experimentos com tryhard que outros e eu enviamos acima, para muitas outras bases de código try() não seria muito útil porque em erros de código de aplicativo tendem a ser realmente tratados, não acabou de passar.

Acho que isso é algo que os designers de Go devem ter em mente, o compilador go e o tempo de execução são um código Go "único", diferente do código do aplicativo Go. Portanto, acho que try() deve ser aprimorado para também ser útil em outros casos em que o erro realmente precisa ser tratado e onde o tratamento de erros com uma instrução defer não é realmente desejável.

@griesemer

será um pouco chato ter que declarar todos os resultados do Try de uma interface{} em uso real.

Você está certo. Esse método requer que o chamador converta o tipo.

Eu não tenho certeza onde você está indo com isso. Você está sugerindo que o try deve aceitar um manipulador como no seu exemplo? (e como tínhamos em uma versão interna anterior do try?)

Eu cometi um erro. Deveria ter sido explicado usando defer em vez de handler. Eu sinto Muito.

O que eu queria dizer é que há um caso em que não contribui para a quantidade de código, pois o processo de tratamento de erros omitido precisa ser descrito no adiamento de qualquer maneira.

Espera-se que o impacto seja mais pronunciado quando você deseja lidar com erros em detalhes.

Assim, ao invés de reduzir o número de linhas de código, podemos entender a proposta, que organiza os locais de tratamento de erros.

Não estou muito preocupado com funções que implementam acidentalmente a interface de erro. Este problema não é novo e não parece ter causado problemas sérios até onde sabemos.

Exatamente é caso raro.

@beoran fiz algumas análises iniciais do Go Corpus (https://github.com/rsc/corpus). Acredito que tryhard em seu estado atual poderia eliminar 41,7% de todos os cheques err != nil no corpus. Se eu excluir o padrão "_test.go", esse número sobe para 51,1% ( tryhard só opera em funções que retornam erros, e tende a não encontrar muitos desses em testes). Aviso, leve esses números com um grão de sal, eu tenho o denominador (ou seja, o número de lugares no código que realizamos verificações err != nil ) usando uma versão hackeada de tryhard , e idealmente esperaríamos até que tryhard relatasse essas estatísticas.

Além disso, se tryhard se tornasse sensível ao tipo, teoricamente poderia realizar transformações como esta:

// Before.
a, err := foo()
if err != nil {
  return 0, nil, errors.Wrapf(err, "some message %v", b)
}

// After.
a, err := foo()
try(errors.Wrapf(err, "some message %v", b))

Isso tira proveito do comportamento errors.Wrap de retornar nil quando o argumento de erro passado é nil . (github.com/pkg/errors também não é único a esse respeito, a biblioteca interna que eu uso para fazer o agrupamento de erros também preserva os erros nil e também funcionaria com esse padrão, assim como a maioria das bibliotecas de tratamento de erros post- try , imagino). A nova geração de bibliotecas de suporte provavelmente também nomearia esses auxiliares de propagação de forma ligeiramente diferente.

Dado que isso se aplicaria a 50% das verificações de err != nil sem teste, antes de qualquer evolução da biblioteca para suportar o padrão, não parece que o compilador Go e o tempo de execução sejam exclusivos, como você sugere .

Sobre o exemplo com CreateDockerVolume https://github.com/golang/go/issues/32437#issuecomment -508199875
Eu encontrei exatamente o mesmo tipo de uso. Na lib eu envolvo o erro com o contexto em cada erro, no uso da lib eu gostaria de usar try e adicionar contexto em defer para toda a função.

Eu tentei imitar isso adicionando uma função de manipulador de erros no início, está funcionando bem:

func MyLib() error {
    return errors.New("Error from my lib")
}
func MyOtherLib() error {
    return errors.New("Error from my otherLib")
}

func Caller(a, b int) error {
    eh := func(err error) error {
        return fmt.Errorf("From Caller with %d and %d i found this error: %v", a, b, err)
    }

    err := MyLib()
    if err != nil {
        return eh(err)
    }

    err = MyOtherLib()
    if err != nil {
        return eh(err)
    }

    return nil
}

Isso ficará bem e idiomático com try+defer

func Caller(a, b int) (err error) {
    defer fmt.Errorf("From Caller with %d and %d i found this error: %v", a, b, &err)

    try(MyLib())
    try(MyOtherLib())

    return nil
}

@griesemer

O documento de design atualmente tem as seguintes declarações:

Se a função delimitadora declarar outros parâmetros de resultado nomeados, esses parâmetros de resultado manterão o valor que tiverem. Se a função declarar outros parâmetros de resultado sem nome, eles assumem seus valores zero correspondentes (o que é o mesmo que manter o valor que já possuem).

Isso implica que este programa imprimiria 1, em vez de 0: https://play.golang.org/p/KenN56iNVg7.

Como me foi apontado no Twitter, isso faz com que try se comporte como um retorno nu, onde os valores retornados estão implícitos; para descobrir quais valores reais estão sendo retornados, pode ser necessário examinar o código a uma distância significativa da chamada para o próprio try .

Dado que essa propriedade de retornos nu (não-localidade) geralmente não é apreciada, o que você acha de ter try sempre retornando os valores zero dos argumentos sem erro (se retornar)?

Algumas considerações:

Isso pode tornar alguns padrões que envolvem o uso de valores de retorno nomeados incapazes de usar try . Por exemplo, para implementações de io.Writer , que precisam retornar uma contagem de bytes gravados, mesmo na situação de gravação parcial. Dito isto, parece que try é propenso a erros neste caso de qualquer maneira (por exemplo n += try(wrappedWriter.Write(...)) não define n para o número correto no caso de um retorno de erro). Parece bom para mim que try será inutilizado para esses tipos de casos de uso, pois cenários em que precisamos de valores e um erro são bastante raros, na minha experiência.

Se houver uma função com muitos usos de try , isso pode levar ao inchaço do código, onde há muitos lugares em uma função que precisam zerar as variáveis ​​de saída. Primeiro, o compilador é muito bom em otimizar gravações desnecessárias hoje em dia. E segundo, se for necessário, parece uma otimização direta ter todos os blocos $ try -gerados goto em um rótulo comum compartilhado em toda a função, que zera os valores de saída sem erro.

Além disso, como eu tenho certeza que você está ciente, tryhard já está implementado desta forma, então como um benefício colateral isso retroativamente tornará tryhard mais correto.

@jonbodner https://go-review.googlesource.com/c/go/+/182717 dá uma boa ideia de como isso pode ser.

Obrigado por fazer este exercício. No entanto, isso confirma para mim o que eu suspeitava, o próprio código-fonte go tem muitos lugares onde try() seria útil porque o erro é apenas repassado. No entanto, como posso ver nos experimentos com tryhard que outros e eu enviamos acima, para muitas outras bases de código try() não seria muito útil porque em erros de código de aplicativo tendem a ser realmente tratados, não acabou de passar.

Eu interpretaria isso de forma diferente.

Nós não tivemos genéricos, então será difícil encontrar código na natureza que se beneficie diretamente de genéricos baseados em código escrito. Isso não significa que os genéricos não seriam úteis.

Para mim, existem 2 padrões que usei no código para tratamento de erros

  1. use panics dentro do pacote, e recupere o panic e retorne um resultado de erro nos poucos métodos exportados
  2. usar seletivamente um manipulador adiado em alguns métodos para que eu possa decorar erros com informações de PC de número de linha/arquivo de pilha rica e mais contexto

Esses padrões não são comuns, mas funcionam. 1) é usado na biblioteca padrão em suas funções não exportadas e 2) é usado extensivamente em minha base de código nos últimos anos porque achei uma boa maneira de usar os recursos ortogonais para fazer decoração de erros simplificada, e a proposta recomenda e abençoou a abordagem. O fato de não serem difundidos não significa que não sejam bons. Mas, como em tudo, as diretrizes da equipe do Go recomendando isso farão com que sejam mais usadas na prática, no futuro .

Um ponto final de observação é que erros de decoração em cada linha do seu código podem ser um pouco demais. Haverá alguns lugares onde faz sentido decorar erros e alguns lugares onde não faz sentido. Como não tínhamos grandes diretrizes antes, as pessoas decidiram que fazia sentido sempre decorar os erros. Mas pode não agregar muito valor decorar sempre cada vez que um arquivo não for aberto, pois pode ser suficiente dentro do pacote apenas ter um erro como "não foi possível abrir o arquivo: conf.json", em oposição a: "incapaz para obter o nome de usuário: não foi possível obter a conexão do banco de dados: não foi possível carregar o arquivo do sistema: não foi possível abrir o arquivo: conf.json".

Com a combinação dos valores de erro e o tratamento conciso de erros, agora estamos obtendo melhores diretrizes sobre como lidar com erros. A preferência parece ser:

  • um erro será simples, por exemplo, "não foi possível abrir o arquivo: conf.json"
  • um quadro de erro pode ser anexado que inclui o contexto: GetUserName --> GetConnection --> LoadSystemFile.
  • Se adicionar ao contexto, você pode agrupar esse erro um pouco, por exemplo, MyAppError{error}

Eu costumo sentir que continuamos ignorando os objetivos da proposta de tentativa e as coisas de alto nível que ela tenta resolver:

  1. reduza o clichê de if err != nil { return err } para lugares onde faz sentido propagar o erro para cima para que ele seja tratado mais acima na pilha
  2. Permitir o uso simplificado de valores de retorno onde err == nil
  3. permitir que a solução seja estendida posteriormente para permitir, por exemplo, mais decoração de erros no site, pular para o manipulador de erros, usar goto em vez de semântica de retorno etc.
  4. Permita que o tratamento de erros não atrapalhe a lógica da base de código, ou seja, coloque-o de lado um pouco com um tipo de manipulador de erros.

Muitas pessoas ainda têm 1). Muitas pessoas trabalharam em torno de 1) porque não existiam diretrizes melhores antes. Mas isso não significa que, depois de começar a usá-lo, sua reação negativa não mudaria para se tornar mais positiva.

Muitas pessoas podem usar 2). Pode haver discordância sobre quanto, mas dei um exemplo onde isso torna meu código muito mais fácil.

var u user = try(db.LoadUser(try(strconv.ParseInt(stringId)))

Em java onde as exceções são a norma, teríamos:

User u = db.LoadUser(Integer.parseInt(stringId)))

Ninguém iria olhar para este código e dizer que temos que fazer isso em 2 linhas, ou seja.

int id = Integer.parseInt(stringId)
User u = db.LoadUser(id))

Não deveríamos ter que fazer isso aqui, sob a diretriz de que try NÃO DEVE ser chamado inline e DEVE estar sempre em sua própria linha .

Além disso, hoje, a maioria dos códigos fará coisas como:

var u user
var err error
var id int
id, err = strconv.ParseInt(stringId)
if err != nil {
  return u, errors.Wrap("cannot load userid from string: %s: %v", stringId, err)
}
u, err = db.LoadUser(id)
if err != nil {
  return u, errors.Wrap("cannot load user given user id: %d: %v", id, err)
}
// now work with u

Agora, alguém lendo isso precisa analisar essas 10 linhas, que em java seriam 1 linha e que poderiam ser 1 linha com a proposta aqui. Eu visualmente tenho que tentar mentalmente ver quais linhas aqui são realmente pertinentes quando leio este código. O clichê torna esse código mais difícil de ler e grok.

Lembro-me da minha vida passada trabalhando em/com programação orientada a aspectos em java. Lá, o objetivo era

Isso permite que comportamentos que não são centrais para a lógica de negócios (como registro em log) sejam adicionados a um programa sem sobrecarregar o código, o núcleo da funcionalidade. (citando da wikipedia https://en.wikipedia.org/wiki/Aspect-oriented_programming ).
O tratamento de erros não é central para a lógica de negócios, mas é central para a correção. A ideia é a mesma - não devemos desordenar nosso código com coisas que não são centrais para a lógica de negócios porque " mas o tratamento de erros é muito importante ". Sim, é, e sim, podemos colocá-lo de lado.

Em relação a 4), muitas propostas sugeriram manipuladores de erros, que são códigos ao lado que tratam de erros, mas não sobrecarregam a lógica de negócios. A proposta inicial tem a palavra-chave handle para ela, e as pessoas sugeriram outras coisas. Esta proposta diz que podemos alavancar o mecanismo de defer para isso, e apenas tornar mais rápido o que era seu calcanhar de Aquiles antes. Eu sei - eu fiz barulho sobre o desempenho do mecanismo de adiamento muitas vezes para a equipe de partida.

Observe que tryhard não sinalizará este código como algo que pode ser simplificado. Mas com try e novas diretrizes, as pessoas podem querer simplificar esse código para um 1-liner e deixar o Error Frame capturar o contexto necessário.

O contexto, que tem sido muito bem usado em linguagens baseadas em exceção, irá capturar que alguém tentou um erro ao carregar um usuário porque o ID do usuário não existia, ou porque o stringId não estava em um formato que um ID inteiro pudesse ser analisado a partir dele.

Combine isso com o Error Formatter, e agora podemos inspecionar ricamente o quadro de erro e o erro em si e formatar a mensagem bem para os usuários, sem o estilo a: b: c: d: e: underlying error difícil de ler que muitas pessoas fizeram e que não temos tinha ótimas orientações para.

Lembre-se de que todas essas propostas juntas nos dão a solução que queremos: tratamento de erros conciso sem clichês desnecessários, ao mesmo tempo em que oferece melhores diagnósticos e melhor formatação de erros para os usuários. Esses são conceitos ortogonais, mas juntos se tornam extremamente poderosos.

Finalmente, dado 3) acima, é difícil usar uma palavra-chave para resolver isso. Por definição, uma palavra-chave não permite que a extensão no futuro passe um manipulador por nome, ou permita a decoração de erro no local, ou suporte a semântica goto (em vez de semântica de retorno). Com uma palavra-chave, temos que ter a solução completa em mente primeiro. E uma palavra-chave não é compatível com versões anteriores. A equipe go afirmou quando Go 2 estava começando, que eles queriam tentar manter a compatibilidade com versões anteriores o máximo possível. A função try mantém isso, e se virmos mais tarde que não há extensão necessária, um simples gofix pode facilmente modificar o código para alterar a função try para uma palavra-chave.

Meus 2 centavos novamente!

Em 04/07/19, Sanjay Menakuru [email protected] escreveu:

@griesemer

[ ... ]
Como foi apontado para mim no Twitter, isso faz try se comportar como um nu
return, onde os valores retornados são implícitos; para descobrir o que
valores reais estão sendo retornados, pode ser necessário examinar o código em um
distância significativa da chamada para o próprio try .

Dado que esta propriedade de retornos nu (não-localidade) é geralmente
não gostou, o que você acha de ter try sempre retornando o zero
valores dos argumentos sem erro (se retornar)?

Retornos nus são permitidos apenas quando os argumentos de retorno são nomeados. Isto
parece que tentar segue uma regra diferente?

Eu gosto da ideia geral de reutilizar defer para resolver o problema. No entanto, estou querendo saber se a palavra-chave try é o caminho certo para fazê-lo. E se pudéssemos reutilizar o padrão já existente. Algo que todos já sabem das importações:

Tratamento explícito

res, err := doSomething()
if err != nil {
    return err
}

Ignorando explícito

res, _ := doSomething()

Tratamento diferido

Comportamento semelhante ao que try vai fazer.

res, . := doSomething()

@piotrkowalczuk
Esta pode ser uma sintaxe melhor para isso, mas não sei o quão fácil seria adaptar Go para tornar isso legal, tanto em Go quanto em marcadores de sintaxe.

@balasanjay (e @lootch): Pelo seu comentário aqui , sim, o programa https://play.golang.org/p/KenN56iNVg7 imprimirá 1.

Como try se preocupa apenas com o resultado do erro, deixa todo o resto em paz. Ele poderia definir outros valores de retorno para seus valores zero, mas obviamente não está claro por que isso seria melhor. Por um lado, pode causar mais trabalho quando os valores de resultado são nomeados porque podem ter que ser definidos como zero; no entanto, o chamador (provavelmente) irá ignorá-los se houver um erro. Mas esta é uma decisão de design que pode ser alterada se houver boas razões para isso.

[editar: Observe que esta questão (de limpar resultados sem erro ao encontrar um erro) não é específica para a proposta try . Qualquer uma das alternativas propostas que não exija um return explícito terá que responder a mesma pergunta.]

Em relação ao seu exemplo de um escritor n += try(wrappedWriter.Write(...)) : Sim, em uma situação em que você precisa incrementar n mesmo em caso de erro, não se pode usar try - mesmo que try não zera valores de resultado sem erro. Isso porque try só retorna algo se não houver erro: try se comporta de forma limpa como uma função (mas uma função que pode não retornar ao chamador, mas ao chamador do chamador). Veja o uso de temporários na implementação de try .

Mas em casos como o seu exemplo, também teria que ser cuidadoso com uma instrução if e certifique-se de incorporar a contagem de bytes retornada em n .

Mas talvez eu esteja entendendo mal sua preocupação.

@griesemer : Estou sugerindo que é melhor definir os outros valores de retorno para seus valores zero, porque fica claro o que try fará apenas inspecionando o callsite. Ele irá a) não fazer nada, ou b) retornar da função com valores zero e o argumento para tentar.

Conforme especificado, try reterá os valores dos valores de retorno nomeados sem erro e, portanto, seria necessário inspecionar toda a função para saber quais valores try estão retornando.

Este é o mesmo problema com um retorno nu (tendo que escanear toda a função para ver qual valor está sendo retornado) e provavelmente foi o motivo do arquivamento https://github.com/golang/go/issues/21291. Isso, para mim, implica que try em uma função grande com valores de retorno nomeados, teria que ser desencorajado sob a mesma base de retornos nu (https://github.com/golang/go/wiki/CodeReviewComments #named-result-parameters). Em vez disso, sugiro que try seja especificado para sempre retornar os valores zero do argumento sem erro.

perplexo e me sinto mal pela equipe go ultimamente. try é uma solução limpa e compreensível para o problema específico que está tentando resolver: verbosidade no tratamento de erros.

a proposta diz: depois de um ano de discussão estamos adicionando este built-in. use-o se quiser um código menos detalhado, caso contrário, continue fazendo o que faz. a reação é uma resistência não totalmente justificada para um recurso opcional para o qual os membros da equipe mostraram vantagens claras!

eu encorajaria ainda mais a equipe go a fazer try um variadic embutido se isso for fácil de fazer

try(outf.Seek(linkstart, 0))
try(io.Copy(outf, exef))

torna-se

try(outf.Seek(linkstart, 0)), io.Copy(outf, exef)))

a próxima coisa detalhada pode ser aquelas chamadas sucessivas para try .

Eu concordo com o nvictor na maior parte, exceto pelos parâmetros variadic para try . Eu ainda acredito que deveria ter um lugar para um manipulador e a proposta variada pode estar empurrando o limite de legibilidade para mim.

@nvictor Go é uma linguagem que não gosta de recursos não ortogonais. Isso significa que se, no futuro, descobrirmos uma solução de tratamento de erros melhor que não seja try , será muito mais complicado mudar (se não for rejeitado por completo porque nosso solução é "boa o suficiente").

Acho que há uma solução melhor do que try , e prefiro ir devagar e encontrar essa solução do que me contentar com esta.

No entanto, eu não ficaria com raiva se isso fosse adicionado. Não é uma solução ruim, só acho que podemos descobrir uma melhor.

No meu ver, eu quero tentar um código de bloco, agora try como um handle err func

Ao ler esta discussão (e discussões no Reddit), nem sempre senti que todos estavam na mesma página.

Assim, escrevi um pequeno post no blog que demonstra como try pode ser usado: https://faiface.github.io/post/how-to-use-try/.

Tentei mostrar vários aspectos desta proposta para que todos possam ver o que ela pode fazer e formar uma opinião mais informada (mesmo que negativa).

Se eu perdi algo importante, por favor me avise!

@faiface tenho certeza que você pode substituir

if err != nil {
    return resps, err
}

com try(err) .

Fora isso - ótimo artigo!

@DmitriyMV Verdade! Mas acho que vou mantê-lo do jeito que está, para que haja pelo menos um exemplo do clássico if err != nil , embora não muito bom.

Tenho duas preocupações:

  • retornos nomeados têm sido muito confusos, e isso os encoraja com um novo e importante caso de uso
  • isso irá desencorajar a adição de contexto aos erros

Na minha experiência, adicionar contexto aos erros imediatamente após cada site de chamada é fundamental para ter um código que possa ser facilmente depurado. E os retornos nomeados causaram confusão para quase todos os desenvolvedores de Go que conheço em algum momento.

Uma preocupação menor e estilística é que é lamentável quantas linhas de código agora serão envolvidas em try(actualThing()) . Eu posso imaginar ver a maioria das linhas em uma base de código envolta em try() . Isso parece lamentável.

Acho que essas preocupações seriam abordadas com um ajuste:

a, b, err := myFunc()
check(err, "calling myFunc on %v and %v", a, b)

check() se comportaria como try() , mas descartaria o comportamento de passar pelos valores de retorno da função genericamente e, em vez disso, forneceria a capacidade de adicionar contexto. Ainda provocaria um retorno.

Isso manteria muitas das vantagens de try() :

  • é um embutido
  • segue o fluxo de controle existente WRT para adiar
  • ele se alinha com a prática existente de adicionar contexto aos erros bem
  • ele se alinha com as propostas e bibliotecas atuais para quebra de erros, como errors.Wrap(err, "context message")
  • resulta em um site de chamadas limpo: não há clichê na linha a, b, err := myFunc()
  • descrever erros com defer fmt.HandleError(&err, "msg") ainda é possível, mas não precisa ser encorajado.
  • a assinatura de check é um pouco mais simples, porque não precisa retornar um número arbitrário de argumentos da função que está envolvendo.

Isso é bom, acho que a equipe go realmente deveria levar este. Isso é melhor do que tentar, mais claramente !!!

@buchanae Eu estaria interessado no que você pensa sobre minha postagem no blog porque você argumentou que try desencorajará adicionar contexto aos erros, enquanto eu argumentaria que pelo menos no meu artigo é ainda mais fácil do que o normal.

Eu só vou jogar isso lá fora no estágio atual. Vou pensar um pouco mais sobre isso, mas pensei em postar aqui para ver o que vocês acham. Talvez eu deva abrir um novo problema para isso? Eu também postei isso no #32811

Então, que tal fazer algum tipo genérico de macro C em vez de abrir para mais flexibilidade?

Assim:

define returnIf(err error, desc string, args ...interface{}) {
    if (err != nil) {
        return fmt.Errorf("%s: %s: %+v", desc, err, args)
    }
}

func CopyFile(src, dst string) error {
    r, err := os.Open(src)
    :returnIf(err, "Error opening src", src)
    defer r.Close()

    w, err := os.Create(dst)
    :returnIf(err, "Error Creating dst", dst)
    defer w.Close()

    ...
}

Essencialmente returnIf será substituído/inline por aquele definido acima. A flexibilidade é que cabe a você o que ele faz. Depurar isso pode ser um pouco estranho, a menos que o editor o substitua no editor de alguma maneira legal. Isso também o torna menos mágico, pois você pode ler claramente a definição. E também, isso permite que você tenha uma linha que poderia retornar em caso de erro. E capaz de ter diferentes mensagens de erro dependendo de onde aconteceu (contexto).

Editar: Também adicionado dois pontos na frente da macro para sugerir que talvez isso possa ser feito para esclarecer que é uma macro e não uma chamada de função.

@nvictor

eu encorajaria ainda mais a equipe go a fazer try um variadic built-in

O que try(foo(), bar()) retornaria se foo e bar não retornassem a mesma coisa?

Eu só vou jogar isso lá fora no estágio atual. Vou pensar um pouco mais sobre isso, mas pensei em postar aqui para ver o que vocês acham. Talvez eu deva abrir um novo problema para isso? Eu também postei isso no #32811

Então, que tal fazer algum tipo genérico de macro C em vez de abrir para mais flexibilidade?

@Chillance , IMHO, acho que um macro sistema higiênico como Rust (e muitas outras linguagens) daria às pessoas a chance de brincar com ideias como try ou genéricos e depois que a experiência for adquirida, as melhores ideias podem se tornar parte da linguagem e das bibliotecas. Mas também acho que há muito pouca chance de que tal coisa seja adicionada ao Go.

@jonbodner atualmente existe uma proposta para adicionar macros higiênicas no Go. Nenhuma sintaxe proposta ou qualquer coisa ainda, porém não houve muito _contra_ a ideia de adicionar macros higiênicas. #32620

@Allenyn , sobre a sugestão anterior de @buchanae que você acabou de citar :

a, b, err := myFunc()
check(err, "calling myFunc on %v and %v", a, b)

Pelo que vi da discussão, meu palpite é que seria um resultado improvável aqui para a semântica de fmt ser puxada para uma função interna. (Veja, por exemplo, a resposta de @josharian ).

Dito isso, não é realmente necessário, inclusive porque permitir uma função de manipulador pode evitar puxar a semântica fmt diretamente para um built-in. Uma dessas abordagens foi proposta por @eihigh no primeiro dia de discussão aqui, que é semelhante ao espírito da sugestão de @buchanae e que sugeriu ajustar o try embutido para ter a seguinte assinatura:

func try(error, optional func(error) error)

Como essa alternativa try não retorna nada, essa assinatura implica:

  • ele não pode ser aninhado dentro de outra chamada de função
  • deve estar no início da linha

Eu não quero acionar bikeshedding o nome, mas essa forma de try pode ficar melhor com um nome alternativo como check . Pode-se imaginar auxiliares de biblioteca padrão que poderiam tornar conveniente a anotação no local opcional, enquanto defer poderia permanecer uma opção para anotação uniforme quando desejado.

Houve algumas propostas relacionadas criadas posteriormente em #32811 ( catch como interno) e #32611 ( on palavra-chave para permitir on err, <statement> ). Esses podem ser bons lugares para discutir mais, ou para adicionar um polegar para cima ou para baixo, ou sugerir possíveis ajustes para essas propostas.

@jonbodner atualmente existe uma proposta para adicionar macros higiênicas no Go. Nenhuma sintaxe proposta ou qualquer coisa ainda, porém não houve muito _contra_ a ideia de adicionar macros higiênicas. #32620

É ótimo que haja uma proposta, mas suspeito que a equipe principal do Go não pretenda adicionar macros. No entanto, eu ficaria feliz em estar errado sobre isso, pois acabaria com todos os argumentos sobre mudanças que atualmente exigem modificações no núcleo da linguagem. Para citar um famoso fantoche, "Faça. Ou não faça. Não há tentativa."

@jonbodner Eu não acho que adicionar macros higiênicas acabaria com o argumento. Muito pelo contrário. Uma crítica comum é que try "esconde" o retorno. Macros seriam estritamente piores deste ponto de vista, porque tudo seria possível em uma macro. E mesmo que Go permitisse macros higiênicas definidas pelo usuário, ainda teríamos que debater se try deveria ser uma macro pré-declarada no bloco do universo, ou não. Seria lógico que aqueles que se opõem a try se oponham ainda mais às macros higiênicas ;-)

@ngrilly existem várias maneiras de garantir que as macros se destaquem e sejam fáceis de ver. A maneira como Rust faz isso é que as macros são sempre precedidas por ! (ou seja try!(...) e println!(...) ).

Eu diria que se macros higiênicas fossem adotadas e fáceis de ver, e não parecessem com chamadas de função normais, elas se encaixariam muito melhor. Devemos optar por soluções mais gerais, em vez de resolver problemas individuais.

@thepudds Concordo que adicionar um parâmetro opcional do tipo func(error) error pode ser útil (essa possibilidade é discutida na proposta, com alguns problemas que precisariam ser resolvidos), mas não vejo sentido em try não retornando nada. O try proposto pela equipe Go é uma ferramenta mais geral.

@deanveloper Sim, o ! no final das macros em Rust é inteligente. Ele lembra os identificadores exportados começando com uma letra maiúscula em Go :-)

Eu concordaria em ter macros higiênicas em Go se e somente se pudermos preservar a velocidade de compilação e resolver problemas complexos relacionados a ferramentas (ferramentas de refatoração precisariam expandir as macros para entender a semântica do código, mas devem gerar código com as macros não expandidas) . É difícil. Enquanto isso, talvez try possa ser renomeado para try! ? ;-)

Uma ideia leve: se o corpo de uma construção if/for contém uma única instrução, não há necessidade de chaves, desde que essa instrução esteja na mesma linha que if ou for . Exemplo:

fd, err := os.Open("foo")
if err != nil return err

Observe que atualmente um tipo error é apenas um tipo de interface comum. O compilador não o trata como algo especial. try muda isso. Se o compilador tiver permissão para tratar error como especial, prefiro um /bin/sh inspirado || :

fd, err := os.Open("foo") || return err

O significado de tal código seria bastante óbvio para a maioria dos programadores, não há fluxo de controle oculto e, como atualmente esse código é ilegal, nenhum código de trabalho é prejudicado.

Embora eu possa imaginar que alguns de vocês estão recuando de horror.

@bakul Em if err != nil return err , como você sabe onde a expressão err != nil termina e onde começa a instrução return err ? Sua ideia seria uma grande mudança na gramática da linguagem, muito maior do que o proposto com try .

Sua segunda ideia se parece com catch |err| return err em Zig . Pessoalmente, não estou "recuando de horror" e diria por que não? Mas deve-se notar que Zig também tem uma palavra-chave try , que é um atalho para catch |err| return err , e quase equivalente ao que a equipe Go propõe aqui como uma função interna. Então, talvez o try seja suficiente e não precisemos da palavra-chave catch ? ;-)

@ngrilly , Atualmente <expr> <statement> não é válido, então não acho que essa mudança tornaria a gramática mais ambígua, mas pode ser um pouco mais frágil.

Isso geraria exatamente o mesmo código que a proposta try, mas a) o retorno é explícito aqui b) não há aninhamento possível como com try e c) essa seria uma sintaxe familiar para usuários do shell (que superam em muito os usuários zig). Não há catch aqui.

Eu trouxe isso como uma alternativa, mas para ser franco, estou perfeitamente bem com o que quer que os designers de linguagem de núcleo decidam.

Carreguei uma versão ligeiramente melhorada de tryhard . Ele agora relata informações mais detalhadas sobre os arquivos de entrada. Por exemplo, executando contra a ponta do repositório Go, ele relata agora:

$ tryhard $HOME/go/src
...
--- stats ---
  55620 (100.0% of   55620) function declarations
  14936 ( 26.9% of   55620) functions returning an error
 116539 (100.0% of  116539) statements
  27327 ( 23.4% of  116539) if statements
   7636 ( 27.9% of   27327) if <err> != nil statements
    119 (  1.6% of    7636) <err> name is different from "err" (use -l flag to list file positions)
   6037 ( 79.1% of    7636) return ..., <err> blocks in if <err> != nil statements
   1599 ( 20.9% of    7636) more complex error handler in if <err> != nil statements; prevent use of try (use -l flag to list file positions)
     17 (  0.2% of    7636) non-empty else blocks in if <err> != nil statements; prevent use of try (use -l flag to list file positions)
   5907 ( 77.4% of    7636) try candidates (use -l flag to list file positions)

Há mais a ser feito, mas isso dá uma imagem mais clara. Especificamente, 28% de todas as instruções if parecem ser para verificação de erros; isso confirma que há uma quantidade significativa de código repetitivo. Dessas verificações de erros, 77% seriam passíveis de try .

$ tryhard .
--- Estatísticas ---
2930 (100,0% de 2930) declarações de função
1408 (48,1% de 2930) funções retornando um erro
10497 (100,0% de 10497) declarações
2265 (21,6% de 10497) se as declarações
1383 (61,1% de 2265) se!= nenhuma instrução
0 (0,0% de 1383)nome é diferente de "err" (use -l flag
para listar posições de arquivo)
645 (46,6% de 1383) retorno...,bloqueia se!= nada
afirmações
738 (53,4% de 1383) manipulador de erros mais complexo em if!= nada
afirmações; evite o uso de try (use -l sinalizador para listar as posições do arquivo)
1 (0,1% de 1383) não vazio else bloqueia se!= nada
afirmações; evite o uso de try (use -l sinalizador para listar as posições do arquivo)
638 (46,1% de 1383) tentam candidatos (use -l sinalizador para listar o arquivo
posições)
$ go fornecedor de mod
$ vendedor tryhard
--- Estatísticas ---
37757 (100,0% de 37757) declarações de função
12557 (33,3% de 37757) funções retornando um erro
88919 (100,0% de 88919) declarações
20143 (22,7% de 88919) se as declarações
6555 (32,5% de 20143) se!= nenhuma instrução
109 (1,7% de 6555)nome é diferente de "err" (use -l flag
para listar posições de arquivo)
5545 (84,6% de 6555) retorno...,bloqueia se!= nada
afirmações
1010 (15,4% de 6555) manipulador de erros mais complexo em if!= nada
afirmações; evite o uso de try (use -l sinalizador para listar as posições do arquivo)
12 (0,2% de 6555) não vazios else bloqueiam if!= nada
afirmações; evite o uso de try (use -l sinalizador para listar as posições do arquivo)
5427 ( 82,8% de 6555) tente candidatos (use -l sinalizador para listar o arquivo
posições)

Então, é por isso que eu adicionei dois pontos no exemplo da macro, para que ele se destacasse e não parecesse uma chamada de função. Não precisa ser dois pontos, é claro. É apenas um exemplo. Além disso, uma macro não esconde nada. Basta olhar para o que a macro faz e pronto. Como se fosse uma função, mas será embutido. É como se você fizesse uma pesquisa e substituísse pela parte de código da macro em suas funções onde o uso da macro foi feito. Naturalmente, se as pessoas fazem macros de macros e começam a complicar as coisas, bem, culpe-se por tornar o código mais complicado. :)

@mirtchovski

$ tryhard .
--- stats ---
   2930 (100.0% of    2930) function declarations
   1408 ( 48.1% of    2930) functions returning an error
  10497 (100.0% of   10497) statements
   2265 ( 21.6% of   10497) if statements
   1383 ( 61.1% of    2265) if <err> != nil statements
      0 (  0.0% of    1383) <err> name is different from "err" (use -l flag to list file positions)
    645 ( 46.6% of    1383) return ..., <err> blocks in if <err> != nil statements
    738 ( 53.4% of    1383) more complex error handler in if <err> != nil statements; prevent use of try (use -l flag to list file positions)
      1 (  0.1% of    1383) non-empty else blocks in if <err> != nil statements; prevent use of try (use -l flag to list file positions)
    638 ( 46.1% of    1383) try candidates (use -l flag to list file
positions)
$ go mod vendor
$ tryhard vendor
--- stats ---
  37757 (100.0% of   37757) function declarations
  12557 ( 33.3% of   37757) functions returning an error
  88919 (100.0% of   88919) statements
  20143 ( 22.7% of   88919) if statements
   6555 ( 32.5% of   20143) if <err> != nil statements
    109 (  1.7% of    6555) <err> name is different from "err" (use -l flag to list file positions)
   5545 ( 84.6% of    6555) return ..., <err> blocks in if <err> != nil statements
   1010 ( 15.4% of    6555) more complex error handler in if <err> != nil statements; prevent use of try (use -l flag to list file positions)
     12 (  0.2% of    6555) non-empty else blocks in if <err> != nil statements; prevent use of try (use -l flag to list file positions)
   5427 ( 82.8% of    6555) try candidates (use -l flag to list file
positions)
$

@av86743 ,

desculpe, não considerei que "Respostas por e-mail não suportam Markdown"

Algumas pessoas comentaram que não é justo contar código de fornecedores em resultados de tryhard . Por exemplo, na biblioteca std, o código fornecido inclui os pacotes syscall gerados que contêm muitas verificações de erros e podem distorcer a imagem geral. A versão mais recente de tryhard agora exclui caminhos de arquivo contendo "vendor" por padrão (isso também pode ser controlado com o novo sinalizador -ignore ). Aplicado à biblioteca std na dica:

tryhard $HOME/go/src
/Users/gri/go/src/cmd/go/testdata/src/badpkg/x.go:1:1: expected 'package', found pkg
/Users/gri/go/src/cmd/go/testdata/src/notest/hello.go:6:1: expected declaration, found Hello
/Users/gri/go/src/cmd/go/testdata/src/syntaxerror/x_test.go:3:11: expected identifier
--- stats ---
  45424 (100.0% of   45424) func declarations
   8346 ( 18.4% of   45424) func declarations returning an error
  71401 (100.0% of   71401) statements
  16666 ( 23.3% of   71401) if statements
   4812 ( 28.9% of   16666) if <err> != nil statements
     86 (  1.8% of    4812) <err> name is different from "err" (-l flag lists details)
   3463 ( 72.0% of    4812) return ..., <err> blocks in if <err> != nil statements
   1349 ( 28.0% of    4812) complex error handler in if <err> != nil statements; cannot use try (-l flag lists details)
     17 (  0.4% of    4812) non-empty else blocks in if <err> != nil statements; cannot use try (-l flag lists details)
   3345 ( 69.5% of    4812) try candidates (-l flag lists details)

Agora, 29% (28,9%) de todas as instruções if parecem ser para verificação de erros (um pouco mais do que antes), e dessas cerca de 70% parecem ser candidatas a try (um pouco menos do que antes).

Alterar https://golang.org/cl/185177 menciona este problema: src: apply tryhard -err="" -ignore="vendor" -r $GOROOT/src

@griesemer você contou "manipuladores de erros complexos", mas não "manipuladores de erros de instrução única".

Se a maioria dos manipuladores "complexos" for uma única instrução, então on err #32611 produziria tanta economia padrão quanto try() -- 2 linhas vs 3 linhas x 70%. E on err adiciona o benefício de um padrão consistente para a grande maioria dos erros.

@nvictor

try é uma solução limpa e compreensível para o problema específico que está tentando resolver:
verbosidade no tratamento de erros.

Verbosidade no tratamento de erros não é _um problema_, é a força do Go.

a proposta diz: depois de um ano de discussão estamos adicionando este built-in. use-o se quiser um código menos detalhado, caso contrário, continue fazendo o que faz. a reação é uma resistência não totalmente justificada para um recurso opcional para o qual os membros da equipe mostraram vantagens claras!

Sua _opt-in_ no momento da escrita é uma _must_ para todos os leitores, incluindo você futuro.

vantagens claras

Se o fluxo de controle confuso pode ser chamado de 'uma vantagem', então sim.

try , por causa dos hábitos dos expatriados de Java e C++, introduz magia que precisa ser compreendida por todos os Gophers. Enquanto isso, poupando uma minoria de algumas linhas para escrever em alguns lugares (como as corridas tryhard mostraram).

Eu argumentaria que minha maneira mais simples de macro onErr pouparia mais linhas de escrita e, para a maioria:

x, err = fa()
onErr break

r, err := fb(x)
onErr return 0, nil, err

if r, err := fc(x); onErr && triesleft > 0 {
  triesleft--
  continue retry
}

_(note que estou no campo 'deixe if err!= nil sozinho' e a contraproposta acima foi publicada para mostrar uma solução mais simples que pode deixar mais chorões felizes.)_

Editar:

eu encorajaria ainda mais a equipe go a fazer try um variadic embutido se isso for fácil de fazer
try(outf.Seek(linkstart, 0)), io.Copy(outf, exef)))

~ Curto para escrever, longo para ler, propenso a deslizes ou mal-entendidos, esquisito e perigoso na fase de manutenção. ~

Eu estava errado. Na verdade, o variadic try seria muito melhor que os nests, pois poderíamos escrevê-lo por linhas:

try( outf.Seek(linkstart, 0),
 io.Copy(outf, exef),
)

e ter try(…) retorno após o primeiro erro.

Eu não acho que esse identificador de erro implícito (sintaxe sugar) como try seja bom, porque você não pode lidar com vários erros intuitivamente, especialmente quando você precisa executar várias funções sequencialmente.

Eu sugeriria algo como Elixir's com declaração: https://www.openmymind.net/Elixirs-With-Statement/

Algo assim abaixo em golang:

switch a, b, err1 := go_func_01(),
       apple, banana, err2 := go_func_02(),
       fans, dissman, err3 := go_func_03()
{
   normal_func()
else
   err1 -> handle_err1()
   err2 -> handle_err2()
   _ -> handle_other_errs()
}

Esse tipo de violação do "Go prefere menos recursos" e "adicionar recursos ao Go não o tornaria melhor, mas maior"? Não tenho certeza...

Eu só quero dizer, pessoalmente, estou perfeitamente satisfeito com o jeito antigo

if err != nil {
    return …, err
}

E definitivamente não quero ler o código escrito por outros usando o try ... O motivo pode ser duas dobras:

  1. às vezes é difícil adivinhar o que está dentro à primeira vista
  2. try s podem ser aninhados, ou seja, try( ... try( ... try ( ... ) ... ) ... ) , difícil de ler

Se você acha que escrever código à moda antiga para passar erros é tedioso, por que não apenas copiar e colar, já que eles estão sempre fazendo o mesmo trabalho?

Bem, você pode pensar que nem sempre queremos fazer o mesmo trabalho, mas então você terá que escrever sua função "handler". Então talvez você não perca nada se ainda escrever da maneira antiga.

O desempenho do adiamento não é um problema com esta solução proposta? Avaliei funções com e sem adiamento e houve um impacto significativo no desempenho. Acabei de pesquisar no Google outra pessoa que fez esse benchmark e encontrei um custo de 16x. Não me lembro do meu ser tão ruim, mas 4x mais lento toca um sino. Como algo que pode dobrar ou piorar o tempo de execução de muitas funções pode ser considerado uma solução geral viável?

@eric-hawthorne Adiar o desempenho é um problema separado. Try não requer inerentemente adiar e não remove a capacidade de lidar com erros sem ele.

@fabian-f Mas esta proposta poderia encorajar a substituição do código em que alguém está decorando os erros separadamente para cada erro inline dentro do escopo do bloco if err != nil. Isso seria uma diferença significativa de desempenho.

@eric-hawthorne Citando o documento de design:

P: O uso de defer para agrupar erros não será lento?

R: Atualmente, uma declaração de diferimento é relativamente cara em comparação com o fluxo de controle comum. No entanto, acreditamos que é possível fazer casos de uso comuns de adiamento para tratamento de erros comparáveis ​​em desempenho com a abordagem “manual” atual. Veja também CL 171758, que deve melhorar o desempenho do diferimento em cerca de 30%.

Aqui estava uma palestra interessante de Rust ligada no Reddit. A parte mais relevante começa aos 47:55

Eu tentei tryhard no meu maior repositório público, https://github.com/dpinela/mflg , e obtive o seguinte:

--- stats ---
    309 (100.0% of     309) func declarations
     36 ( 11.7% of     309) func declarations returning an error
    305 (100.0% of     305) statements
     73 ( 23.9% of     305) if statements
     29 ( 39.7% of      73) if <err> != nil statements
      0 (  0.0% of      29) <err> name is different from "err"
     19 ( 65.5% of      29) return ..., <err> blocks in if <err> != nil statements
     10 ( 34.5% of      29) complex error handler in if <err> != nil statements; cannot use try
      0 (  0.0% of      29) non-empty else blocks in if <err> != nil statements; cannot use try
     15 ( 51.7% of      29) try candidates

A maior parte do código nesse repositório está gerenciando o estado do editor interno e não faz nenhuma E/S e, portanto, tem poucas verificações de erros - portanto, os locais onde o try pode ser usado são relativamente limitados. Fui em frente e reescrevi manualmente o código para usar try sempre que possível; git diff --stat retorna o seguinte:

 application.go                  | 42 +++++++++++-------------------------------
 internal/atomicwrite/write.go   | 35 ++++++++++++++---------------------
 internal/clipboard/clipboard.go | 17 +++--------------
 internal/config/config.go       | 15 +++++++--------
 internal/termesc/term.go        |  5 +----
 render.go                       |  8 ++------
 6 files changed, 38 insertions(+), 84 deletions(-)

(Diferença completa aqui .)

Dos 10 manipuladores que tryhard relata como "complexos", 5 são falsos negativos em internal/atomicwrite/write.go; eles estavam usando pkg/errors.WithMessage para quebrar o erro. O encapsulamento era exatamente o mesmo para todos eles, então reescrevi essa função para usar manipuladores try e deferred. Acabei com este diff (+14, -21 linhas):

@@ -20,21 +20,20 @@ const (
 // The file is created with mode 0644 if it doesn't already exist; if it does, its permissions will be
 // preserved if possible.
 // If some of the directories on the path don't already exist, they are created with mode 0755.
-func Write(filename string, contentWriter func(io.Writer) error) error {
+func Write(filename string, contentWriter func(io.Writer) error) (err error) {
+       defer func() { err = errors.WithMessage(err, errString(filename)) }()
+
        dir := filepath.Dir(filename)
-       if err := os.MkdirAll(dir, defaultDirPerms); err != nil {
-               return errors.WithMessage(err, errString(filename))
-       }
-       tf, err := ioutil.TempFile(dir, "mflg-atomic-write")
-       if err != nil {
-               return errors.WithMessage(err, errString(filename))
-       }
+       try(os.MkdirAll(dir, defaultDirPerms))
+       tf := try(ioutil.TempFile(dir, "mflg-atomic-write"))
        name := tf.Name()
-       if err = contentWriter(tf); err != nil {
-               os.Remove(name)
-               tf.Close()
-               return errors.WithMessage(err, errString(filename))
-       }
+       defer func() {
+               if err != nil {
+                       tf.Close()
+                       os.Remove(name)
+               }
+       }()
+       try(contentWriter(tf))
        // Keep existing file's permissions, when possible. This may race with a chmod() on the file.
        perms := defaultPerms
        if info, err := os.Stat(filename); err == nil {
@@ -42,14 +41,8 @@ func Write(filename string, contentWriter func(io.Writer) error) error {
        }
        // It's better to save a file with the default TempFile permissions than not save at all, so if this fails we just carry on.
        tf.Chmod(perms)
-       if err = tf.Close(); err != nil {
-               os.Remove(name)
-               return errors.WithMessage(err, errString(filename))
-       }
-       if err = os.Rename(name, filename); err != nil {
-               os.Remove(name)
-               return errors.WithMessage(err, errString(filename))
-       }
+       try(tf.Close())
+       try(os.Rename(name, filename))
        return nil
 }

Observe o primeiro adiamento, que anota o erro - consegui encaixá-lo confortavelmente em uma linha graças ao WithMessage retornando nil para um erro nil. Parece que esse tipo de wrapper funciona tão bem com essa abordagem quanto as sugeridas na proposta.

Dois dos outros manipuladores "complexos" estavam em implementações de ReadFrom e WriteTo:

var line string
line, err = br.ReadString('\n')
b.lines = append(b.lines, line)
if err != nil {
  if err == io.EOF {
    err = nil
  }
  return
}
func (b *Buffer) WriteTo(w io.Writer) (int64, error) {
    var n int64
    for _, line := range b.lines {
        nw, err := w.Write([]byte(line))
        n += int64(nw)
        if err != nil {
            return n, err
        }
    }
    return n, nil
}

Estes realmente não eram passíveis de tentar, então os deixei sozinhos.

Dois outros eram códigos como este, onde estou retornando um erro totalmente diferente daquele que verifiquei (não apenas embrulhando-o). Eu os deixei inalterados também:

n, err := strconv.ParseInt(s[1:], 16, 32)
if err != nil {
    return Color{}, errors.WithMessage(err, fmt.Sprintf("color: parse %q", s))
}

A última estava em uma função para carregar um arquivo de configuração, que sempre retorna uma configuração (diferente de zero) mesmo que haja um erro. Ele só tinha essa verificação de erro, então não se beneficiou muito de tentar:

-func Load() (*Config, error) {
-       c := Config{
+func Load() (c *Config, err error) {
+       defer func() { err = errors.WithMessage(err, "error loading config file") }()
+
+       c = &Config{
                TabWidth:    4,
                ScrollSpeed: 1,
                Lang:        make(map[string]LangConfig),
        }
-       f, err := basedir.Config.Open(filepath.Join("mflg", "config.toml"))
-       if err != nil {
-               return &c, errors.WithMessage(err, "error loading config file")
-       }
+       f := try(basedir.Config.Open(filepath.Join("mflg", "config.toml")))
        defer f.Close()
-       _, err = toml.DecodeReader(f, &c)
+       _, err = toml.DecodeReader(f, c)
        if c.TextStyle.Comment == (Style{}) {
                c.TextStyle.Comment = Style{Foreground: &color.Color{R: 0, G: 200, B: 0}}
        }
        if c.TextStyle.String == (Style{}) {
                c.TextStyle.String = Style{Foreground: &color.Color{R: 0, G: 0, B: 200}}
        }
-       return &c, errors.WithMessage(err, "error loading config file")
+       return c, err
 }

Na verdade, confiar no comportamento do try de manter os valores dos parâmetros de retorno - como um retorno nu - parece, na minha opinião, um pouco mais difícil de seguir; a menos que eu adicionasse mais verificações de erros, eu ficaria com if err != nil neste caso específico.

TL;DR: try só é útil em uma porcentagem bastante pequena (por contagem de linhas) deste código, mas onde ajuda, realmente ajuda.

(Noob aqui). Outra ideia para vários argumentos. E quanto a:

package trytest

import "fmt"

func errorInner() (string, error) {
   return "", fmt.Errorf("inner error")
}

func errorOuter() (string, error) {
   tryreturn errorInner()
   return "", nil
}

func errorOuterWithArg() (string, error) {
   var toProcess string
   tryreturn toProcess, _ = errorOuter()
   return toProcess + "", nil
}

func errorOuterWithArgStretch() (bool, string, error) {
   var toProcess string
   tryreturn false, ( toProcess,_ = errorOuterWithArg() )
   return true, toProcess + "", nil
}

ou seja, tryreturn aciona o retorno de todos os valores se houver erro no último
valor, senão a execução continua.

Os princípios com os quais concordo:
-

  • O tratamento de erros de uma chamada de função merece sua própria linha. Go é deliberadamente explícito no fluxo de controle, e acho que empacotar isso em uma expressão está em desacordo com sua clareza.
  • Seria benéfico ter um método de tratamento de erros que se encaixasse em uma linha. (E, idealmente, exija apenas uma palavra ou alguns caracteres de clichê antes do tratamento real do erro). 3 linhas de tratamento de erros para cada chamada de função é um ponto de atrito na linguagem que merece amor e atenção.
  • Qualquer builtin que retorna (como o try proposto) deve ser pelo menos uma declaração e, idealmente, ter a palavra return nela. Novamente, acho que o fluxo de controle em Go deve ser explícito.
  • Os erros de Go são mais úteis quando têm contexto extra incluído (quase sempre adiciono contexto aos meus erros). Uma solução para esse problema também deve oferecer suporte ao código de tratamento de erros de adição de contexto.

Sintaxe que eu apoio:
-

  • uma instrução reterr _x_ (açúcar sintático para if err != nil { return _x_ } , explicitamente nomeado para indicar que retornará)

Portanto, os casos comuns podem ser uma linha curta e explícita:

func foo() error {
    a, err := bar()
    reterr err

    b, err := baz(a)
    reterr fmt.Errorf("getting the baz of %v: %v", a, err)

    return nil
}

Em vez das 3 linhas são agora:

func foo() error {
    a, err := bar()
    if err != nil {
        return err
    }

    b, err := baz()
    if err != nil {
        return fmt.Errorf("getting the baz of %v: %v", a, err)
    }

    return nil
}

Coisas que eu discordo:



    • "Esta é uma mudança muito pequena para valer a pena mudar o idioma"

      Eu discordo, esta é uma mudança de qualidade de vida que remove a maior fonte de atrito que tenho ao escrever código Go. Ao chamar uma função requer 4 linhas

  • "Seria melhor esperar por uma solução mais geral"
    Eu discordo, acho que esse problema é digno de sua própria solução dedicada. A versão generalizada desse problema está reduzindo o código clichê, e a resposta generalizada são macros - o que vai contra o espírito Go de código explícito. Se o Go não for fornecer um recurso geral de macro, ele deve fornecer algumas macros específicas e amplamente usadas, como reterr (toda pessoa que escreve Go se beneficiaria do reterr).

@Qhesz Não é muito diferente com try:

func foo() error {
    a, err := bar()
    try(err)

    b, err := baz(a)
    try(wrap(err, "getting the baz of %v", a))

    return nil
}

@reusee Agradeço essa sugestão, não sabia que poderia ser usado assim. Parece um pouco irritante para mim, porém, estou tentando colocar meu dedo no porquê.

Eu acho que "tentar" é uma palavra estranha para usar dessa maneira. "try(action())" faz sentido em inglês, enquanto "try(value)" não faz sentido. Eu ficaria mais bem com isso se fosse uma palavra diferente.

Também try(wrap(...)) avalia wrap(...) primeiro, certo? Quanto disso você acha que é otimizado pelo compilador? (Comparado a apenas executar if err != nil ?)

Também o #32611 é uma proposta vagamente semelhante, e os comentários têm algumas opiniões esclarecedoras tanto da equipe principal do Go quanto dos membros da comunidade, em particular sobre as diferenças entre palavras-chave e funções internas.

@Qhesz Concordo com você sobre a nomenclatura. Talvez check seja mais apropriado, pois "check(action())" ou "check(err)" lê bem.

@reusee O que é um pouco irônico, já que o rascunho original usava check .

Em 06/07/19, mirtchovski [email protected] escreveu:

$ tryhard .
--- Estatísticas ---
2930 (100,0% de 2930) declarações de função
1408 (48,1% de 2930) funções retornando um erro
[ ... ]

Não posso deixar de ser travesso aqui: é que "funções retornando um
erro como o último argumento"?

Lúcio.

Pensamento final na minha pergunta acima, eu ainda prefiro a sintaxe try(err, wrap("getting the baz of %v: %v", a, err)) , com wrap() executado apenas se err não for nil. Em vez de try(wrap(err, "getting the baz of %v", a)) .

@Qhesz Uma possível implementação de wrap poderia ser:

func wrap(err error, format string, args ...interface{}) error {
    if err == nil {
        return nil
    }
    return fmt.Errorf("%s: %w", fmt.Sprintf(format, args...), err)
}

Se o compilador puder inline wrap , não haverá diferença de desempenho entre a cláusula wrap e if err != nil .

@reusee acho que você quis dizer if err == nil ;)

@Qhesz Uma possível implementação de wrap poderia ser:

func wrap(err error, format string, args ...interface{}) error {
  if err == nil {
      return nil
  }
  return fmt.Errorf("%s: %w", fmt.Sprintf(format, args...), err)
}

Se o compilador puder inline wrap , não haverá diferença de desempenho entre a cláusula wrap e if err != nil .

%w não é válido go verbo

(Eu presumo que ele quis dizer %v...)

Portanto, embora seja preferível escrever uma palavra-chave, entendo que um builtin é a maneira preferida de implementá-la.

Acho que estaria de acordo com esta proposta se

  • era check em vez de try
  • alguma parte das ferramentas Go impostas, ela só poderia ser usada como uma instrução (ou seja, trate-a como uma 'instrução' interna, não como uma 'função' interna. implementado pela linguagem.) Por exemplo, se não retornou nada, então nunca foi válido dentro de uma expressão, como panic() .
  • ~talvez algum indicador de que é uma macro e influencia o fluxo de controle, algo que o diferencia de uma chamada de função. (por exemplo check!(...) como Rust faz, mas não tenho uma opinião forte sobre a sintaxe específica)~ Mudei de ideia

Então isso seria ótimo, eu usaria em cada chamada de função que eu fizer.

E pequenas desculpas ao tópico, só agora encontrei os comentários acima que descrevem praticamente o que acabei de dizer.

@deanveloper corrigido, obrigado.

@olekukonko @Qhesz %w foi adicionado recentemente na dica: https://tip.golang.org/pkg/fmt/#Errorf

Peço desculpas por não ter lido tudo neste tópico, mas gostaria de mencionar algo que não vi.

Vejo dois casos separados em que o tratamento de erros do Go1 pode ser irritante: código "bom" que está correto, mas um pouco repetitivo; e código "ruim" que está errado, mas funciona principalmente.

No primeiro caso, realmente deve haver alguma lógica no bloco if-err, e mudar para uma construção de estilo try desencoraja essa boa prática, dificultando a adição de lógica extra.

No segundo caso, o código incorreto geralmente tem a forma:

..., _ := might_error()

ou apenas

might_error()

Onde isso ocorre, normalmente é porque o autor não acha importante o suficiente gastar tempo no tratamento de erros e está apenas esperando que tudo funcione. Este caso pode ser melhorado por algo muito próximo do esforço zero, como:

..., XXX := might_error()

onde XXX é um símbolo que significa "qualquer coisa aqui deve parar a execução de alguma forma". Isso deixaria claro que este não é um código pronto para produção - o autor está ciente de um caso de erro, mas não investiu tempo para decidir o que fazer.

Claro que isso não exclui uma solução do tipo returnif handle(err) .

Eu sou contra tentar, em equilíbrio, com elogios aos colaboradores pelo design bem minimalista. Não sou um especialista em Go, mas fui um dos primeiros a adotar e tenho código em produção aqui e ali. Trabalho no grupo Serverless na AWS e parece que lançaremos um serviço baseado em Go ainda este ano, cujo primeiro check-in foi substancialmente escrito por mim. Eu sou um cara muito velho, meu caminho a percorrer passou por C, Perl, Java e Ruby. Meus problemas apareceram antes no resumo do debate muito útil, mas ainda acho que vale a pena reiterar.

  1. Go é uma linguagem pequena e simples e, portanto, alcançou uma legibilidade inigualável. Sou reflexivamente contra acrescentar qualquer coisa a ela, a menos que o benefício seja realmente qualitativamente substancial. Geralmente não se percebe uma ladeira escorregadia até que se esteja nela, então não vamos dar o primeiro passo.
  2. Fui bastante afetado pelo argumento acima sobre facilitar a depuração. Gosto do ritmo visual, em código de infraestrutura de baixo nível, de pequenas estrofes de código ao longo das linhas de “Faça A. Verifique se funcionou. Faça B. Verifique se funcionou... etc" Porque as linhas "Check" são onde você coloca o printf ou o ponto de interrupção. Talvez todos os outros sejam mais inteligentes, mas eu acabo usando esse idioma de ponto de interrupção regularmente.
  3. Assumindo valores de retorno nomeados, "try" é aproximadamente equivalente a if err != nil { return } (eu acho?) Eu pessoalmente gosto de valores de retorno nomeados e, dados os benefícios dos decoradores de erro, suspeito que a proporção de valores de retorno de erro nomeados aumentar monotonicamente; o que enfraquece os benefícios de tentar.
  4. Inicialmente, gostei da proposta de que o gofmt abençoe o one-liner na linha acima, mas, no geral, os IDEs sem dúvida adotarão esse idioma de exibição de qualquer maneira, e o one-liner sacrificaria o benefício de depuração aqui.
  5. Parece bastante provável que algumas formas de aninhamento de expressão contendo "try" abram a porta para os complicadores em nossa profissão causarem o mesmo tipo de estrago que eles têm com fluxos e spliterators Java e assim por diante. Go tem sido mais bem sucedido do que a maioria das outras linguagens em negar aos inteligentes entre nós oportunidades de demonstrar suas habilidades.

Mais uma vez, parabéns à comunidade pela boa proposta limpa e pela discussão construtiva.

Passei uma quantidade significativa de tempo entrando e lendo bibliotecas desconhecidas ou pedaços de código nos últimos anos. Apesar do tédio, if err != nil fornece um idioma muito fácil de ler, embora verticalmente detalhado. O espírito do que try() está tentando realizar é nobre, e acho que há algo a ser feito, mas esse recurso parece mal priorizado e que a proposta está vendo a luz do dia muito cedo (ou seja, deveria vir depois que xerr e genéricos tiveram a chance de marinar em uma versão estável por 6-12 meses).

Apresentar try() parece ser uma proposta nobre e valiosa (por exemplo, 29% - ~40% das declarações de if são para verificação de if err != nil ). Na superfície, parece que a redução do clichê associado ao tratamento de erros melhorará as experiências do desenvolvedor. A compensação da introdução de try() vem na forma de carga cognitiva dos casos especiais semi-sutis. Uma das maiores virtudes do Go é que ele é simples e há muito pouca carga cognitiva necessária para fazer algo (comparado ao C++, onde a especificação da linguagem é grande e diferenciada). Reduzir uma métrica quantitativa (LoC de if err != nil ) em troca de aumentar a métrica quantitativa da complexidade mental é uma pílula difícil de engolir (ou seja, o imposto mental sobre o recurso mais precioso que temos, o poder do cérebro).

Em particular, os novos casos especiais para a maneira try() é tratado com go , defer e variáveis ​​de retorno nomeadas tornam try() mágico o suficiente para tornar o código menos explícito de tal forma que todos os autores ou leitores de código Go terão que conhecer esses novos casos especiais para ler ou escrever Go corretamente e tal ônus não existia anteriormente. Eu gosto que existam casos especiais explícitos para essas situações - especialmente contra a introdução de alguma forma de comportamento indefinido, mas o fato de que eles precisam existir em primeiro lugar indica que isso está incompleto no momento. Se os casos especiais fossem para qualquer coisa menos tratamento de erros, poderia ser aceitável, mas se já estamos falando de algo que pode impactar até 40% de todo o LoC, esses casos especiais precisarão ser treinados em toda a comunidade e que eleva o custo da carga cognitiva dessa proposta a um nível alto o suficiente para justificar preocupação.

Há outro exemplo em Go onde as regras de casos especiais já são uma ladeira cognitiva escorregadia, ou seja, variáveis ​​fixadas e não fixadas. A necessidade de fixar variáveis ​​não é difícil de entender na prática, mas é perdida porque há um comportamento implícito aqui e isso causa uma incompatibilidade entre o autor, o leitor e o que acontece com o executável compilado em tempo de execução. Mesmo com linters como scopelint muitos desenvolvedores ainda não parecem entender essa pegadinha (ou pior, eles sabem disso, mas não percebem porque essa pegadinha escapa de sua mente). Alguns dos bugs de tempo de execução mais inesperados e difíceis de diagnosticar de programas em funcionamento vieram desse problema específico (por exemplo, N objetos são preenchidos com o mesmo valor em vez de iterar sobre uma fatia e obter os valores distintos esperados). O domínio de falha de try() é diferente das variáveis ​​fixadas, mas haverá um impacto em como as pessoas escrevem código como resultado.

IMNSHO, as propostas xerr e genéricas precisam de tempo para serem produzidas por 6-12 meses antes de tentar conquistar o clichê de if err != nil . Os genéricos provavelmente abrirão o caminho para um tratamento de erros mais rico e uma nova maneira idiomática de tratamento de erros. Uma vez que o tratamento de erros idiomáticos com genéricos começa a surgir, então e somente então, faz sentido revisitar uma discussão em torno try() ou qualquer outra coisa.

Não pretendo saber como os genéricos afetarão o tratamento de erros, mas parece certo para mim que os genéricos serão usados ​​para criar tipos ricos que quase certamente serão usados ​​no tratamento de erros. Uma vez que os genéricos tenham permeado as bibliotecas e tenham sido adicionados ao tratamento de erros, pode haver uma maneira óbvia de redirecionar o try() para melhorar a experiência do desenvolvedor em relação ao tratamento de erros.

Os pontos de preocupação que tenho são:

  1. try() não é complicado isoladamente, mas é uma sobrecarga cognitiva onde antes não existia.
  2. Ao inserir err != nil no comportamento assumido de try() , a linguagem está impedindo o uso de err como uma forma de comunicar o estado da pilha.
  3. Esteticamente try() parece inteligência forçada, mas não inteligente o suficiente para satisfazer o teste explícito e óbvio que a maioria da linguagem Go gosta. Como a maioria das coisas que envolvem critérios subjetivos, isso é uma questão de gosto e experiência pessoal e difícil de quantificar.
  4. O tratamento de erros com instruções switch / case e o agrupamento de erros parecem intocados por esta proposta e uma oportunidade perdida, o que me leva a acreditar que esta proposta é um tijolo tímido de tornar um desconhecido-desconhecido um conhecido -conhecido (ou na pior das hipóteses, um conhecido-desconhecido).

Por fim, a proposta try() parece uma nova ruptura na barragem que estava retendo uma enxurrada de nuances específicas da linguagem, como a que escapamos ao deixar o C++ para trás.

TL; DR: não é uma resposta #nevertry tanto quanto é, "não agora, ainda não, e vamos considerar isso novamente no futuro depois que xerr e os genéricos amadurecerem no ecossistema. "

O #32968 vinculado acima não é exatamente uma contraproposta completa, mas se baseia no meu desacordo com a perigosa capacidade de aninhar que a macro try possui. Ao contrário do #32946, esta é uma proposta séria, que espero não tenha falhas graves (é sua para ver, avaliar e comentar, é claro). Excerto:

  • _A macro check não é de uma linha: ela ajuda mais onde muitos
    verificações usando a mesma expressão devem ser realizadas nas proximidades._
  • _Sua versão implícita já compila no playground._

Restrições de projeto (atendidas)

É um built-in, não aninha em uma única linha, permite muito mais fluxos do que try e não tem expectativas sobre a forma de um código dentro. Não incentiva retornos nu.

exemplo de uso

// built-in 'check' macro signature: 
func check(Condition bool) {}

check(err != nil) // explicit catch: label.
{
    ucred, err := getUserCredentials(user)
    remote, err := connectToApi(remoteUri)
    err, session, usertoken := remote.Auth(user, ucred)
    udata, err := session.getCalendar(usertoken)

  catch:               // sad path
    ucred.Clear()      // cleanup passwords
    remote.Close()     // do not leak sockets
    return nil, 0, err // dress before leaving
}
// happy path

// implicit catch: label is above last statement
check(x < 4) 
  {
    x, y = transformA(x, z)
    y, z = transformB(x, y)
    x, y = transformC(y, z)
    break // if x was < 4 after any of above
  }

Espero que isso ajude, Divirta-se!

Li o máximo que pude para entender o tópico. Sou a favor de deixar as coisas exatamente como estão.

Meus motivos:

  1. Eu, e ninguém que ensinei, Go _nunca_ não entendeu o tratamento de erros
  2. Eu me vejo nunca pulando uma armadilha de erro porque é tão fácil fazê-lo ali mesmo

Além disso, talvez eu tenha entendido mal a proposta, mas geralmente, a construção try em outras linguagens resulta em várias linhas de código que podem potencialmente gerar um erro e, portanto, exigem tipos de erro. Adicionando complexidade e muitas vezes algum tipo de arquitetura de erro inicial e esforço de design.

Nesses casos (e eu mesmo fiz isso), vários blocos try são adicionados. que alonga o código e ofusca a implementação.

Se a implementação Go de try for diferente da de outras linguagens, ainda mais confusão surgirá.

Minha sugestão é deixar o tratamento de erros do jeito que está

Eu sei que muitas pessoas opinaram, mas gostaria de adicionar minha crítica à especificação como está.

A parte da especificação que mais me incomoda são esses dois pedidos:

Portanto, sugerimos não permitir try como a função chamada em uma instrução go.
...
Portanto, sugerimos não permitir try como a função chamada em uma instrução defer também.

Esta seria a primeira função interna da qual isso é verdade (você pode até defer e go a panic ) editar porque o resultado não precisa ser descartado. Criar uma nova função interna que exija que o compilador considere o fluxo de controle especial parece uma grande pergunta e quebra a coerência semântica de go. Todos os outros tokens de fluxo de controle em go não são uma função.

Um contra-argumento à minha reclamação é que ser capaz de defer e go a panic é provavelmente um acidente e não muito útil. No entanto, meu ponto é que a coerência semântica das funções em go é quebrada por esta proposta, não que seja importante que defer e go sempre façam sentido usar. Provavelmente, existem muitas funções não incorporadas que nunca fariam sentido usar defer ou go com, mas não há uma razão explícita, semanticamente, para que elas não possam ser. Por que esse builtin consegue se isentar do contrato semântico das funções em andamento?

Eu sei que @griesemer não quer opiniões estéticas sobre essa proposta injetadas na discussão, mas acho que uma das razões pelas quais as pessoas estão achando essa proposta esteticamente revoltante é que eles podem sentir que ela não se soma como uma função.

A proposta diz:

Propomos adicionar uma nova função integrada chamada try com assinatura (pseudo-código)

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

Exceto que isso não é uma função (o que a proposta basicamente admite). É, efetivamente, uma macro única embutida na especificação da linguagem (se for aceita). Existem alguns problemas com esta assinatura.

  1. O que significa para uma função aceitar uma expressão genérica como argumento, sem mencionar uma expressão chamada. Toda vez que a palavra "expressão" é usada na especificação, significa algo como uma função não chamada. Como é que uma função "chamada" pode ser pensada como sendo uma expressão, quando em todos os outros contextos seus valores de retorno são o que é semanticamente ativo. IE pensamos em uma função chamada como sendo seus valores de retorno. As exceções, surpreendentemente, são go e defer , que são ambos tokens brutos não funções internas.

  2. Além disso, esta proposta obtém sua própria assinatura de função incorreta, ou pelo menos não faz sentido, a assinatura real é:

func try(R1, R2, ... Rn) ((R|T)1, (R|T)2, ... (R|T)(n-1), ?Rn) 
// where T is the return params of the function that try is being called from
// where `R` is a return value from a function, `Rn` must be an error
// try will return the R values if Rn is nil and not return Tn at all
// if Rn is not nil then the T values will be returned as well as Rn at the end 
  1. A proposta não inclui o que acontece em situações onde try é chamado com argumentos. O que acontece se try for chamado com argumentos:
try(arg1, arg2,..., err)

Eu acho que a razão pela qual isso não é abordado é porque try está tentando aceitar um argumento expr que na verdade representa n número de argumentos de retorno de uma função mais outra coisa, mais ilustrativa do fato que esta proposta quebra a coerência semântica do que é uma função.

Minha reclamação final contra essa proposta é que ela quebra ainda mais o significado semântico das funções internas. Não sou indiferente à ideia de que funções builtin às vezes precisam ser isentas das regras semânticas de funções "normais" (como não poder atribuí-las a variáveis, etc), mas esta proposta cria um grande conjunto de isenções do " regras normais" que parecem governar funções dentro de golang.

Esta proposta efetivamente torna try uma coisa nova que não teve, não é bem um token e não é bem uma função, é ambos, o que parece ser um precedente pobre em termos de criação de coerência semântica em todo o idioma.

Se vamos adicionar uma nova coisa de fluxo de controle, argumento que faz mais sentido torná-lo um token bruto como goto , et al. Eu sei que não devemos divulgar propostas nesta discussão, mas a título de breve exemplo, acho que algo assim faz muito mais sentido:

f, err := os.Open("/dev/stdout")
throw err

Embora isso adicione uma linha extra de código, acho que resolve todos os problemas que levantei e também elimina toda a deficiência de assinaturas de função "alterna" com try .

edit1 : nota sobre exceções para os casos defer e go em que o builtin não pode ser usado, porque os resultados serão desconsiderados, enquanto que com try não pode ser realmente disse que a função tem resultados.

@nathanjsweet a proposta que você procura é #32611 :-)

@nathanjsweet Parte do que você diz acaba não sendo o caso. A linguagem não permite o uso de defer ou go com as funções pré-declaradas append cap complex imag len make new real . Também não permite defer ou go com as funções definidas por especificação unsafe.Alignof unsafe.Offsetof unsafe.Sizeof .

Obrigado @nathanjsweet por seu extenso comentário - @ianlancetaylor já apontou que seus argumentos estão tecnicamente incorretos. Deixe-me expandir um pouco:

1) Você menciona que a parte da especificação que não permite try com go e defer o incomoda mais porque try seria o primeiro embutido onde isso é verdade. Isso não está correto. O compilador já não permite, por exemplo, defer append(a, 1) . O mesmo vale para outros built-ins que produzem um resultado que é jogado no chão. Essa mesma restrição também se aplicaria a try (exceto quando try não retornar um resultado). (A razão pela qual mencionamos essas restrições no documento de design é ser o mais completo possível - elas são realmente irrelevantes na prática. Além disso, se você ler o documento de design com precisão, ele não diz que não podemos fazer try trabalhe com go ou defer - simplesmente sugere que não o permitimos; principalmente como uma medida prática. É um "pedido grande" - para usar suas palavras - para fazer try trabalha com go e defer mesmo sendo praticamente inútil.)

2) Você sugere que algumas pessoas achem try "esteticamente repugnante" porque não é tecnicamente uma função, e então você se concentra nas regras especiais para a assinatura. Considere new , make , append , unsafe.Offsetof : todos eles têm regras especializadas que não podemos expressar com uma função Go comum. Veja unsafe.Offsetof que tem exatamente o tipo de requisito sintático para seu argumento (deve ser um campo struct!) que exigimos do argumento para try (deve ser um único valor do tipo error ou uma chamada de função retornando um error como último resultado). Não expressamos essas assinaturas formalmente na especificação, pois nenhum desses built-ins porque eles não se encaixam no formalismo existente - se o fizessem, não precisariam ser embutidos. Em vez disso, expressamos suas regras em prosa. É por isso que eles são integrados que _são_ a escotilha de escape em Go, por design, desde o primeiro dia. Observe também que o documento de design é muito explícito sobre isso.

3) A proposta também aborda o que acontece quando try é chamado com argumentos (mais de um): Não é permitido. O documento de design afirma explicitamente que try aceita uma (uma) expressão de argumento de entrada.

4) Você está afirmando que "esta proposta quebra o significado semântico das funções internas". Em nenhum lugar o Go restringe o que um built-in pode fazer e o que não pode fazer. Temos total liberdade aqui.

Obrigado.

@griesemer

Observe também que o documento de design é muito explícito sobre isso.

Você pode apontar isso. Fiquei surpreso ao ler isso.

Você está afirmando que "esta proposta quebra o significado semântico das funções internas". Em nenhum lugar o Go restringe o que um built-in pode fazer e o que não pode fazer. Temos total liberdade aqui.

Acho que esse é um ponto justo. No entanto, eu acho que há o que está escrito nos documentos de design e o que parece "ir" (que é algo que Rob Pike fala muito). Eu acho que é justo dizer que a proposta try expande as maneiras pelas quais as funções internas quebram as regras pelas quais esperamos que as funções se comportem, e eu reconheço que entendo por que isso é necessário para outras funções internas , mas acho que neste caso a expansão de quebrar as regras é:

  1. Contra-intuitivo em alguns aspectos. Esta é a primeira função que altera a lógica do fluxo de controle de uma forma que não desenrola a pilha (como panic e os.Exit fazem)
  2. Uma nova exceção ao funcionamento das convenções de chamada de uma função. Você deu o exemplo de unsafe.Offsetof como um caso em que há um requisito sintático para uma chamada de função (é surpreendente para mim que isso cause um erro em tempo de compilação, mas isso é outro problema), mas o requisito sintático , neste caso, é um requisito sintático diferente daquele que você declarou. unsafe.Offsetof requer um argumento, enquanto try requer uma expressão que pareceria, em todos os outros contextos, como um valor retornado de uma função (ou seja try(os.Open("/dev/stdout")) ) e poderia ser assumida com segurança em todos os outros contextos para retornar apenas um valor (a menos que a expressão se pareça com try(os.Open("/dev/stdout")...) ).

@nathanjsweet escreveu:

Observe também que o documento de design é muito explícito sobre isso.

Você pode apontar isso. Fiquei surpreso ao ler isso.

Está na seção "Conclusões" da proposta:

Em Go, os built-ins são o mecanismo de escape de linguagem de escolha para operações que são irregulares de alguma forma, mas que não justificam uma sintaxe especial.

Estou surpreso que você tenha perdido ;-)

@ngrilly não quero dizer nesta proposta, quero dizer na especificação da linguagem go. Tive a impressão de que @griesemer estava dizendo que a especificação da linguagem go chama as funções internas como sendo o mecanismo especificamente útil para quebrar a convenção sintática.

@nathanjsweet

Contra-intuitivo em alguns aspectos. Esta é a primeira função que altera a lógica de fluxo de controle de uma forma que não desenrola a pilha (como panic e os.Exit fazem)

Eu não acho que os.Exit desenrola a pilha em qualquer sentido útil. Ele encerra o programa imediatamente sem executar nenhuma função adiada. Parece-me que os.Exit é o estranho aqui, já que tanto panic quanto try executam funções diferidas e percorrem a pilha.

Concordo que os.Exit é o estranho, mas tem que ser assim. os.Exit para todas as goroutines; não faria sentido apenas executar as funções adiadas apenas da goroutine que chama os.Exit . Ele deve executar todas as funções adiadas ou nenhuma. E é muito mais fácil não executar nenhum.

Executado tryhard em nossa base de código e foi isso que obtivemos:

--- stats ---
  15298 (100.0% of   15298) func declarations
   3026 ( 19.8% of   15298) func declarations returning an error
  33941 (100.0% of   33941) statements
   7765 ( 22.9% of   33941) if statements
   3747 ( 48.3% of    7765) if <err> != nil statements
    131 (  3.5% of    3747) <err> name is different from "err"
   1847 ( 49.3% of    3747) return ..., <err> blocks in if <err> != nil statements
   1900 ( 50.7% of    3747) complex error handler in if <err> != nil statements; cannot use try
     19 (  0.5% of    3747) non-empty else blocks in if <err> != nil statements; cannot use try
   1789 ( 47.7% of    3747) try candidates

Primeiro, quero esclarecer que, como Go (antes de 1.13) carece de contexto em erros, implementamos nosso próprio tipo de erro que implementa a interface error , algumas funções são declaradas como retornando foo.Error em vez de error , e parece que este analisador não detectou isso, então esses resultados não são "justos".

Eu estava no campo do "sim! vamos fazer isso", e acho que será um experimento interessante para 1.13 ou 1.14 betas , mas estou preocupado com os _" 47,7% ... tentar candidatos"_. Agora significa que existem 2 maneiras de fazer as coisas, que eu não gosto. No entanto, também existem 2 maneiras de criar um ponteiro ( new(Foo) vs &Foo{} ), bem como 2 maneiras de criar uma fatia ou mapa com make([]Foo) e []Foo{} .

Agora estou no campo de "vamos _tentar_ isso" :^) e ver o que a comunidade pensa. Talvez vamos mudar nossos padrões de codificação para sermos preguiçosos e parar de adicionar contexto, mas talvez tudo bem se os erros obtiverem um contexto melhor do xerrors impl que está vindo de qualquer maneira.

Obrigado, @Goodwine por fornecer dados mais concretos!

(Como um aparte, fiz uma pequena alteração em tryhard ontem à noite para dividir a contagem de "manipuladores de erros complexos" em duas contagens: manipuladores complexos e retornos do formulário return ..., expr onde o último o valor do resultado não é <err> . Isso deve fornecer algumas informações adicionais.)

Que tal alterar a proposta para ser variada em vez desse argumento de expressão estranha?

Isso resolveria muitos problemas. No caso em que as pessoas quisessem apenas retornar o erro, a única coisa que mudaria seria a variável explícita ... . POR EXEMPLO:

try(os.Open("/dev/stdout")...)

no entanto, as pessoas que desejam uma situação mais flexível podem fazer algo como:

f, err := os.Open("/dev/stdout")
try(WrapErrorf(err, "whatever wrap does: %v"))

Uma coisa que essa ideia faz é tornar a palavra try menos apropriada, mas mantém a compatibilidade com versões anteriores.

@nathanjsweet escreveu:

Não quero dizer nesta proposta, quero dizer na especificação da linguagem go.

Aqui estão os extratos que você estava procurando na especificação do idioma:

Na seção "Declarações de expressão":

As seguintes funções internas não são permitidas no contexto da instrução: append cap complex imag len make new real unsafe.Alignof unsafe.Offsetof unsafe.Sizeof

Nas seções "Declarações Go" e "Declarações Adiadas":

As chamadas de funções internas são restritas como para instruções de expressão.

Na seção "Funções incorporadas":

As funções internas não possuem tipos Go padrão, portanto, elas só podem aparecer em expressões de chamada; eles não podem ser usados ​​como valores de função.

@nathanjsweet escreveu:

Tive a impressão de que @griesemer estava dizendo que a especificação da linguagem go chama as funções internas como sendo o mecanismo especificamente útil para quebrar a convenção sintática .

As funções incorporadas não quebram as convenções sintáticas do Go (parênteses, vírgulas entre argumentos, etc.). Eles usam a mesma sintaxe que as funções definidas pelo usuário, mas permitem coisas que não podem ser feitas em funções definidas pelo usuário.

@nathanjsweet Isso já foi considerado (na verdade, foi um descuido), mas torna try não extensível. Consulte https://go-review.googlesource.com/c/proposal/+/181878 .

De maneira mais geral, acho que você está focando sua crítica na coisa errada: as regras especiais para o argumento try não são realmente um problema - praticamente todos os built-in têm regras especiais.

@griesemer obrigado por trabalhar nisso e dedicar um tempo para responder às preocupações da comunidade. Tenho certeza de que você respondeu a muitas das mesmas perguntas neste momento. Percebo que é muito difícil resolver esses problemas e manter a compatibilidade com versões anteriores ao mesmo tempo. Obrigado!

@nathanjsweet Em relação ao seu comentário aqui :

Veja a seção Conclusão que fala com destaque sobre o papel dos built-ins em Go.

Em relação aos seus comentários sobre try estendendo os built-ins de diferentes maneiras: Sim, o requisito que unsafe.Offsetof coloca em seu argumento é diferente daquele de try . Mas ambos esperam sintaticamente uma expressão. Ambos têm algumas restrições adicionais nessa expressão. O requisito para try se encaixa tão facilmente na sintaxe do Go que nenhuma das ferramentas de análise de front-end precisa ser ajustada. Entendo que me pareça incomum para você, mas isso não é o mesmo que uma razão técnica contra isso.

@griesemer o _tryhard_ mais recente conta "manipuladores de erros complexos", mas não "manipuladores de erros de instrução única". Isso poderia ser adicionado?

@networkimprov O que é um manipulador de erros de instrução única? Um bloco if que contém uma única instrução sem retorno?

@griesemer , um manipulador de erro de instrução única é um bloco if err != nil que contém _any_ instrução única, incluindo um retorno.

@networkimprov Pronto. "manipuladores complexos" agora são divididos em "instrução única, depois ramificação" e "complexo, depois ramificação".

Dito isso, observe que essas contagens podem ser enganosas: Por exemplo, essas contagens incluem qualquer instrução if que verifica qualquer variável em relação a zero (se -err="" que agora é o padrão para tryhard ). Eu deveria consertar isso. Resumindo, como tryhard superestima muito o número de oportunidades de manipulador complexo ou de instrução única. Por exemplo, veja archive/tar/common.go , linha 701.

@networkimprov tryhard agora fornece contagens mais precisas sobre por que uma verificação de erro não é um candidato try . O número geral de contagens de try permanece inalterado, mas o número de oportunidades para manipuladores mais simples e complexos agora é mais preciso (e aproximadamente 50% menor do que era antes, porque antes de qualquer then complexo ramificação de uma instrução if foi considerada desde que if contivesse uma verificação <varname> != nil , envolvendo verificação de erros ou não).

Se alguém quiser experimentar try de uma maneira um pouco mais prática, criei um playground WASM aqui com uma implementação de protótipo:

https://ccbrown.github.io/wasm-go-playground/experimental/try-builtin/

E se alguém estiver realmente interessado em compilar código localmente com try, tenho um Go fork com o que acredito ser uma implementação totalmente funcional/atualizada aqui: https://github.com/ccbrown/go/pull/1

eu gosto de 'tentar'. Acho que gerenciar o estado local de err e usar := vs = com err, junto com importações associadas, é uma distração regular. além disso, não vejo isso como criar duas maneiras de fazer a mesma coisa, mais como dois casos, um onde você deseja passar um erro sem agir sobre ele, o outro onde você deseja explicitamente lidar com isso na função de chamada por exemplo. exploração madeireira.

Corri tryhard em um pequeno projeto interno no qual trabalhei há mais de um ano. O diretório em questão tem o código para 3 servidores ("microservices", suponho), um rastreador que é executado periodicamente como um cron job e algumas ferramentas de linha de comando. Ele também possui testes de unidade bastante abrangentes. (FWIW, as várias peças estão funcionando sem problemas há mais de um ano e provou ser simples depurar e resolver quaisquer problemas que surjam)

Aqui estão as estatísticas:

--- stats ---
    370 (100.0% of     370) func declarations
    115 ( 31.1% of     370) func declarations returning an error
   1159 (100.0% of    1159) statements
    258 ( 22.3% of    1159) if statements
    123 ( 47.7% of     258) if <err> != nil statements
     64 ( 52.0% of     123) try candidates
      0 (  0.0% of     123) <err> name is different from "err"
--- non-try candidates (-l flag lists file positions) ---
     54 ( 43.9% of     123) { return ... zero values ..., expr }
      2 (  1.6% of     123) single statement then branch
      3 (  2.4% of     123) complex then branch; cannot use try
      1 (  0.8% of     123) non-empty else branch; cannot use try

Alguns comentários:
1) 50% de todas as instruções if nesta base de código estavam fazendo verificação de erros, e try poderia substituir ~ metade delas. Isso significa que um quarto de todas as instruções if nesta (pequena) base de código são uma versão digitada de try .

2) Devo observar que isso é surpreendentemente alto para mim, porque algumas semanas antes de iniciar este projeto, li sobre uma família de funções auxiliares internas ( status.Annotate ) que anotam uma mensagem de erro, mas preservam a código de status gRPC. Por exemplo, se você chamar um RPC e ele retornar um erro com um código de status associado de PERMISSION_DENIED, o erro retornado dessa função auxiliar ainda terá um código de status associado de PERMISSION_DENIED (e teoricamente, se esse código de status associado foi propagado por todos os caminho até um manipulador RPC, então o RPC falharia com esse código de status associado). Resolvi usar essas funções para tudo neste novo projeto. Mas, aparentemente, para 50% de todos os erros, eu simplesmente propaguei um erro sem anotações. (Antes de correr tryhard , eu tinha previsto 10%).

3) status.Annotate preserva os erros nil (ou seja status.Annotatef(err, "some message: %v", x) retornará nil se err == nil ). Examinei todos os candidatos que não foram da primeira categoria e parece que todos seriam passíveis de reescrever o seguinte:

```
// Before
enc, err := keymaster.NewEncrypter(encKeyring)                                                     
if err != nil {                                                                                    
  return status.Annotate(err, "failed to create encrypter")                                        
}

// After
enc, err := keymaster.NewEncrypter(encKeyring)                                                                                                                                                                  
try(status.Annotate(err, "failed to create encrypter"))
```

To be clear, I'm not saying this transformation is always necessarily a good idea, but it seemed worth mentioning since it boosts the count significantly to a bit under half of all `if` statements.

4) A anotação de erro baseada defer parece um pouco ortogonal a try , para ser honesto, já que funcionará com e sem try . Mas enquanto examinava o código deste projeto, já que estava analisando atentamente o tratamento de erros, notei várias instâncias em que os erros gerados pelo chamado fariam mais sentido. Como exemplo, notei várias instâncias de código chamando clientes gRPC assim:

```
resp, err := s.someClient.SomeMethod(ctx, req)
if err != nil {
  return ..., status.Annotate(err, "failed to call SomeMethod")
}
```

This is actually a bit redundant in retrospect, since gRPC already prefixes its errors with something like "/Service.Method to [ip]:port : ".

There was also code that called standard library functions using the same pattern:

```
hreq, err := http.NewRequest("GET", targetURL, nil)
if err != nil {
  return status.Annotate(err, "http.NewRequest failed")
}
```

In retrospect, this code demonstrates two issues: first, `http.NewRequest` isn't calling a gRPC API, so using `status.Annotate` was unnecessary, and second, assuming the standard library also return errors with callee context, this particular use of error annotation was unnecessary (although I am fairly certain the standard library does not consistently follow this pattern).

De qualquer forma, achei um exercício interessante voltar a este projeto e observar atentamente como ele lidou com os erros.

Uma coisa, @griesemer : tryhard tem o denominador certo para "candidatos que não tentam"?
Edit: respondido abaixo, eu interpretei mal as estatísticas.

EDIT: O que era para ser feedback se transformou em uma proposta, que nos foi explicitamente solicitado a não fazer aqui. Mudei meu comentário para uma essência .

@balasanjay Obrigado pelo seu comentário baseado em fatos; isso é muito útil.

Em relação à sua pergunta sobre tryhard : Os "candidatos não-experimentados" (melhor sugestão de título bem-vinda) são simplesmente o número de casos em que a instrução if atendeu a todos os critérios para uma "verificação de erros" (ou seja, , tivemos o que parecia ser uma atribuição a uma variável de erro <err> , seguida por uma verificação de if <err> != nil na fonte), mas onde não podemos usar try facilmente por causa de o código nos blocos if . Especificamente, em ordem de aparição na saída "não-experimentar candidatos", estas são instruções if que têm uma instrução return que retorna algo diferente de <err> no final, if instruções com uma única instrução return (ou outra) mais complexa, instruções if com várias instruções na ramificação "then" e instruções if com ramo else não vazio. Algumas dessas instruções if podem ter várias dessas condições satisfeitas simultaneamente, portanto, esses números não se somam. Eles têm a intenção de dar uma ideia do que deu errado para que try seja utilizável.

Eu fiz os ajustes mais recentes para isso hoje (você executou a versão mais recente). Antes da última alteração, algumas dessas condições eram contadas mesmo que não houvesse verificação de erros envolvida, o que parecia fazer menos sentido porque parecia que try não poderia ser usado em muitos outros casos quando na verdade try não fazia sentido nesses casos em primeiro lugar.

Mais importante, porém, para uma determinada base de código, o número geral de try candidatos não mudou com esses refinamentos, uma vez que as condições relevantes para try permaneceram as mesmas.

Se você tiver uma sugestão melhor de como e/ou o que medir, ficarei feliz em ouvir isso. Fiz vários ajustes com base no feedback da comunidade. Obrigado.

@subfuzion Obrigado pelo seu comentário, mas não estamos procurando propostas alternativas. Consulte https://github.com/golang/go/issues/32437#issuecomment -501878888 . Obrigado.

No interesse de ser contado, independentemente do resultado:

Eu sou da opinião, junto com minha equipe, que enquanto o framework try proposto por Rob é uma ideia razoável e interessante, ele não atinge o nível em que seria apropriado como embutido. Um pacote de biblioteca padrão seria uma abordagem muito mais apropriada até que os padrões de uso sejam estabelecidos na prática. Se try entrasse na linguagem dessa forma, nós a usaríamos em vários lugares diferentes.

Em uma nota mais geral, vale a pena preservar a combinação do Go de uma linguagem central muito estável e uma biblioteca padrão muito rica. Quanto mais devagar a equipe de idiomas avançar nas mudanças de idioma principal, melhor. O pipeline x -> stdlib continua sendo uma abordagem forte para esse tipo de coisa.

@griesemer Ah, desculpe. Eu interpretei mal as estatísticas, está usando o contador "if err != nil statement" (123) como denominador, não o contador "try candidate" (64) como denominador. Eu vou atacar essa pergunta.

Obrigado!

@mattpalmer Os padrões de uso se estabeleceram por cerca de uma década. São esses padrões de uso exatos que influenciaram diretamente o design de try . A quais padrões de uso você se refere?

@griesemer Desculpe, a culpa é minha - o que começou na minha mente explicando o que me incomodou em try se transformou em sua própria proposta de não adicioná-lo. Isso foi claramente contra as regras básicas estabelecidas (sem mencionar que, diferentemente desta proposta para uma nova função integrada, ela introduz um novo operador). Seria útil excluir o comentário para manter a conversa simplificada (ou isso é considerado uma forma ruim)?

@subfuzion eu não me preocuparia com isso. É uma sugestão controversa e há muitas propostas. Muitos são estranhos

Nós repetimos esse design várias vezes e solicitamos feedback de muitas pessoas antes de nos sentirmos confortáveis ​​o suficiente para publicá-lo e recomendar avançar para a fase real do experimento, mas ainda não fizemos o experimento. Faz sentido voltar à prancheta se o experimento falhar ou se o feedback nos disser antecipadamente que ele claramente falhará.

@griesemer você pode elaborar as métricas específicas que a equipe usará para estabelecer o sucesso ou o fracasso do experimento?

@eu e

Eu perguntei isso ao @rsc há um tempo atrás (https://github.com/golang/go/issues/32437#issuecomment-503245958):

@rsc
Não faltarão locais onde essa comodidade possa ser colocada. Que métrica está sendo buscada para provar a substância do mecanismo além disso? Existe uma lista de casos de tratamento de erros classificados? Como o valor será derivado dos dados quando grande parte do processo público é impulsionado pelo sentimento?

A resposta foi proposital, mas sem inspiração e sem substância (https://github.com/golang/go/issues/32437#issuecomment-503295558):

A decisão é baseada em quão bem isso funciona em programas reais. Se as pessoas nos mostrarem que try é ineficaz na maior parte de seu código, isso é um dado importante. O processo é conduzido por esse tipo de dados. Não é movido pelo sentimento.

Sentimentos adicionais foram oferecidos (https://github.com/golang/go/issues/32437#issuecomment-503408184):

Fiquei surpreso ao encontrar um caso em que try levou a um código claramente melhor, de uma maneira que não havia sido discutida antes.

Eventualmente, eu respondi minha própria pergunta "Existe uma lista de casos de tratamento de erros classificados?". Haverá efetivamente 6 modos de tratamento de erros - Manual Direto, Manual de Passagem, Manual Indireto, Automático Direto, Automático de Passagem, Automático Indireto. Atualmente, é comum usar apenas 2 desses modos. Os modos indiretos, que têm um esforço significativo em sua facilitação, parecem fortemente proibitivos para a maioria dos esquilos veteranos e essa preocupação parece estar sendo ignorada. (https://github.com/golang/go/issues/32437#issuecomment-507332843).

Além disso, sugeri que as transformações automatizadas fossem verificadas antes da transformação para tentar garantir o valor dos resultados (https://github.com/golang/go/issues/32437#issuecomment-507497656). Com o tempo, felizmente, mais dos resultados oferecidos parecem ter melhores retrospectivas, mas isso ainda não aborda o impacto dos métodos indiretos de maneira sóbria e concertada. Afinal (na minha opinião), assim como os usuários devem ser tratados como hostis, os desenvolvedores devem ser tratados como preguiçosos.

A falha da abordagem atual para perder candidatos valiosos também foi apontada (https://github.com/golang/go/issues/32437#issuecomment-507505243).

Eu acho que vale a pena ser barulhento sobre esse processo ser geralmente carente e notavelmente surdo.

@iand A resposta dada por @rsc ainda é válida. Não tenho certeza de qual parte dessa resposta é "falta de substância" ou o que é preciso para ser "inspirador". Mas deixe-me tentar adicionar mais "substância":

O objetivo do processo de avaliação da proposta é identificar, em última análise, "se uma mudança gerou os benefícios esperados ou criou custos inesperados" (etapa 5 do processo).

Passamos na etapa 1: a equipe Go selecionou propostas específicas que parecem valer a pena aceitar; esta proposta é uma delas. Nós não a teríamos selecionado se não tivéssemos pensado muito sobre isso e considerado que valeu a pena. Especificamente, acreditamos que há uma quantidade significativa de clichê no código Go relacionado apenas ao tratamento de erros. A proposta também não vem do nada - estamos discutindo isso há mais de um ano de várias formas.

Estamos atualmente na etapa 2, portanto, ainda um pouco longe de uma decisão final. A etapa 2 é para coletar feedback e preocupações - que parecem ser muitas. Mas para ficar claro aqui: até agora houve apenas um único comentário apontando uma deficiência _técnica_ com o design, que corrigimos . Houve também alguns comentários com dados concretos baseados em código real que indicavam que try realmente reduziria o clichê e simplificaria o código; e houve alguns comentários - também baseados em dados em código real - que mostraram que try não ajudaria muito. Esse feedback concreto, baseado em dados reais, ou apontando deficiências técnicas, é acionável e muito útil. Vamos absolutamente levar isso em consideração.

E depois houve a grande quantidade de comentários que são essencialmente sentimentos pessoais. Isso é menos acionável. Isso não quer dizer que estamos ignorando. Mas só porque estamos aderindo ao processo não significa que somos "surdos".

Em relação a estes comentários: Há talvez duas, talvez três dúzias de oponentes vocais desta proposta - você sabe quem você é. Eles estão dominando essa discussão com postagens frequentes, às vezes várias por dia. Há pouca informação nova a ser obtida a partir disso. O aumento do número de postagens também não reflete um sentimento "mais forte" da comunidade; significa apenas que essas pessoas são mais vocais do que outras.

@iand A resposta dada por @rsc ainda é válida. Não tenho certeza de qual parte dessa resposta é "falta de substância" ou o que é preciso para ser "inspirador". Mas deixe-me tentar adicionar mais "substância":

@griesemer Tenho certeza de que não foi intencional, mas gostaria de observar que nenhuma das palavras que você citou foi minha, mas de um comentarista posterior.

Fora isso, espero que, além de reduzir o clichê e simplificar o sucesso do try , seja avaliado se ele nos permite escrever um código melhor e mais claro.

@iand De fato - isso foi apenas um descuido meu. Me desculpe.

Acreditamos que try nos permite escrever código mais legível - e muitas das evidências que recebemos de código real e nossos próprios experimentos com tryhard mostram limpezas significativas. Mas a legibilidade é mais subjetiva e mais difícil de quantificar.

@griesemer

A quais padrões de uso você se refere?

Estou me referindo aos padrões de uso que se desenvolverão em torno de try ao longo do tempo, não o padrão de verificação nula existente para lidar com erros. O potencial de uso indevido e abuso é uma grande incógnita, especialmente com o fluxo contínuo de programadores que usaram versões semanticamente diferentes de try-catch em outras linguagens.

Tudo isto e as considerações sobre a estabilidade a longo prazo da linguagem core levam-me a pensar que introduzir esta funcionalidade ao nível dos pacotes x ou da biblioteca standard (seja como pacote errors/try ou como errors.Try() ) seria preferível a introduzi-lo como embutido.

@mattparlmer Corrija-me se estiver errado, mas acredito que essa proposta teria que estar no tempo de execução do Go para usar g's, m's (necessário para substituir o fluxo de execução).

@fabian-f

@mattparlmer Corrija-me se estiver errado, mas acredito que essa proposta teria que estar no tempo de execução do Go para usar g's, m's (necessário para substituir o fluxo de execução).

Esse não é o caso; como observa o documento de design , é implementável como uma transformação de árvore de sintaxe em tempo de compilação.

Isso é possível porque a semântica de try pode ser totalmente expressa em termos de if e return ; ele realmente não "substitui o fluxo de execução" mais do que if e return .

Aqui está um relatório tryhard da base de código Go de 300 mil linhas da minha empresa:

Execução inicial:

--- stats ---
  13879 (100.0% of   13879) func declarations
   4381 ( 31.6% of   13879) func declarations returning an error
  38435 (100.0% of   38435) statements
   8028 ( 20.9% of   38435) if statements
   4496 ( 56.0% of    8028) if <err> != nil statements
    453 ( 10.1% of    4496) try candidates
      4 (  0.1% of    4496) <err> name is different from "err"
--- non-try candidates (-l flag lists file positions) ---
   3066 ( 68.2% of    4496) { return ... zero values ..., expr }
    356 (  7.9% of    4496) single statement then branch
    345 (  7.7% of    4496) complex then branch; cannot use try
     63 (  1.4% of    4496) non-empty else branch; cannot use try

Temos uma convenção de usar o pacote errgo do juju (https://godoc.org/github.com/juju/errgo) para mascarar erros e adicionar informações de rastreamento de pilha a eles, o que impediria a maioria das reescritas. Isso significa que é improvável que adotemos try , pelo mesmo motivo que geralmente evitamos retornos de erro nu.

Como parece ser uma métrica útil, removi as chamadas errgo.Mask() (que retornam o erro sem anotação) e executei novamente tryhard . Esta é uma estimativa de quantas verificações de erro poderiam ser reescritas se não usássemos errgo:

--- stats ---
  13879 (100.0% of   13879) func declarations
   4381 ( 31.6% of   13879) func declarations returning an error
  38435 (100.0% of   38435) statements
   8028 ( 20.9% of   38435) if statements
   4496 ( 56.0% of    8028) if <err> != nil statements
   3114 ( 69.3% of    4496) try candidates
      7 (  0.2% of    4496) <err> name is different from "err"
--- non-try candidates (-l flag lists file positions) ---
    381 (  8.5% of    4496) { return ... zero values ..., expr }
    358 (  8.0% of    4496) single statement then branch
    345 (  7.7% of    4496) complex then branch; cannot use try
     63 (  1.4% of    4496) non-empty else branch; cannot use try

Então, acho que ~ 70% dos retornos de erro seriam compatíveis com try .

Por fim, minha principal preocupação com a proposta não parece ser capturada em nenhum dos comentários que li nem nos resumos das discussões:

Esta proposta aumenta significativamente o custo relativo da anotação de erros.

Atualmente, o custo marginal de adicionar algum contexto a um erro é muito baixo; é pouco mais do que digitar a string de formato. Se essa proposta fosse adotada, eu me preocupo que os engenheiros prefiram cada vez mais a estética oferecida por try , tanto porque faz seu código "parecer mais elegante" (o que lamento dizer que é uma consideração para algumas pessoas, na minha experiência) e agora requer um bloco adicional para adicionar contexto. Eles poderiam justificá-lo com base em um argumento de "legibilidade", como adicionar contexto expande o método em mais 3 linhas e distrai o leitor do ponto principal. Eu acho que as bases de código corporativas são diferentes da biblioteca padrão Go no sentido de que facilitar fazer a coisa certa provavelmente tem um impacto mensurável na qualidade do código resultante, as revisões de código são de qualidade variável e as práticas da equipe variam independentemente umas das outras . De qualquer forma, como você disse antes, nem sempre poderíamos adotar try para nossa base de código.

Obrigado pela consideração

@mattparlmer

Tudo isto e as considerações sobre a estabilidade a longo prazo da linguagem core levam-me a pensar que introduzir esta funcionalidade ao nível dos pacotes x ou da biblioteca standard (seja como pacote errors/try ou como errors.Try() ) seria preferível a introduzi-lo como embutido.

try não pode ser implementado como uma função de biblioteca; não há como uma função retornar de seu chamador (habilitando que foi proposto como #32473) e, como a maioria dos outros built-ins, também não há como expressar a assinatura de try em Go. Mesmo com os genéricos, é improvável que isso se torne possível; veja o documento de design FAQ , perto do final.

Além disso, implementar try como uma função de biblioteca exigiria que ela tivesse um nome mais detalhado, o que anula parcialmente o objetivo de usá-lo.

No entanto, pode ser - e foi duas vezes - implementado como um pré-processador de código-fonte: consulte https://github.com/rhysd/trygo e https://github.com/lunixbochs/og.

Parece que ~60% da base de código do tegola seria capaz de usar esse recurso.

Aqui está a saída de tryhard para o projeto tegola: (http://github.com/go-spatial/tegola)

--- try candidates ---
      1  tegola/atlas/atlas.go:84
      2  tegola/atlas/map.go:232
      3  tegola/atlas/map.go:238
      4  tegola/atlas/map.go:248
      5  tegola/atlas/map.go:253
      6  tegola/basic/geometry_math.go:248
      7  tegola/basic/geometry_math.go:251
      8  tegola/basic/geometry_math.go:268
      9  tegola/basic/geometry_math.go:276
     10  tegola/basic/json_marshal.go:33
     11  tegola/basic/json_marshal.go:153
     12  tegola/basic/json_marshal.go:276
     13  tegola/cache/azblob/azblob.go:54
     14  tegola/cache/azblob/azblob.go:61
     15  tegola/cache/azblob/azblob.go:67
     16  tegola/cache/azblob/azblob.go:74
     17  tegola/cache/azblob/azblob.go:80
     18  tegola/cache/azblob/azblob.go:105
     19  tegola/cache/azblob/azblob.go:109
     20  tegola/cache/azblob/azblob.go:204
     21  tegola/cache/azblob/azblob.go:259
     22  tegola/cache/file/file.go:42
     23  tegola/cache/file/file.go:56
     24  tegola/cache/file/file.go:110
     25  tegola/cache/file/file.go:116
     26  tegola/cache/file/file.go:129
     27  tegola/cache/redis/redis.go:41
     28  tegola/cache/redis/redis.go:46
     29  tegola/cache/redis/redis.go:51
     30  tegola/cache/redis/redis.go:56
     31  tegola/cache/redis/redis.go:70
     32  tegola/cache/redis/redis.go:79
     33  tegola/cache/redis/redis.go:84
     34  tegola/cache/s3/s3.go:85
     35  tegola/cache/s3/s3.go:102
     36  tegola/cache/s3/s3.go:112
     37  tegola/cache/s3/s3.go:118
     38  tegola/cache/s3/s3.go:123
     39  tegola/cache/s3/s3.go:138
     40  tegola/cache/s3/s3.go:164
     41  tegola/cache/s3/s3.go:172
     42  tegola/cache/s3/s3.go:179
     43  tegola/cache/s3/s3.go:284
     44  tegola/cache/s3/s3.go:340
     45  tegola/cmd/tegola/cmd/cache/format.go:97
     46  tegola/cmd/tegola/cmd/cache/seed_purge.go:94
     47  tegola/cmd/tegola/cmd/cache/seed_purge.go:103
     48  tegola/cmd/tegola/cmd/cache/seed_purge.go:170
     49  tegola/cmd/tegola/cmd/cache/tile_list.go:51
     50  tegola/cmd/tegola/cmd/cache/tile_list.go:64
     51  tegola/cmd/tegola/cmd/cache/tile_name.go:35
     52  tegola/cmd/tegola/cmd/cache/tile_name.go:43
     53  tegola/cmd/tegola/cmd/root.go:58
     54  tegola/cmd/tegola/cmd/root.go:61
     55  tegola/cmd/xyz2svg/cmd/draw.go:62
     56  tegola/cmd/xyz2svg/cmd/draw.go:70
     57  tegola/cmd/xyz2svg/cmd/draw.go:214
     58  tegola/config/config.go:96
     59  tegola/internal/env/parse.go:30
     60  tegola/internal/env/parse.go:69
     61  tegola/internal/env/parse.go:116
     62  tegola/internal/env/parse.go:174
     63  tegola/internal/env/parse.go:221
     64  tegola/internal/env/types.go:67
     65  tegola/internal/env/types.go:86
     66  tegola/internal/env/types.go:105
     67  tegola/internal/env/types.go:124
     68  tegola/internal/env/types.go:143
     69  tegola/maths/makevalid/main.go:189
     70  tegola/maths/makevalid/main.go:207
     71  tegola/maths/makevalid/main.go:221
     72  tegola/maths/makevalid/main.go:295
     73  tegola/maths/makevalid/main.go:504
     74  tegola/maths/makevalid/makevalid.go:77
     75  tegola/maths/makevalid/makevalid.go:89
     76  tegola/maths/makevalid/makevalid.go:118
     77  tegola/maths/makevalid/makevalid_test.go:93
     78  tegola/maths/makevalid/makevalid_test.go:163
     79  tegola/maths/makevalid/plyg/ring.go:518
     80  tegola/maths/triangle.go:1023
     81  tegola/mvt/layer.go:73
     82  tegola/mvt/layer.go:79
     83  tegola/mvt/vector_tile/vector_tile.pb.go:64
     84  tegola/provider/gpkg/gpkg.go:138
     85  tegola/provider/gpkg/gpkg.go:223
     86  tegola/provider/gpkg/gpkg_register.go:46
     87  tegola/provider/gpkg/gpkg_register.go:51
     88  tegola/provider/gpkg/gpkg_register.go:186
     89  tegola/provider/gpkg/gpkg_register.go:227
     90  tegola/provider/gpkg/gpkg_register.go:240
     91  tegola/provider/gpkg/gpkg_register.go:245
     92  tegola/provider/gpkg/gpkg_register.go:256
     93  tegola/provider/gpkg/gpkg_register.go:377
     94  tegola/provider/postgis/postgis.go:112
     95  tegola/provider/postgis/postgis.go:117
     96  tegola/provider/postgis/postgis.go:122
     97  tegola/provider/postgis/postgis.go:127
     98  tegola/provider/postgis/postgis.go:136
     99  tegola/provider/postgis/postgis.go:142
    100  tegola/provider/postgis/postgis.go:148
    101  tegola/provider/postgis/postgis.go:153
    102  tegola/provider/postgis/postgis.go:158
    103  tegola/provider/postgis/postgis.go:163
    104  tegola/provider/postgis/postgis.go:181
    105  tegola/provider/postgis/postgis.go:198
    106  tegola/provider/postgis/postgis.go:264
    107  tegola/provider/postgis/postgis.go:441
    108  tegola/provider/postgis/postgis.go:446
    109  tegola/provider/postgis/postgis.go:529
    110  tegola/provider/postgis/postgis.go:559
    111  tegola/provider/postgis/postgis.go:603
    112  tegola/provider/postgis/util.go:31
    113  tegola/provider/postgis/util.go:36
    114  tegola/provider/postgis/util.go:200
    115  tegola/server/bindata/bindata.go:89
    116  tegola/server/bindata/bindata.go:109
    117  tegola/server/bindata/bindata.go:129
    118  tegola/server/bindata/bindata.go:149
    119  tegola/server/bindata/bindata.go:169
    120  tegola/server/bindata/bindata.go:189
    121  tegola/server/bindata/bindata.go:209
    122  tegola/server/bindata/bindata.go:229
    123  tegola/server/bindata/bindata.go:370
    124  tegola/server/bindata/bindata.go:374
    125  tegola/server/bindata/bindata.go:378
    126  tegola/server/bindata/bindata.go:382
    127  tegola/server/bindata/bindata.go:386
    128  tegola/server/bindata/bindata.go:402
    129  tegola/server/middleware_gzip.go:71
    130  tegola/server/middleware_gzip.go:78
    131  tegola/server/server_test.go:85

--- <err> name is different from "err" ---
      1  tegola/basic/json_marshal.go:276

--- { return ... zero values ..., expr } ---
      1  tegola/basic/geometry_math.go:214
      2  tegola/basic/geometry_math.go:222
      3  tegola/basic/geometry_math.go:230
      4  tegola/cache/azblob/azblob.go:131
      5  tegola/cache/azblob/azblob.go:140
      6  tegola/cache/azblob/azblob.go:149
      7  tegola/cache/azblob/azblob.go:171
      8  tegola/cache/file/file.go:47
      9  tegola/cache/s3/s3.go:92
     10  tegola/cmd/internal/register/maps.go:108
     11  tegola/cmd/tegola/cmd/cache/flags.go:20
     12  tegola/cmd/tegola/cmd/cache/tile_name.go:51
     13  tegola/cmd/tegola/cmd/cache/worker.go:112
     14  tegola/cmd/tegola/cmd/cache/worker.go:123
     15  tegola/cmd/tegola/cmd/root.go:73
     16  tegola/cmd/tegola/cmd/root.go:78
     17  tegola/cmd/xyz2svg/cmd/root.go:60
     18  tegola/provider/gpkg/gpkg.go:90
     19  tegola/provider/gpkg/gpkg.go:95
     20  tegola/provider/gpkg/gpkg_register.go:264
     21  tegola/provider/gpkg/gpkg_register.go:297
     22  tegola/provider/gpkg/gpkg_register.go:302
     23  tegola/provider/gpkg/gpkg_register.go:313
     24  tegola/provider/gpkg/gpkg_register.go:328
     25  tegola/provider/postgis/postgis.go:193
     26  tegola/provider/postgis/postgis.go:208
     27  tegola/provider/postgis/postgis.go:222
     28  tegola/provider/postgis/postgis.go:228
     29  tegola/provider/postgis/postgis.go:234
     30  tegola/provider/postgis/postgis.go:243
     31  tegola/provider/postgis/postgis.go:249
     32  tegola/provider/postgis/postgis.go:255
     33  tegola/provider/postgis/postgis.go:304
     34  tegola/provider/postgis/postgis.go:315
     35  tegola/provider/postgis/postgis.go:319
     36  tegola/provider/postgis/postgis.go:364
     37  tegola/provider/postgis/postgis.go:456
     38  tegola/provider/postgis/postgis.go:520
     39  tegola/provider/postgis/postgis.go:534
     40  tegola/provider/postgis/postgis.go:565
     41  tegola/provider/postgis/util.go:108
     42  tegola/provider/postgis/util.go:113
     43  tegola/server/bindata/bindata.go:29
     44  tegola/server/bindata/bindata.go:245
     45  tegola/server/bindata/bindata.go:271
     46  tegola/server/bindata/bindata.go:396

--- single statement then branch ---
      1  tegola/cache/azblob/azblob.go:241
      2  tegola/cache/file/file.go:87
      3  tegola/cache/s3/s3.go:321
      4  tegola/cmd/internal/register/caches.go:18
      5  tegola/cmd/internal/register/providers.go:43
      6  tegola/cmd/internal/register/providers.go:62
      7  tegola/cmd/internal/register/providers.go:75
      8  tegola/config/config.go:192
      9  tegola/config/config.go:207
     10  tegola/config/config.go:217
     11  tegola/internal/env/dict.go:43
     12  tegola/internal/env/dict.go:121
     13  tegola/internal/env/dict.go:197
     14  tegola/internal/env/dict.go:273
     15  tegola/internal/env/dict.go:348
     16  tegola/internal/env/parse.go:79
     17  tegola/internal/env/parse.go:126
     18  tegola/internal/env/parse.go:184
     19  tegola/internal/env/parse.go:231
     20  tegola/maths/makevalid/plyg/ring.go:541
     21  tegola/maths/maths.go:239
     22  tegola/maths/validate/validate.go:49
     23  tegola/maths/validate/validate.go:53
     24  tegola/maths/validate/validate.go:59
     25  tegola/maths/validate/validate.go:69
     26  tegola/mvt/feature.go:94
     27  tegola/mvt/feature.go:99
     28  tegola/mvt/feature.go:592
     29  tegola/mvt/feature.go:603
     30  tegola/mvt/layer.go:90
     31  tegola/mvt/tile.go:48
     32  tegola/provider/postgis/postgis.go:570
     33  tegola/provider/postgis/postgis.go:586
     34  tegola/tile.go:172

--- complex then branch; cannot use try ---
      1  tegola/cache/azblob/azblob.go:226
      2  tegola/cache/file/file.go:78
      3  tegola/cache/file/file.go:122
      4  tegola/cache/s3/s3.go:195
      5  tegola/cache/s3/s3.go:206
      6  tegola/cache/s3/s3.go:219
      7  tegola/cache/s3/s3.go:307
      8  tegola/provider/gpkg/gpkg.go:39
      9  tegola/provider/gpkg/gpkg.go:45
     10  tegola/provider/gpkg/gpkg.go:131
     11  tegola/provider/gpkg/gpkg.go:154
     12  tegola/provider/gpkg/gpkg_register.go:171
     13  tegola/provider/gpkg/gpkg_register.go:195

--- stats ---
   1294 (100.0% of    1294) func declarations
    246 ( 19.0% of    1294) func declarations returning an error
   2693 (100.0% of    2693) statements
    551 ( 20.5% of    2693) if statements
    238 ( 43.2% of     551) if <err> != nil statements
    131 ( 55.0% of     238) try candidates
      1 (  0.4% of     238) <err> name is different from "err"
--- non-try candidates ---
     46 ( 19.3% of     238) { return ... zero values ..., expr }
     34 ( 14.3% of     238) single statement then branch
     13 (  5.5% of     238) complex then branch; cannot use try
      0 (  0.0% of     238) non-empty else branch; cannot use try

E o projeto complementar: (http://github.com/go-spatial/geom)

--- try candidates ---
      1  geom/bbox.go:202
      2  geom/encoding/geojson/geojson.go:152
      3  geom/encoding/geojson/geojson.go:157
      4  geom/encoding/wkb/internal/tcase/symbol/symbol.go:73
      5  geom/encoding/wkb/internal/tcase/tcase.go:161
      6  geom/encoding/wkb/internal/tcase/tcase.go:172
      7  geom/encoding/wkb/wkb.go:50
      8  geom/encoding/wkb/wkb.go:110
      9  geom/encoding/wkt/internal/token/token.go:176
     10  geom/encoding/wkt/internal/token/token.go:252
     11  geom/internal/parsing/parsing.go:44
     12  geom/internal/parsing/parsing.go:85
     13  geom/internal/rtreego/rtree_test.go:110
     14  geom/multi_line_string.go:34
     15  geom/multi_polygon.go:35
     16  geom/planar/clip/linestring.go:82
     17  geom/planar/clip/linestring.go:181
     18  geom/planar/clip/point.go:23
     19  geom/planar/intersect/xsweep.go:106
     20  geom/planar/makevalid/makevalid.go:92
     21  geom/planar/makevalid/makevalid.go:191
     22  geom/planar/makevalid/setdiff/polygoncleaner.go:283
     23  geom/planar/makevalid/setdiff/polygoncleaner.go:345
     24  geom/planar/makevalid/setdiff/polygoncleaner.go:543
     25  geom/planar/makevalid/setdiff/polygoncleaner.go:554
     26  geom/planar/makevalid/setdiff/polygoncleaner.go:572
     27  geom/planar/makevalid/setdiff/polygoncleaner.go:578
     28  geom/planar/simplify/douglaspeucker.go:84
     29  geom/planar/simplify/douglaspeucker.go:88
     30  geom/planar/simplify.go:13
     31  geom/planar/triangulate/constraineddelaunay/triangle.go:186
     32  geom/planar/triangulate/constraineddelaunay/triangulator.go:134
     33  geom/planar/triangulate/constraineddelaunay/triangulator.go:138
     34  geom/planar/triangulate/constraineddelaunay/triangulator.go:142
     35  geom/planar/triangulate/constraineddelaunay/triangulator.go:173
     36  geom/planar/triangulate/constraineddelaunay/triangulator.go:176
     37  geom/planar/triangulate/constraineddelaunay/triangulator.go:203
     38  geom/planar/triangulate/constraineddelaunay/triangulator.go:248
     39  geom/planar/triangulate/constraineddelaunay/triangulator.go:396
     40  geom/planar/triangulate/constraineddelaunay/triangulator.go:466
     41  geom/planar/triangulate/constraineddelaunay/triangulator.go:553
     42  geom/planar/triangulate/constraineddelaunay/triangulator.go:583
     43  geom/planar/triangulate/constraineddelaunay/triangulator.go:667
     44  geom/planar/triangulate/constraineddelaunay/triangulator.go:672
     45  geom/planar/triangulate/constraineddelaunay/triangulator.go:677
     46  geom/planar/triangulate/constraineddelaunay/triangulator.go:814
     47  geom/planar/triangulate/constraineddelaunay/triangulator.go:818
     48  geom/planar/triangulate/constraineddelaunay/triangulator.go:823
     49  geom/planar/triangulate/constraineddelaunay/triangulator.go:865
     50  geom/planar/triangulate/constraineddelaunay/triangulator.go:870
     51  geom/planar/triangulate/constraineddelaunay/triangulator.go:875
     52  geom/planar/triangulate/constraineddelaunay/triangulator.go:897
     53  geom/planar/triangulate/constraineddelaunay/triangulator.go:901
     54  geom/planar/triangulate/constraineddelaunay/triangulator.go:907
     55  geom/planar/triangulate/constraineddelaunay/triangulator.go:1107
     56  geom/planar/triangulate/constraineddelaunay/triangulator.go:1146
     57  geom/planar/triangulate/constraineddelaunay/triangulator.go:1157
     58  geom/planar/triangulate/constraineddelaunay/triangulator.go:1202
     59  geom/planar/triangulate/constraineddelaunay/triangulator.go:1206
     60  geom/planar/triangulate/constraineddelaunay/triangulator.go:1216
     61  geom/planar/triangulate/delaunaytriangulationbuilder.go:66
     62  geom/planar/triangulate/incrementaldelaunaytriangulator.go:46
     63  geom/planar/triangulate/incrementaldelaunaytriangulator.go:78
     64  geom/planar/triangulate/quadedge/lastfoundquadedgelocator.go:65
     65  geom/planar/triangulate/quadedge/quadedgesubdivision.go:976
     66  geom/slippy/tile.go:133

--- { return ... zero values ..., expr } ---
      1  geom/internal/parsing/parsing.go:125
      2  geom/planar/triangulate/constraineddelaunay/triangulator.go:428
      3  geom/planar/triangulate/constraineddelaunay/triangulator.go:447
      4  geom/planar/triangulate/constraineddelaunay/triangulator.go:460

--- single statement then branch ---
      1  geom/bbox.go:259
      2  geom/encoding/wkb/internal/decode/decode.go:29
      3  geom/encoding/wkb/internal/decode/decode.go:55
      4  geom/encoding/wkb/internal/decode/decode.go:63
      5  geom/encoding/wkb/internal/decode/decode.go:70
      6  geom/encoding/wkb/internal/decode/decode.go:79
      7  geom/encoding/wkb/internal/decode/decode.go:84
      8  geom/encoding/wkb/internal/decode/decode.go:93
      9  geom/encoding/wkb/internal/decode/decode.go:99
     10  geom/encoding/wkb/internal/decode/decode.go:105
     11  geom/encoding/wkb/internal/decode/decode.go:114
     12  geom/encoding/wkb/internal/decode/decode.go:119
     13  geom/encoding/wkb/internal/decode/decode.go:135
     14  geom/encoding/wkb/internal/decode/decode.go:140
     15  geom/encoding/wkb/internal/decode/decode.go:149
     16  geom/encoding/wkb/internal/decode/decode.go:155
     17  geom/encoding/wkb/internal/decode/decode.go:161
     18  geom/encoding/wkb/internal/decode/decode.go:170
     19  geom/encoding/wkb/internal/decode/decode.go:176
     20  geom/encoding/wkb/internal/tcase/token/token.go:162
     21  geom/encoding/wkt/internal/token/token.go:136

--- complex then branch; cannot use try ---
      1  geom/encoding/wkb/internal/tcase/tcase.go:74
      2  geom/encoding/wkt/internal/symbol/symbol.go:125
      3  geom/planar/intersect/xsweep.go:165
      4  geom/planar/makevalid/makevalid.go:85
      5  geom/planar/makevalid/makevalid.go:172
      6  geom/planar/makevalid/triangulate.go:19
      7  geom/planar/makevalid/triangulate.go:28
      8  geom/planar/makevalid/triangulate.go:36
      9  geom/planar/makevalid/triangulate.go:58
     10  geom/planar/triangulate/constraineddelaunay/triangulator.go:358
     11  geom/planar/triangulate/constraineddelaunay/triangulator.go:373
     12  geom/planar/triangulate/constraineddelaunay/triangulator.go:453
     13  geom/planar/triangulate/constraineddelaunay/triangulator.go:1237
     14  geom/planar/triangulate/constraineddelaunay/triangulator.go:1243
     15  geom/planar/triangulate/constraineddelaunay/triangulator.go:1249

--- stats ---
    820 (100.0% of     820) func declarations
    146 ( 17.8% of     820) func declarations returning an error
   1715 (100.0% of    1715) statements
    391 ( 22.8% of    1715) if statements
    111 ( 28.4% of     391) if <err> != nil statements
     66 ( 59.5% of     111) try candidates
      0 (  0.0% of     111) <err> name is different from "err"
--- non-try candidates ---
      4 (  3.6% of     111) { return ... zero values ..., expr }
     21 ( 18.9% of     111) single statement then branch
     15 ( 13.5% of     111) complex then branch; cannot use try
      0 (  0.0% of     111) non-empty else branch; cannot use try

Sobre o assunto de custos inesperados, repostei isso de #32611...

Vejo três classes de custo:

  1. o custo para a especificação, que é elaborado no documento de projeto.
  2. o custo de ferramentas (ou seja, revisão de software), também explorado no documento de design.
  3. o custo para o ecossistema, que a comunidade detalhou detalhadamente acima e em #32825.

Re nos. 1 e 2, os custos de try() são modestos.

Para simplificar não. 3, a maioria dos comentaristas acredita que try() prejudicaria nosso código e/ou o ecossistema de código do qual dependemos e, assim, reduziria nossa produtividade e qualidade do produto. Essa percepção generalizada e bem fundamentada não deve ser menosprezada como "não factual" ou "estética".

O custo para o ecossistema é muito mais importante do que o custo para as especificações ou ferramentas.

@griesemer é evidentemente injusto afirmar que "três dúzias de oponentes vocais" são a maior parte da oposição. Centenas de pessoas comentaram aqui e no #32825. Você me disse em 12 de junho: "Reconheço que cerca de 2/3 dos entrevistados não estão satisfeitos com a proposta". Desde então, mais de 2.000 pessoas votaram em "deixar err != nil paz" com 90% de aprovação.

@gdey , você poderia alterar sua postagem para incluir apenas _estatísticas e candidatos não testados_?

@robfig , @gdey Obrigado por fornecer esses dados, especialmente a comparação antes/depois.

@griesemer
Você certamente adicionou alguma substância esclarecendo que minhas preocupações (e de outros) podem ser abordadas diretamente. Minha pergunta, então, é se a equipe Go vê o provável abuso dos modos indiretos (ou seja, retornos simples e/ou mutação de erro de escopo pós-função via adiamento) como um custo que vale a pena discutir durante a etapa 5, e que vale a pena potencialmente tomar medidas para sua mitigação. O clima atual é que esse aspecto mais desconcertante da proposta é visto como um recurso inteligente/novo pela equipe Go (essa preocupação não é abordada pela avaliação das transformações automatizadas e parece ser ativamente incentivada/apoiada. - errd , em conversa, etc.).

edit to add... A preocupação com a equipe Go incentivando o que Gophers veteranos veem como proibitivo é o que eu quis dizer com relação à surdez.
... A indireção é um custo com o qual muitos de nós estão profundamente preocupados como uma questão de dor experiencial. Pode não ser algo que possa ser comparado facilmente (se razoavelmente), mas é falso considerar essa preocupação como sentimental em si. Em vez disso, desconsiderar a sabedoria da experiência compartilhada em favor de números simples sem julgamento contextual sólido é o tipo de sentimento contra o qual estamos tentando trabalhar.

@networkimprov Desculpas por não ser claro o suficiente. O que eu disse foi:

Há talvez duas, talvez três dúzias de oponentes vocais desta proposta - você sabe quem você é. Eles estão dominando essa discussão com postagens frequentes, às vezes várias por dia.

Eu estava falando sobre comentários reais (como em "postagens frequentes"), não emojis. Há apenas um número relativamente pequeno de pessoas postando aqui _repetidamente_, o que acredito ainda estar correto. Eu também não estava falando sobre #32825; Eu estava falando sobre essa proposta.

Olhando para os emojis, a situação está praticamente inalterada em relação a um mês atrás: 1/3 dos emojis indicam uma opinião favorável e 2/3 indicam uma opinião negativa.

@griesemer

Lembrei-me de algo enquanto escrevia meu comentário acima: enquanto o documento de design diz que try pode ser implementado como uma transformação direta de árvore de sintaxe e, em muitos casos, esse é obviamente o caso, há alguns casos em que não veja uma maneira simples de fazer isso. Por exemplo, suponha que temos o seguinte:

switch x {
case rand.Int():
  a()
case 5, try(strconv.Atoi(y)):
  b()
}

Dada a ordem de avaliação de switch , não vejo como retirar trivialmente o strconv.Atoi(y) da cláusula case preservando a semântica pretendida; o melhor que consegui é reescrever o switch como a cadeia equivalente de if / else , assim:

if x == rand.Int() {
  a()
} else if x == 5 {
  b()
} else if _v, _err := strconv.Atoi(y); _err != nil {
  return _err
} else if x == _v {
  b()
}

(Há outras situações em que isso pode surgir, mas este é um dos exemplos mais simples e o primeiro que me vem à mente.)

Na verdade, antes de você publicar esta proposta, eu estava trabalhando em um tradutor AST para implementar o operador check do rascunho do projeto e me deparei com esse problema. No entanto, eu estava usando uma versão hackeada dos pacotes go/* stdlib; talvez o front-end do compilador esteja estruturado de uma maneira que facilite isso? Ou eu perdi alguma coisa e realmente existe uma maneira direta de fazer isso?

Veja também https://github.com/rhysd/trygo; de acordo com o README, ele não implementa expressões try e observa essencialmente a mesma preocupação que estou levantando aqui; Eu suspeito que pode ser por isso que o autor não implementou esse recurso.

O código @daved Professional não está sendo desenvolvido no vácuo - existem convenções locais, recomendações de estilo, revisões de código, etc. (eu já disse isso antes). Assim, não vejo por que o abuso seria "provável" (é possível, mas isso é verdade para qualquer construção de linguagem).

Observe que usar defer para decorar erros é possível com ou sem try . Certamente há boas razões para uma função que contém muitas verificações de erros, todas decorando os erros da mesma maneira, para fazer essa decoração uma vez, por exemplo, usando um defer . Ou talvez use uma função wrapper que faça a decoração. Ou qualquer outro mecanismo que se encaixe na conta e nas recomendações de codificação local. Afinal, "erros são apenas valores" e faz todo o sentido escrever e fatorar um código que lide com erros.

Devoluções nuas podem ser problemáticas quando usadas de forma indisciplinada. Isso não significa que eles são geralmente ruins. Por exemplo, se os resultados de uma função são válidos apenas se não houver erro, parece perfeitamente correto usar um retorno nu no caso de um erro - contanto que sejamos disciplinados em definir o erro (como os outros valores de retorno não não importa neste caso). try garante exatamente isso. Não vejo nenhum "abuso" aqui.

@dpinela O compilador já traduz uma instrução switch como a sua como uma sequência de if-else-if , então não vejo problema aqui. Além disso, a "árvore de sintaxe" que o compilador está usando não é a árvore de sintaxe "go/ast". A representação interna do compilador permite um código muito mais flexível que não pode necessariamente ser traduzido de volta para Go.

@griesemer
Sim, claro, tudo o que você está dizendo tem base. A área cinzenta, no entanto, não é tão simplista quanto você está imaginando. Os retornos nus são normalmente tratados com muita cautela por aqueles de nós que ensinam os outros (nós, que nos esforçamos para crescer/promover a comunidade). Eu aprecio que o stdlib o tenha espalhado por toda parte. Mas, ao ensinar os outros, os retornos explícitos são sempre enfatizados. Deixe o indivíduo atingir sua própria maturidade para se voltar para a abordagem mais "fantasiosa", mas incentivá-la desde o início certamente seria promover códigos difíceis de ler (ou seja, maus hábitos). Esta, novamente, é a surdez que estou tentando trazer à luz.

Pessoalmente, não desejo proibir devoluções nuas ou manipulação de valor diferido. Quando eles são realmente adequados, fico feliz que esses recursos estejam disponíveis (embora outros usuários experientes possam adotar uma postura mais rígida). No entanto, encorajar a aplicação dessas características menos comuns e geralmente frágeis de uma maneira tão difundida é completamente a direção oposta que eu imaginei que Go tomaria. A mudança pronunciada no caráter de evitar a magia e formas precárias de indireção é uma mudança proposital? Devemos também começar a enfatizar o uso de DICs e outros mecanismos difíceis de depurar?

ps Seu tempo é muito apreciado. Sua equipe e o idioma tem meu respeito e carinho. Não desejo nenhum pesar para ninguém ao falar; Espero que você ouça a natureza da minha/nossa preocupação e tente ver as coisas de nossa perspectiva de "linha de frente".

Adicionando alguns comentários ao meu downvote.

Para a proposta específica em mãos:

1) Eu preferiria que isso fosse uma palavra-chave versus uma função interna por razões previamente articuladas de fluxo de controle e legibilidade de código.

2) Semanticamente, "tentar" é um pára-raios. E, a menos que haja uma exceção lançada, "try" seria melhor renomeado para algo como guard ou ensure .

3) Além desses dois pontos, acho que essa é a melhor proposta que já vi para esse tipo de coisa.

Mais alguns comentários articulando minha objeção a qualquer adição de um conceito try/guard/ensure vs. deixar if err != nil sozinho:

1) Isso vai contra um dos mandatos originais do golang (pelo menos como eu percebi) de ser explícito, fácil de ler/compreender, com muito pouca 'mágica'.

2) Isso estimulará a preguiça no momento exato em que o pensamento for necessário: "qual é a melhor coisa para o meu código fazer no caso desse erro?". Existem muitos erros que podem surgir ao fazer coisas "clichê", como abrir arquivos, transferir dados por uma rede, etc. Embora você possa começar com um monte de "tentativas" que ignoram cenários de falha não comuns, eventualmente muitos desses trys" desaparecerá, pois você pode precisar implementar suas próprias tarefas de retirada/repetição, registro/rastreamento e/ou limpeza. “Eventos de baixa probabilidade” são garantidos em escala.

Aqui estão mais algumas estatísticas brutas de tryhard . Isso é apenas levemente validado, portanto, sinta-se à vontade para apontar erros. ;-)

Primeiros 20 "Pacotes Populares" em godoc.org

Esses são os repositórios que correspondem aos primeiros 20 Pacotes Populares em https://godoc.org , classificados por porcentagem de candidatos de tentativa. Isso está usando as configurações padrão tryhard , que em teoria deveriam estar excluindo os diretórios vendor .

O valor médio para candidatos de teste nesses 20 repositórios é de 58%.

| projeto | local | se stmts | if != nil (% de if) | tentar candidatos (% de if != nil) |
|---------|-----|---------------|----------------- -----|---------------
| github.com/google/uuid | 1714 | 12 | 16,7% | 0,0% |
| github.com/pkg/errors | 1886 | 10 | 0,0% | 0,0% |
| github.com/aws/aws-sdk-go | 1911309 | 32015 | 9,4% | 8,9% |
| github.com/jinzhu/gorm | 15246 | 44 | 11,4% | 20,0% |
| github.com/robfig/cron | 1911 | 20 | 35,0% | 28,6% |
| github.com/gorilla/websocket | 6959 | 212 | 32,5% | 39,1% |
| github.com/dgrijalva/jwt-go | 3270 | 118 | 29,7% | 40,0% |
| github.com/gomodule/redigo | 7119 | 187 | 34,8% | 41,5% |
| github.com/unixpickle/kahoot-hack | 1743 | 52 | 75,0% | 43,6% |
| github.com/lib/pq | 13396 | 239 | 30,1% | 55,6% |
| github.com/sirupsen/logrus | 5063 | 29 | 17,2% | 60,0% |
| github.com/prometheus/client_golang | 17791 | 194 | 49,0% | 62,1% |
| github.com/go-redis/redis | 21182 | 326 | 42,6% | 73,4% |
| github.com/mongodb/mongo-go-driver | 86605 | 2097 | 37,8% | 73,9% |
| github.com/uber-go/zap | 15363 | 84 | 36,9% | 74,2% |
| github.com/golang/protobuf | 42959 | 685 | 22,9% | 77,1% |
| github.com/gin-gonic/gin | 14574 | 96 | 53,1% | 86,3% |
| github.com/go-pg/pg | 26369 | 831 | 37,7% | 86,9% |
| github.com/Shopify/sarama | 36427 | 1369 | 68,2% | 91,0% |
| github.com/stretchr/testify | 13496 | 32 | 43,8% | 92,9% |

A coluna " if stmts " está apenas registrando instruções if em funções que retornam um erro, que é como tryhard o relata e que, esperançosamente, explica por que é tão baixo para algo como gorm .

10 diversos Projetos Go "grandes"

Dado que os pacotes populares no godoc.org tendem a ser pacotes de bibliotecas, eu também queria verificar as estatísticas de alguns projetos maiores.

Estes são diversos. grandes projetos que passaram a ser top-of-mind para mim (ou seja, nenhuma lógica real por trás desses 10). Isso é novamente classificado por porcentagem de tentativa de candidato.

O valor médio para candidatos de teste nesses 10 repositórios é de 59%.

| projeto | local | se stmts | if != nil (% de if) | tentar candidatos (% de if != nil) |
|---------|-----|---------------|----------------- -----|---------------------------------|
| github.com/juju/juju | 1026473 | 26904 | 51,9% | 17,5% |
| github.com/go-kit/kit | 38949 | 467 | 57,0% | 51,9% |
| github.com/boltdb/bolt | 12426 | 228 | 46,1% | 53,3% |
| github.com/hashicorp/consul | 249369 | 5477 | 47,6% | 54,5% |
| github.com/docker/docker | 251152 | 8690 | 48,7% | 56,8% |
| github.com/istio/istio | 429636 | 7564 | 40,4% | 61,9% |
| github.com/gohugoio/hugo | 94875 | 1853 | 42,4% | 64,8% |
| github.com/etcd-io/etcd | 209603 | 4657 | 38,3% | 65,5% |
| github.com/kubernetes/kubernetes | 1789172 | 40289 | 43,3% | 66,5% |
| github.com/cockroachdb/cockroach | 1038529 | 22018 | 39,9% | 74,0% |


Essas duas tabelas, é claro, representam apenas uma amostra de projetos de código aberto e apenas os razoavelmente conhecidos. Já vi pessoas teorizando que bases de código privado mostrariam maior diversidade, e há pelo menos alguma evidência disso com base em alguns dos números que várias pessoas postaram.

@thepudds , isso não se parece com o _tryhard_ mais recente, que fornece "candidatos não testados".

@networkimprov Posso confirmar que pelo menos por gorm estes são os resultados dos últimos tryhard . Os "candidatos não-experimentados" simplesmente não são relatados nas tabelas acima.

@daved Em primeiro lugar, deixe-me garantir que eu/nós ouvimos você alto e claro. Embora ainda estejamos no início do processo e muitas coisas possam mudar. Não vamos pular a arma.

Eu entendo (e aprecio) que alguém pode querer escolher uma abordagem mais conservadora ao ensinar Go. Obrigado.

@griesemer FYI aqui estão os resultados da execução da versão mais recente do tryhard em 233k linhas de código em que estive envolvido, grande parte não de código aberto:

--- stats ---
   8760 (100.0% of    8760) functions (function literals are ignored)
   2942 ( 33.6% of    8760) functions returning an error
  22991 (100.0% of   22991) statements in functions returning an error
   5548 ( 24.1% of   22991) if statements
   2929 ( 52.8% of    5548) if <err> != nil statements
    163 (  5.6% of    2929) try candidates
      0 (  0.0% of    2929) <err> name is different from "err"
--- non-try candidates (-l flag lists file positions) ---
   2213 ( 75.6% of    2929) { return ... zero values ..., expr }
    167 (  5.7% of    2929) single statement then branch
    253 (  8.6% of    2929) complex then branch; cannot use try
     14 (  0.5% of    2929) non-empty else branch; cannot use try

Grande parte do código usa um idioma semelhante a:

 if err != nil {
     return ... zero values ..., errors.Wrap(err)
 }

Pode ser interessante se tryhard puder identificar quando todas essas expressões em uma função usam uma expressão idêntica - ou seja, quando pode ser possível reescrever a função com um único manipulador defer comum.

Aqui estão as estatísticas de uma pequena ferramenta auxiliar do GCP para automatizar a criação de usuários e projetos:

$ tryhard -r .
--- stats ---
    129 (100.0% of     129) functions (function literals are ignored)
     75 ( 58.1% of     129) functions returning an error
    725 (100.0% of     725) statements in functions returning an error
    164 ( 22.6% of     725) if statements
     93 ( 56.7% of     164) if <err> != nil statements
     64 ( 68.8% of      93) try candidates
      0 (  0.0% of      93) <err> name is different from "err"
--- non-try candidates (-l flag lists file positions) ---
     17 ( 18.3% of      93) { return ... zero values ..., expr }
      7 (  7.5% of      93) single statement then branch
      1 (  1.1% of      93) complex then branch; cannot use try
      0 (  0.0% of      93) non-empty else branch; cannot use try

Depois disso, fui em frente e verifiquei todos os lugares no código que ainda estão lidando com uma variável err para ver se eu poderia encontrar algum padrão significativo.

Coletando err s

Em alguns lugares, não queremos interromper a execução no primeiro erro e, em vez disso, poder ver todos os erros que ocorreram uma vez no final da execução. Talvez haja uma maneira diferente de fazer isso que se integre bem com try ou alguma forma de suporte para erros múltiplos seja adicionada ao próprio Go.

var errs []error
for _, p := range toDelete {
    fmt.Println("delete:", p.ProjectID)
    if err := s.DeleteProject(ctx, p.ProjectID); err != nil {
        errs = append(errs, err)
    }
}

Responsabilidade de decoração de erro

Depois de ler este comentário novamente, de repente havia muitos casos potenciais de try que chamaram minha atenção. Eles são todos semelhantes, pois a função de chamada está decorando o erro de uma função chamada com informações que a função chamada já poderia ter adicionado ao erro:

func run() error {
    key := "MY_ENV_VAR"
    client, err := ClientFromEnvironment(key)
    if err != nil {
        // "github.com/pkg/errors"
        return errors.Wrap(err, key)
    }
    // do something with `client`
}

func ClientFromEnvironment(key string) (*http.Client, error) {
    filename, ok := os.LookupEnv(key)
    if !ok {
        return nil, errors.New("environment variable not set")
    }
    return ClientFromFile(filename)
}

Citando a parte importante do blog Go aqui novamente para maior clareza:

É responsabilidade da implementação do erro resumir o contexto. O erro retornado por os.Open formata como "abrir /etc/passwd: permissão negada", não apenas "permissão negada". O erro retornado pelo nosso Sqrt está faltando informações sobre o argumento inválido.

Com isso em mente, o código acima agora se torna:

func run() error {
    key := "MY_ENV_VAR"
    client := try(ClientFromEnvironment(key))
    // do something with `client`
}

func ClientFromEnvironment(key string) (*http.Client, error) {
    filename, ok := os.LookupEnv(key)
    if !ok {
        return nil, fmt.Errorf("environment variable not set: %s", key)
    }
    return ClientFromFile(filename)
}

À primeira vista, isso parece uma pequena mudança, mas na minha opinião, isso pode significar que try está realmente incentivando a manipulação de erros melhor e mais consistente na cadeia de funções e mais próxima da fonte ou pacote.

Notas Finais

No geral, acho que o valor que try está trazendo a longo prazo é maior do que os possíveis problemas que vejo atualmente com ele, que são:

  1. Uma palavra-chave pode "sentir" melhor porque try está alterando o fluxo de controle.
  2. Usar try significa que você não pode mais colocar uma rolha de depuração no caso return err .

Como essas preocupações já são conhecidas da equipe Go, estou curioso para ver como elas se desenrolarão no "mundo real". Obrigado pelo seu tempo em ler e responder a todas as nossas mensagens.

Atualizar

Corrigida uma assinatura de função que não retornava error antes. Obrigado @magical por identificar isso!

func main() {
    key := "MY_ENV_VAR"
    client := try(ClientFromEnvironment(key))
    // do something with `client`
}

@mrkanister Nitpicking, mas você não pode realmente usar try neste exemplo porque main não retorna um error .

Este é um comentário de apreciação;
obrigado @griesemer pela jardinagem e tudo o que você tem feito nesta questão, assim como em outros lugares.

Caso você tenha muitas linhas como estas (de https://github.com/golang/go/issues/32437#issuecomment-509974901):

if !ok {
    return nil, fmt.Errorf("environment variable not set: %s", key)
}

Você pode usar uma função auxiliar que retorna apenas um erro não nulo se alguma condição for verdadeira:

try(condErrorf(!ok, "environment variable not set: %s", key))

Uma vez que os padrões comuns são identificados, acho que será possível lidar com muitos deles com apenas alguns auxiliares, primeiro no nível do pacote, e talvez eventualmente chegando à biblioteca padrão. Tryhard é ótimo, está fazendo um trabalho maravilhoso e dando muitas informações interessantes, mas há muito mais.

Compacto de linha única se

Como uma adição à proposta if de linha única de @zeebo e outros, a instrução if pode ter uma forma compacta que remove o != nil e as chaves:

if err return err
if err return errors.Wrap(err, "foo: failed to boo")
if err return fmt.Errorf("foo: failed to boo: %v", err)

Eu acho que isso é simples, leve e legível. Existem duas partes:

  1. Faça com que as instruções if verifiquem implicitamente os valores de erro para nil (ou talvez interfaces de maneira mais geral). IMHO isso melhora a legibilidade reduzindo a densidade e o comportamento é bastante óbvio.
  2. Adicione suporte para if variable return ... . Como o return está tão próximo do lado esquerdo, parece ainda ser muito fácil percorrer o código - a dificuldade extra de fazê-lo é um dos principais argumentos contra ifs de linha única (?) Go também já tem precedentes para simplificar a sintaxe, por exemplo, removendo parênteses de sua instrução if.

Estilo atual:

a, err := BusinessLogic(state)
if err != nil {
   return nil, err
}

Uma linha se:

a, err := BusinessLogic(state)
if err != nil { return nil, err }

Compacto de uma linha se:

a, err := BusinessLogic(state)
if err return nil, err
a, err := BusinessLogic(state)
if err return nil, errors.Wrap(err, "some context")
func (c *Config) Build() error {
    pkgPath, err := c.load()
    if err return nil, errors.WithMessage(err, "load config dir")

    b := bytes.NewBuffer(nil)
    err = templates.ExecuteTemplate(b, "main", c)
    if err return nil, errors.WithMessage(err, "execute main template")

    buf, err := format.Source(b.Bytes())
    if err return nil, errors.WithMessage(err, "format main template")

    target := fmt.Sprintf("%s.go", filename(pkgPath))
    err = ioutil.WriteFile(target, buf, 0644)
    if err return nil, errors.WithMessagef(err, "write file %s", target)

    // ...
}

@eug48 veja #32611

Aqui estão as estatísticas tryhard para um monorepo (linhas de código go, excluindo o código do fornecedor: 2.282.731):

--- stats ---
 117551 (100.0% of  117551) functions (function literals are ignored)
  35726 ( 30.4% of  117551) functions returning an error
 263725 (100.0% of  263725) statements in functions returning an error
  50690 ( 19.2% of  263725) if statements
  25042 ( 49.4% of   50690) if <err> != nil statements
  12091 ( 48.3% of   25042) try candidates
     36 (  0.1% of   25042) <err> name is different from "err"
--- non-try candidates (-l flag lists file positions) ---
   3561 ( 14.2% of   25042) { return ... zero values ..., expr }
   3304 ( 13.2% of   25042) single statement then branch
   4966 ( 19.8% of   25042) complex then branch; cannot use try
    296 (  1.2% of   25042) non-empty else branch; cannot use try

Dado que as pessoas ainda estão propondo alternativas, eu gostaria de saber com mais detalhes qual funcionalidade é que a comunidade Go mais ampla realmente deseja de qualquer novo recurso de tratamento de erros proposto.

Eu montei uma pesquisa listando vários recursos diferentes, peças de funcionalidade de tratamento de erros que já vi pessoas proporem. Eu cuidadosamente _omiti qualquer nomenclatura ou sintaxe proposta_ e, é claro, tentei tornar a pesquisa neutra em vez de favorecer minhas próprias opiniões.

Se as pessoas quiserem participar, aqui está o link, encurtado para compartilhar:

https://forms.gle/gaCBgxKRE4RMCz7c7

Todos que participam devem poder ver os resultados do resumo. Talvez isso possa ajudar a focar a discussão?

if err := os.Setenv("GO111MODULE", "on"); err != nil {
    return err
}

O manipulador de adiar adicionando contexto não funciona neste caso ou funciona? Caso contrário, seria bom torná-lo mais visível, se possível, pois acontece muito rápido, especialmente porque esse é o padrão até agora.

Ah, e por favor, apresente try , encontrei muitos casos de uso aqui também.

--- stats ---
    929 (100.0% of     929) functions (function literals are ignored)
    230 ( 24.8% of     929) functions returning an error
   1480 (100.0% of    1480) statements in functions returning an error
    320 ( 21.6% of    1480) if statements
    206 ( 64.4% of     320) if <err> != nil statements
    109 ( 52.9% of     206) try candidates
      2 (  1.0% of     206) <err> name is different from "err"
--- non-try candidates (-l flag lists file positions) ---
     53 ( 25.7% of     206) { return ... zero values ..., expr }
     18 (  8.7% of     206) single statement then branch
     17 (  8.3% of     206) complex then branch; cannot use try
      2 (  1.0% of     206) non-empty else branch; cannot use try

@lpar Você pode discutir alternativas , mas não faça isso nesta edição. Trata-se da proposta try . O melhor lugar seria na verdade uma das listas de discussão, por exemplo, go-nuts. O rastreador de problemas é realmente melhor para rastrear e discutir um problema específico, em vez de uma discussão geral. Obrigado.

@fabstu O manipulador defer funcionará bem no seu exemplo, com e sem try . Expandindo seu código com a função de inclusão:

func f() (err error) {
    defer func() {
       if err != nil {
          err = decorate(err, "msg") // here you can modify the result error as you please
       }
    }()
    ...
    if err := os.Setenv("GO111MODULE", "on"); err != nil {
        return err
    }
    ...
}

(note que o resultado err será definido pelo return err ; e o err usado pelo return é o declarado localmente com o if - estas são apenas as regras normais de escopo em ação).

Ou, usando um try , que eliminará a necessidade da variável local err :

func f() (err error) {
    defer func() {
       if err != nil {
          err = decorate(err, "msg") // here you can modify the result error as you please
       }
    }()
    ...
    try(os.Setenv("GO111MODULE", "on"))
    ...
}

E muito provavelmente, você gostaria de usar uma das funções propostas errors/errd :

func f() (err error) {
    defer errd.Wrap(&err, ... )
    ...
    try(os.Setenv("GO111MODULE", "on"))
    ...
}

E se você não precisar de embrulho, será apenas:

func f() error {
    ...
    try(os.Setenv("GO111MODULE", "on"))
    ...
}

@fastu E finalmente, você pode usar errors/errd também sem try e então você obtém:

func f() (err error) {
    defer errd.Wrap(&err, ... )
    ...
    if err := os.Setenv("GO111MODULE", "on"); err != nil {
        return err
    }
    ...
}

Quanto mais penso, mais gosto desta proposta.
As únicas coisas que me incomodam é usar o retorno nomeado em todos os lugares. É finalmente uma boa prática e devo usá-la (nunca tentei)?

De qualquer forma, antes de mudar todo o meu código, vai funcionar assim?

func f() error {
  var err error
  defer errd.Wrap(&err,...)
  try(...)
}

@flibustenet Parâmetros de resultado nomeados por si só não são uma prática ruim; a preocupação usual com resultados nomeados é que eles habilitam naked returns ; ou seja, pode-se simplesmente escrever return sem a necessidade de especificar os resultados reais _com o return _. Em geral (mas nem sempre!) tal prática torna mais difícil ler e raciocinar sobre o código porque não se pode simplesmente olhar para a instrução return e concluir qual é o resultado. É preciso escanear o código para os parâmetros de resultado. Pode-se deixar de definir um valor de resultado e assim por diante. Assim, em algumas bases de código, os retornos simples são simplesmente desencorajados.

Mas, como mencionei antes , se os resultados forem inválidos em caso de erro, não há problema em definir o erro e ignorar o resto. Um retorno nu nesses casos é perfeitamente aceitável, desde que o resultado do erro seja definido de forma consistente. try garantirá exatamente isso.

Finalmente, os parâmetros de resultado nomeados são necessários apenas se você quiser aumentar o retorno de erro com defer . O documento de design também discute brevemente a possibilidade de fornecer outro built-in para acessar o resultado do erro. Isso eliminaria completamente a necessidade de retornos nomeados.

Em relação ao seu exemplo de código: Isso não funcionará como esperado porque try _always_ define o _result error_ (que não tem nome neste caso). Mas você está declarando uma variável local diferente err e a errd.Wrap opera nela. Não será definido por try .

Relatório de experiência rápida: estou escrevendo um manipulador de solicitação HTTP que se parece com isso:

func Handler(w http.ResponseWriter, r *http.Request) {
        ctx := r.Context()
        id := chi.URLParam(r, "id")

        var err error
        // starts as bad request, then it's an internal server error after we parse inputs
        var statusCode = http.StatusBadRequest

        defer func() {
            if err != nil {
                wrap := xerrors.Errorf("handler fail: %w", err)
                logger.With(zap.Error(wrap)).Error("error")
                http.Error(w, wrap.Error(), statusCode)
            }
        }()
        var c Thingie
        err = unmarshalBody(r, &c)
        if err != nil {
            return
        }
        statusCode = http.StatusInternalServerError
        s, err := DoThing(ctx, c)
        if err != nil {
            return
        }
        d, err := DoThingWithResult(ctx, id, s)
        if err != nil {
            return
        }
        data, err := json.Marshal(detail)
        if err != nil {
            return
        }
        w.Header().Set("Content-Type", "application/json")
        w.WriteHeader(http.StatusCreated)
        _, err = w.Write(data)
        if err != nil {
            return
        }
}

À primeira vista, parece que este é um candidato ideal para try pois há muito tratamento de erros onde não há nada a fazer, exceto retornar uma mensagem, o que pode ser feito de forma adiada. Mas você não pode usar try porque um manipulador de solicitação não retorna error . Para usá-lo, eu teria que envolver o corpo em um fechamento com a assinatura func() error . Isso parece... deselegante e eu suspeito que o código que se parece com isso é um padrão um tanto comum.

@jonbodner

Isso funciona (https://play.golang.org/p/NaBZe-QShpu):

package main

import (
    "errors"
    "fmt"

    "golang.org/x/xerrors"
)

func main() {
    var err error
    defer func() {
        filterCheck(recover())
        if err != nil {
            wrap := xerrors.Errorf("app fail (at count %d): %w", ct, err)
            fmt.Println(wrap)
        }
    }()

    check(retNoErr())

    n, err := intNoErr()
    check(err)

    n, err = intErr()
    check(err)

    check(retNoErr())

    check(retErr())

    fmt.Println(n)
}

func check(err error) {
    if err != nil {
        panic(struct{}{})
    }
}

func filterCheck(r interface{}) {
    if r != nil {
        if _, ok := r.(struct{}); !ok {
            panic(r)
        }
    }
}

var ct int

func intNoErr() (int, error) {
    ct++
    return 0, nil
}

func retNoErr() error {
    ct++
    return nil
}

func intErr() (int, error) {
    ct++
    return 0, errors.New("oops")
}

func retErr() error {
    ct++
    return errors.New("oops")
}

Ah, o primeiro downvote! Boa. Deixe o pragmatismo fluir através de você.

Corri tryhard em algumas das minhas bases de código. Infelizmente, alguns dos meus pacotes têm candidatos para tentar 0 apesar de serem muito grandes porque os métodos neles usam uma implementação de erro personalizada. Por exemplo, ao construir servidores, gosto que meus métodos de camada de lógica de negócios emitam apenas SanitizedError s em vez de error s para garantir no tempo de compilação que coisas como caminhos do sistema de arquivos ou informações do sistema não vazar para os usuários em mensagens de erro.

Por exemplo, um método que usa esse padrão pode ser algo assim:

func (a *App) GetFriendsOfUser(userId model.Id) ([]*model.User, SanitizedError) {
    if user, err := a.GetUserById(userId); err != nil {
        // (*App).GetUserById returns (*model.User, SanitizedError)
        // This could be a try() candidate.
        return err
    } else if user == nil {
        return NewUserError("The specified user doesn't exist.")
    }

    friends, err := a.Store.GetFriendsOfUser(userId)
    // (*Store).GetFriendsOfUser returns ([]*model.User, error)
    // This could be a SQL error or a network error or who knows what.
    return friends, NewInternalError(err)
}

Existe alguma razão pela qual não podemos relaxar a proposta atual para funcionar, desde que o último valor de retorno da função delimitadora e da expressão da função try implementem erro e sejam do mesmo tipo? Isso ainda evitaria qualquer confusão de interface nil -> concreta, mas permitiria tentar em situações como a acima.

Obrigado, @jonbodner , pelo seu exemplo . Eu escreveria esse código da seguinte forma (não obstante os erros de tradução):

func Handler(w http.ResponseWriter, r *http.Request) {
    statusCode, err := internalHandler(w, r)
    if err != nil {
        wrap := xerrors.Errorf("handler fail: %w", err)
        logger.With(zap.Error(wrap)).Error("error")
        http.Error(w, wrap.Error(), statusCode)
    }
}

func internalHandler(w http.ResponseWriter, r *http.Request) (statusCode int, err error) {
    ctx := r.Context()
    id := chi.URLParam(r, "id")

    // starts as bad request, then it's an internal server error after we parse inputs
    statusCode = http.StatusBadRequest
    var c Thingie
    try(unmarshalBody(r, &c))

    statusCode = http.StatusInternalServerError
    s := try(DoThing(ctx, c))
    d := try(DoThingWithResult(ctx, id, s))
    data := try(json.Marshal(detail))

    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusCreated)
    try(w.Write(data))

    return
}

Ele usa duas funções, mas é muito mais curto (29 linhas vs 40 linhas) - e eu usei um bom espaçamento - e esse código não precisa de defer . O defer em particular, junto com o statusCode sendo alterado no caminho para baixo e usado no defer torna o código original mais difícil de seguir do que o necessário. O novo código, embora use resultados nomeados e um retorno nu (você pode facilmente substituir isso por return statusCode, nil se quiser) é mais simples porque separa claramente o tratamento de erros da "lógica de negócios".

Apenas repostar meu comentário em outra edição https://github.com/golang/go/issues/32853#issuecomment -510340544

Acho que se pudermos fornecer outro parâmetro funcname , isso será ótimo, caso contrário ainda não sabemos o erro é retornado por qual função.

func foo() error {
    handler := func(err error, funcname string) error {
        return fmt.Errorf("%s: %v", funcname, err) // wrap something
        //return nil // or dismiss
    }

    a, b := try(bar1(), handler) 
    c, d := try(bar2(), handler) 
}

@ccbrown Gostaria de saber se o seu exemplo seria passível do mesmo tratamento acima ; isto é, se fizer sentido fatorar o código de forma que os erros internos sejam encapsulados uma vez (por uma função delimitadora) antes de saírem (em vez de envolvê-los em todos os lugares). Parece-me (sem saber muito sobre o seu sistema) que isso seria preferível, pois centralizaria o agrupamento de erros em um lugar e não em todos os lugares.

Mas em relação à sua pergunta: eu teria que pensar em fazer try aceitar um tipo de erro mais geral (e retornar um também). Não vejo nenhum problema com isso no momento (exceto que é mais complicado de explicar) - mas pode haver um problema, afinal.

Nessa linha, nos perguntamos desde o início se try poderia ser generalizado para que não funcionasse apenas para tipos error , mas para qualquer tipo, e o teste err != nil se tornar x != zero onde x é o equivalente a err (o último resultado), e zero o respectivo valor zero para o tipo de x . Infelizmente, isso não funciona para o caso comum de booleanos (e praticamente qualquer outro tipo básico), porque o valor zero de bool é false e ok != false é exatamente o oposto do que gostaríamos de testar.

@lunny A versão proposta de try não aceita uma função de manipulador.

@griesemer Ah. Que pena! Caso contrário, posso remover github.com/pkg/errors e todos os errors.Wrap .

@ccbrown Gostaria de saber se o seu exemplo seria passível do mesmo tratamento acima; isto é, se fizer sentido fatorar o código de forma que os erros internos sejam encapsulados uma vez (por uma função delimitadora) antes de saírem (em vez de envolvê-los em todos os lugares). Parece-me (sem saber muito sobre o seu sistema) que isso seria preferível, pois centralizaria o agrupamento de erros em um lugar e não em todos os lugares.

@griesemer Retornar error em vez de uma função delimitadora tornaria possível esquecer de categorizar cada erro como um erro interno, erro de usuário, erro de autorização, etc. Como está, o compilador captura isso e usando try não valeria a pena trocar essas verificações em tempo de compilação por verificações em tempo de execução.

Eu gostaria de dizer que gosto do design de try , mas ainda há if no manipulador defer enquanto você usa try . Eu não acho que seria mais simples do que if instruções sem try e defer manipulador. Talvez usar apenas try fosse muito melhor.

@ccbrown Entendi. Em retrospecto, acho que seu relaxamento sugerido não deve ser problema. Acredito que poderíamos relaxar try para trabalhar com qualquer tipo de interface (e tipo de resultado correspondente), não apenas error , desde que o teste relevante permaneça x != nil . Algo para pensar sobre. Isso poderia ser feito antecipadamente, ou retroativamente, pois seria uma mudança compatível com versões anteriores, acredito.

@jonbodner example , e a maneira como @griesemer o reescreveu , é exatamente o tipo de código que eu tenho onde eu realmente gostaria de usar try .

Ninguém se incomoda com esse tipo de uso de try:

data := try(json.Marshal(detalhe))

Independentemente do fato de que o erro de marshaling pode resultar em encontrar a linha correta no código escrito, sinto-me desconfortável sabendo que este é um erro simples retornado sem nenhuma informação de número de linha/chamador incluída. Conhecer o arquivo de origem, o nome da função e o número da linha geralmente é o que incluo ao lidar com erros. Talvez eu esteja entendendo mal alguma coisa embora.

@griesemer Eu não estava planejando discutir alternativas aqui. O fato de todos continuarem sugerindo alternativas é exatamente por isso que acho que uma pesquisa para descobrir o que as pessoas realmente querem seria uma boa ideia. Acabei de postar sobre isso aqui para tentar pegar o maior número possível de pessoas interessadas na possibilidade de melhorar o tratamento de erros do Go.

@trende-jp Eu realmente depende do contexto desta linha de código - por si só, não pode ser julgada de maneira significativa. Se esta for a única chamada para json.Marshal e você quiser aumentar o erro, uma instrução if pode ser melhor. Se houver muitas chamadas json.Marshal , adicionar contexto ao erro pode ser feito com um defer ; ou talvez envolvendo todas essas chamadas dentro de um encerramento local que retorna o erro. Há muitas maneiras de como isso pode ser fatorado, se necessário (ou seja, se houver muitas dessas chamadas na mesma função). "Erros são valores" também é verdade aqui: use código para tornar o tratamento de erros gerenciável.

try não vai resolver todos os seus problemas de manipulação de erros - essa não é a intenção. É simplesmente mais uma ferramenta na caixa de ferramentas. E também não é uma maquinaria realmente nova, é uma forma de açúcar sintático para um padrão que observamos com frequência ao longo de quase uma década. Temos algumas evidências de que funcionaria muito bem em algum código e que também não ajudaria muito em outro código.

@trende-jp

Não pode ser resolvido com defer ?

defer fmt.HandleErrorf(&err, "decoding %q", path)

Números de linha em mensagens de erro também podem ser resolvidos como mostrei em meu blog: How to use 'try' .

@trende-jp @faiface Além do número da linha, você pode armazenar a string do decorador em uma variável. Isso permitiria isolar a chamada de função específica que está falhando.

Eu realmente acho que isso absolutamente não deveria ser uma função interna .

Já foi mencionado algumas vezes que panic() e recover() também alteram o fluxo de controle. Muito bem, não acrescentemos mais.

@networkimprov escreveu https://github.com/golang/go/issues/32437#issuecomment -498960081:

Não se lê como Go.

Eu não poderia concordar mais.

Se alguma coisa, acredito que qualquer mecanismo para resolver o problema raiz (e não tenho certeza se existe um), ele deve ser acionado por uma palavra-chave (ou símbolo de chave ?).

Como você se sentiria se go func() fosse go(func()) ?

Que tal usar bang(!) em vez da função try . Isso poderia tornar possível a cadeia de funções:

func foo() {
    f := os.Open!("")
    defer f.Close()
    // etc
}

func bar() {
    count := mustErr!().Read!()
}

@sylr

Como você se sentiria se go func() fosse go(func()) ?

Vamos lá, isso seria bastante aceitável.

@sylr Obrigado, mas não estamos solicitando propostas alternativas neste tópico. Veja também isso em manter o foco.

Em relação ao seu comentário : Go é uma linguagem pragmática - usar um built-in aqui é uma escolha pragmática. Ele tem várias vantagens em relação ao uso de uma palavra-chave, conforme explicado detalhadamente no documento de design . Observe que try é simplesmente açúcar sintático para um padrão comum (em contraste com go que implementa um recurso importante do Go e não pode ser implementado com outros mecanismos Go), como append , copy , etc. Usar um built-in é uma boa escolha.

(Mas, como eu disse antes, se _that_ for a única coisa que impede try de ser aceitável, podemos considerar torná-la uma palavra-chave.)

Eu estava apenas ponderando sobre um pedaço do meu próprio código, e como isso ficaria com try :

slurp, err := ioutil.ReadFile(path)
if err != nil {
    return err
}
return ioutil.WriteFile(path, append(copyrightText, slurp...), 0666)

Poderia se tornar:

return ioutil.WriteFile(path, append(copyrightText, try(ioutil.ReadFile(path))...), 0666)

Não tenho certeza se isso é melhor. Parece tornar o código muito mais difícil de ler. Mas pode ser apenas uma questão de se acostumar com isso.

@gbbr Você tem uma escolha aqui. Você poderia escrevê-lo como:

slurp := try(ioutil.ReadFile(path))
return ioutil.WriteFile(path, append(copyrightText, slurp...), 0666)

o que ainda economiza muito clichê, mas o torna muito mais claro. Isso não é inerente a try . Só porque você pode espremer tudo em uma única expressão não significa que você deveria. Isso se aplica geralmente.

@griesemer Este exemplo _é_ inerente a tentar, você não pode aninhar código que pode falhar hoje - você é forçado a lidar com erros com fluxo de controle. Gostaria de esclarecer algo em https://github.com/golang/go/issues/32825#issuecomment -507099786 / https://github.com/golang/go/issues/32825#issuecomment -507136111 ao qual você respondeu https://github.com/golang/go/issues/32825#issuecomment -507358397. Mais tarde, o mesmo problema foi discutido novamente em https://github.com/golang/go/issues/32825#issuecomment -508813236 e https://github.com/golang/go/issues/32825#issuecomment -508937177 - o último dos quais declaro:

Que bom que você leu meu argumento central contra try: a implementação não é restritiva o suficiente. Acredito que tanto a implementação deve corresponder a todos os exemplos de uso das propostas que sejam concisos e fáceis de ler.

_O_ a proposta deve conter exemplos que correspondam à implementação para que todas as pessoas que a considerem possam ser expostas ao que inevitavelmente aparecerá no código Go. Junto com todos os casos de canto que podemos enfrentar ao solucionar problemas de software menos do que idealmente escrito, o que ocorre em qualquer idioma/ambiente. Ele deve responder a perguntas como a aparência dos rastreamentos de pilha com vários níveis de aninhamento, os locais dos erros são facilmente reconhecíveis? E quanto aos valores de método, literais de função anônimas? Que tipo de rastreamento de pilha o abaixo produz se a linha que contém as chamadas para fn() falhar?

fn := func(n int) (int, error) { ... }
return try(func() (int, error) { 
    mu.Lock()
    defer mu.Unlock()
    return try(try(fn(111111)) + try(fn(101010)) + try(func() (int, error) {
       // yea...
    })(2))
}(try(fn(1)))

Estou bem ciente de que haverá muito código razoável escrito, mas agora estamos fornecendo uma ferramenta que nunca existiu antes: a capacidade de escrever código potencialmente sem fluxo de controle claro. Então, eu quero justificar porque nós permitimos isso em primeiro lugar, eu nunca quero que meu tempo seja desperdiçado depurando esse tipo de código. Porque eu sei que vou, a experiência me ensinou que alguém vai fazer isso se você permitir. Esse alguém é muitas vezes um eu desinformado.

Go fornece as maneiras menos possíveis para outros desenvolvedores e eu desperdiçarmos o tempo um do outro, limitando-nos a usar as mesmas construções mundanas. Eu não quero perder isso sem um benefício esmagador. Não acredito que "porque o try é implementado como uma função" seja um benefício esmagador. Você pode fornecer uma razão para isso?

Ter um rastreamento de pilha que mostra onde as falhas acima seriam úteis, talvez adicionar um literal composto com campos que chamam essa função na mistura? Estou pedindo isso porque sei como os rastreamentos de pilha se parecem hoje para esse tipo de problema, o Go não fornece informações de coluna facilmente digeríveis nas informações de pilha apenas o endereço de entrada da função hexadecimal. Várias coisas me preocupam com isso, como consistência de rastreamento de pilha entre arquiteturas, por exemplo, considere este código:

package main
import "fmt"
func dopanic(b bool) int { if b { panic("panic") }; return 1 }
type bar struct { a, b, d int; b *bar }
func main() {
    fmt.Println(&bar{
        a: 1,
        c: 1,
        d: 1,
        b: &bar{
            a: 1,
            c: 1,
            d: dopanic(true) + dopanic(false),
        },
    })
}

Observe como o primeiro playground falha no dopânico à esquerda, o segundo à direita, mas ambos imprimem um rastreamento de pilha idêntico:
https://play.golang.org/p/SYs1r4hBS7O
https://play.golang.org/p/YMKkflcQuav

panic: panic

goroutine 1 [running]:
main.dopanic(...)
    /tmp/sandbox709874298/prog.go:7
main.main()
    /tmp/sandbox709874298/prog.go:27 +0x40

Eu esperava que o segundo fosse +0x41 ou algum deslocamento após 0x40, que poderia ser usado para determinar a chamada real que falhou no pânico. Mesmo se obtivermos os deslocamentos hexadecimais corretos, não poderei determinar onde ocorreu a falha sem depuração adicional. Hoje este é um caso extremo, algo que as pessoas raramente enfrentarão. Se você lançar uma versão aninhada de try, ela se tornará a norma, já que até a proposta inclui um try() + try() strconv mostrando que é possível e aceitável usar try dessa maneira.

1) Dadas as informações acima, quais alterações nos rastreamentos de pilha você planeja fazer (se houver) para que eu ainda possa dizer onde meu código falhou?

2) A tentativa de aninhamento é permitida porque você acredita que deveria ser? Em caso afirmativo, quais são os benefícios de tentar aninhar e como você evitará o abuso? Eu acho que o tryhard deve ser ajustado para realizar tentativas aninhadas onde você o considera aceitável para que as pessoas possam tomar uma decisão mais informada sobre como isso afeta seu código, já que atualmente estamos obtendo apenas exemplos de uso melhores / mais rigorosos. Isso nos dará uma idéia de que tipo de limitações vet serão impostas, a partir de agora você disse que o vet será a defesa contra tentativas irracionais, mas como isso se materializará?

3) Tente aninhar porque é uma consequência da implementação? Se sim, isso não parece um argumento muito fraco para a mudança de idioma mais notável desde que o Go foi lançado?

Acho que essa mudança precisa de mais consideração em torno do aninhamento. Cada vez que penso nisso, algum novo ponto de dor surge em algum lugar, fico muito preocupado que todos os potenciais negativos não surjam até que sejam revelados na natureza. O aninhamento também fornece uma maneira fácil de vazar recursos, conforme mencionado em https://github.com/golang/go/issues/32825#issuecomment -506882164, que não é possível hoje. Acho que a história do "vet" precisa de um plano muito mais concreto com exemplos de como fornecerá feedback se for usado como defesa contra os exemplos prejudiciais de try() que dei aqui, ou a implementação deve fornecer erros de tempo de compilação para uso fora de suas melhores práticas ideais.

edit: eu perguntei em gophers sobre a arquitetura play.golang.org e alguém mencionou que compila via NaCl, então provavelmente apenas uma consequência / bug disso. Mas eu pude ver isso sendo um problema em outro arco, acho que muitos dos problemas que podem surgir da introdução de vários retornos por linha simplesmente não foram totalmente explorados, pois a maioria dos usos se concentra no uso de uma única linha sã e limpa.

Oh não, por favor, não adicione essa 'mágica' na linguagem.
Estes não se parecem com o resto da linguagem.
Já vejo código como este aparecendo em todos os lugares.

a, b := try( f() )
if a != 0 && b != "" {
...
}
...

ao invés de

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

ou

a,b,_:= f()

O call if err.... era um pouco artificial no começo para mim, mas agora estou acostumado
Eu me sinto mais fácil lidar com erros, pois eles podem chegar no fluxo de execução em vez de escrever wrappers/handlers que terão que acompanhar algum tipo de estado para agir uma vez disparado.
E se eu decidir ignorar os erros para salvar a vida do meu teclado, sei que um dia entrarei em pânico.

eu até mudei meus hábitos no vbscript para:

on error resume next
a = f()
if er.number <> 0 then
    ...
end if
...

gostei desta proposta

Todas as preocupações que tive (por exemplo, idealmente deve ser uma palavra-chave e não incorporada) são abordadas pelo documento aprofundado

Não é 100% perfeito, mas é uma solução boa o suficiente para que a) resolva um problema real e b) o faça considerando muita compatibilidade com versões anteriores e outros problemas

Claro que faz alguma 'mágica', mas também defer . A única diferença é palavra-chave vs. builtin, e a escolha de evitar uma palavra-chave aqui faz sentido.

Eu sinto que todo o feedback importante contra a proposta de try() já foi expresso. Mas deixe-me tentar resumir:

1) try() move a complexidade do código vertical para horizontal
2) As chamadas try() aninhadas são tão difíceis de ler quanto os operadores ternários
3) Introduz o fluxo de controle de 'retorno' invisível que não é visualmente distinto (em comparação com blocos recuados começando com a palavra-chave return )
4) Torna a prática de quebra de erros pior (contexto da função em vez de uma ação específica)
5) Divide a comunidade #golang e o estilo de código (anti-gofmt)
6) Fará com que os desenvolvedores reescrevem try() para if-err-nil e vice-versa com frequência (tryhard vs. adição de lógica de limpeza/logs adicionais/melhor contexto de erro)

@VojtechVitek Acho que os pontos que você faz são subjetivos e só podem ser avaliados quando as pessoas começarem a usá-lo a sério.

No entanto, acredito que há um ponto técnico que não foi muito discutido. O padrão de usar defer para quebra/decoração de erros tem implicações de desempenho além do simples custo de defer si, pois as funções que usam defer não podem ser embutidas.

Isso significa que a adoção try com empacotamento de erros impõe dois custos potenciais em comparação com o retorno de um erro empacotado diretamente após uma verificação err != nil :

  1. um adiamento para todos os caminhos pela função, mesmo os bem-sucedidos
  2. perda de forro

Embora haja algumas melhorias de desempenho impressionantes para defer o custo ainda é diferente de zero.

try tem muito potencial, então seria bom se a equipe Go pudesse revisitar o design para permitir que algum tipo de embalagem seja feito no ponto de falha, em vez de preventivamente via defer .

vet" precisa de um plano muito mais concreto

história do veterinário é conto de fadas. Ele só funcionará para tipos conhecidos e será inútil nos personalizados.

Olá a todos,

Nosso objetivo com propostas como esta é ter uma discussão em toda a comunidade sobre implicações, compensações e como proceder, e então usar essa discussão para ajudar a decidir o caminho a seguir.

Com base na resposta esmagadora da comunidade e extensa discussão aqui, estamos marcando esta proposta como recusada antes do previsto .

No que diz respeito ao feedback técnico, esta discussão identificou algumas considerações importantes que perdemos, principalmente as implicações para adicionar impressões de depuração e analisar a cobertura de código.

Mais importante, ouvimos claramente as muitas pessoas que argumentaram que esta proposta não visava um problema que valesse a pena. Ainda acreditamos que o tratamento de erros em Go não é perfeito e pode ser melhorado significativamente, mas está claro que nós, como comunidade, precisamos falar mais sobre quais aspectos específicos do tratamento de erros são problemas que devemos abordar.

No que diz respeito à discussão do problema a ser resolvido, tentamos expor nossa visão do problema em agosto passado na “ Visão geral do problema de tratamento de erros do Go 2 ”, mas, em retrospectiva, não chamamos atenção suficiente para essa parte e não incentivamos o suficiente discussão sobre se o problema específico era o certo. A proposta try pode ser uma boa solução para o problema descrito lá, mas para muitos de vocês simplesmente não é um problema a ser resolvido. No futuro, precisamos fazer um trabalho melhor chamando a atenção para essas declarações iniciais de problemas e certificando-nos de que haja um amplo acordo sobre o problema que precisa ser resolvido.

(Também é possível que a declaração do problema de tratamento de erros tenha sido totalmente ofuscada pela publicação de um rascunho de design genérico no mesmo dia.)

No tópico mais amplo sobre o que melhorar no tratamento de erros do Go, ficaríamos muito felizes em ver relatórios de experiência sobre quais aspectos do tratamento de erros no Go são mais problemáticos para você em suas próprias bases de código e ambientes de trabalho e qual seria o impacto de uma boa solução. tem em seu próprio desenvolvimento. Se você escrever tal relatório, poste um link na página Go2ErrorHandlingFeedback .

Obrigado a todos que participaram desta discussão, aqui e em outros lugares. Como Russ Cox apontou antes, discussões em toda a comunidade como esta são de código aberto no seu melhor . Agradecemos a ajuda de todos ao examinar esta proposta específica e, de forma mais geral, ao discutir as melhores maneiras de melhorar o estado do tratamento de erros em Go.

Robert Griesemer, para o Comitê de Revisão de Propostas.

Obrigado, Go Team, pelo trabalho que entrou na proposta do try. E obrigado aos comentaristas que lutaram com isso e propuseram alternativas. Às vezes, essas coisas ganham vida própria. Obrigado Go Team por ouvir e responder adequadamente.

Yay!

Obrigado a todos por esclarecerem isso para que pudéssemos ter o melhor resultado possível!

A chamada é para uma lista de problemas e experiências negativas com o tratamento de erros do Go. No entanto,
Eu e as equipes estamos muito felizes com xerrors.As, xerrors.Is e xerrors.Errorf em produção. Essas novas adições mudam completamente o tratamento de erros de uma maneira maravilhosa para nós, agora que adotamos totalmente as mudanças. No momento, não encontramos nenhum problema ou necessidade que não seja abordada.

@griesemer Só queria agradecer a você (e provavelmente a muitos outros que trabalharam com você) por sua paciência e esforços.

Boa!

@griesemer Obrigado a você e a todos da equipe Go por ouvir incansavelmente todos os comentários e aceitar todas as nossas opiniões variadas.

Então, talvez agora seja um bom momento para encerrar este tópico e passar para coisas futuras?

@griesemer @rsc e @all , legal, obrigado a todos. para mim, é uma ótima discussão/identificação/esclarecimento. o aprimoramento de alguma parte como o problema de 'erro' em go, precisa de uma discussão mais aberta (na proposta e comentários ...) para identificar / esclarecer primeiro as questões centrais.

ps, o x/xerrors está bom por enquanto.

(pode fazer sentido bloquear este tópico também...)

Obrigado à equipe e à comunidade por se envolverem nisso. Eu amo quantas pessoas se preocupam com Go.

Eu realmente espero que a comunidade veja primeiro o esforço e a habilidade que foram colocados na proposta de tentativa em primeiro lugar, e depois o espírito do engajamento que se seguiu e nos ajudou a chegar a essa decisão. O futuro do Go é muito brilhante se conseguirmos continuar assim, especialmente se todos pudermos manter atitudes positivas.

func M() (Dados, erro){
a, err1 := A()
b, err2 := B()
retornar b, nada
} => (if err1 != nil){ return a, err1}.
(if err2 != nil){ return b, err2}

Ok... Gostei dessa proposta, mas adoro a forma como a comunidade e a equipe Go reagiram e se envolveram em uma discussão construtiva, mesmo que às vezes fosse um pouco difícil.

Tenho 2 dúvidas sobre este resultado:
1/ O "tratamento de erros" ainda é uma área de pesquisa?
2/ As melhorias de adiamento são repriorizadas?

Isso prova mais uma vez que a comunidade Go está sendo ouvida e capaz de discutir propostas controversas de mudança de idioma. Assim como as mudanças que chegam ao idioma, as mudanças que não chegam são uma melhoria. Obrigado, equipe Go e comunidade, pelo trabalho duro e discussão civilizada em torno desta proposta!

Excelente!

incrível, bastante útil

Talvez eu esteja muito apegado ao Go, mas acho que um ponto foi mostrado aqui, pois
Russ descreveu: há um ponto em que a comunidade não é apenas um
galinha sem cabeça, é uma força a ser considerada e a ser
aproveitado para seu próprio bem.

Com os devidos agradecimentos à coordenação fornecida pela Go Team, podemos
todos se orgulhem de termos chegado a uma conclusão, uma com a qual podemos conviver e
revisitará, sem dúvida, quando as condições estiverem mais maduras.

Vamos torcer para que a dor sentida aqui nos sirva bem no futuro
(não seria triste, caso contrário?).

Lúcio.

Não concordo com a decisão. No entanto, eu absolutamente endosso a abordagem que a equipe de go empreendeu. Ter uma discussão ampla na comunidade e considerar o feedback dos desenvolvedores é o que o código aberto deveria ser.

Eu me pergunto se as otimizações de adiamento virão também. Eu gosto de anotar erros com ele e xerrors juntos bastante e é muito caro agora.

@pierrec Acho que precisamos de uma compreensão mais clara de quais alterações no tratamento de erros seriam úteis. Algumas das alterações de valores de erro estarão na próxima versão 1.13 (https://tip.golang.org/doc/go1.13#errors), e ganharemos experiência com elas. No decorrer desta discussão, vimos muitas propostas de tratamento de erros sintáticos, e seria útil se as pessoas pudessem votar e comentar sobre qualquer que parecesse particularmente útil. De maneira mais geral, como disse @griesemer , os relatórios de experiência seriam úteis.

Também seria útil entender melhor até que ponto a sintaxe de tratamento de erros é problemática para pessoas novas na linguagem, embora isso seja difícil de determinar.

Há um trabalho ativo para melhorar o desempenho de defer em https://golang.org/cl/183677 e, a menos que algum grande obstáculo seja encontrado, espero que isso chegue à versão 1.14.

@griesemer @ianlancetaylor @rsc Você ainda planeja abordar a verbosidade do tratamento de erros, com outra proposta resolvendo alguns ou todos os problemas levantados aqui?

Então, tarde para a festa, já que isso já foi recusado, mas para futuras discussões sobre o tópico, que tal uma sintaxe de retorno condicional do tipo ternário? (Eu não vi nada parecido com isso na minha varredura do tópico ou olhando para a visão que Russ Cox postou no Twitter.) Exemplo:

f, err := Foo()
return err != nil ? nil, err

Retorna nil, err se err for diferente de nil, continua a execução se err for nil. A forma de declaração seria

return <boolean expression> ? <return values>

e isso seria açúcar sintático para:

if <boolean expression> {
    return <return values>
}

Os principais benefícios são que isso é mais flexível do que uma palavra-chave check ou uma função interna try , porque pode acionar mais do que erros (por exemplo return err != nil || f == nil ? nil, fmt.Errorf("failed to get Foo") , em mais do que apenas o erro não ser nulo (ex. return err != nil && err != io.EOF ? nil, err ), etc, enquanto ainda é bastante intuitivo para entender quando lido (especialmente para aqueles acostumados a ler operadores ternários em outras linguagens).

Ele também garante que o tratamento de erros _ainda ocorra no local da chamada_, em vez de acontecer automaticamente com base em alguma instrução defer. Uma das maiores queixas que tive com a proposta original é que ela tenta, de alguma forma, fazer do _handling_ real de erros um processo implícito que acontece automaticamente quando o erro é não nulo, sem nenhuma indicação clara de que o fluxo de controle retornará se a chamada da função retornar um erro não nulo. Todo o _point_ do Go usando retornos de erros explícitos em vez de um sistema de exceção é para incentivar os desenvolvedores a verificar e manipular explicitamente e intencionalmente seus erros, em vez de apenas deixá-los se propagarem na pilha para serem, em teoria, tratados em algum ponto mais alto acima. Pelo menos uma declaração de retorno explícita, se condicional, anota claramente o que está acontecendo.

@ngrilly Como @griesemer disse, acho que precisamos entender melhor quais aspectos do tratamento de erros os programadores Go acham mais problemáticos.

Falando pessoalmente, não acho que valha a pena fazer uma proposta que elimine um pouco de verbosidade. Afinal, a linguagem funciona bem o suficiente hoje. Cada mudança tem um custo. Se vamos fazer uma mudança, precisamos de um benefício significativo. Acho que essa proposta forneceu um benefício significativo na redução da verbosidade, mas claramente há um segmento significativo de programadores de Go que sentem que os custos adicionais impostos foram muito altos. Não sei se há um meio termo aqui. E não sei se vale a pena abordar o problema.

@kaedys Este problema fechado e extremamente detalhado definitivamente não é o lugar certo para discutir sintaxes alternativas específicas para tratamento de erros.

@ianlancetaylor

Acho que essa proposta forneceu um benefício significativo na redução da verbosidade, mas claramente há um segmento significativo de programadores de Go que sentem que os custos adicionais impostos foram muito altos.

Receio que haja um viés de auto-seleção. Go é conhecido por seu tratamento detalhado de erros e sua falta de genéricos. Isso naturalmente atrai desenvolvedores que não se importam com esses dois problemas. Enquanto isso, outros desenvolvedores continuam usando suas linguagens atuais (Java, C++, C#, Python, Ruby, etc.) e/ou mudam para linguagens mais modernas (Rust, TypeScript, Kotlin, Swift, Elixir, etc.) . Conheço muitos desenvolvedores que evitam o Go principalmente por esse motivo.

Eu também acho que há um viés de confirmação em jogo. Gophers têm sido usados ​​para defender o tratamento de erros verboso e a falta de tratamento de erros quando as pessoas criticam o Go. Isso torna mais difícil avaliar objetivamente uma proposta como try.

Steve Klabnik publicou um comentário interessante no Reddit há alguns dias. Ele era contra a introdução de ? em Rust, porque eram "duas maneiras de escrever a mesma coisa" e era "muito implícito". Mas agora, depois de ter escrito mais do que algumas linhas de código, ? é um de seus recursos favoritos.

@ngrilly concordo com seus comentários. Esses preconceitos são muito difíceis de evitar. O que seria muito útil é uma compreensão mais clara de quantas pessoas evitam o Go devido ao tratamento detalhado de erros. Tenho certeza que o número é diferente de zero, mas é difícil de medir.

Dito isto, também é verdade que try introduziu uma nova mudança no fluxo de controle que era difícil de ver e que, embora try fosse destinado a ajudar no tratamento de erros, não ajudou na anotação de erros .

Obrigado pela citação de Steve Klabnik. Embora eu aprecie e concorde com o sentimento, vale a pena considerar que, como linguagem, o Rust parece um pouco mais disposto a confiar em detalhes sintáticos do que o Go.

Como defensor desta proposta, estou naturalmente desapontado por ela ter sido retirada, embora ache que a equipe Go fez a coisa certa nas circunstâncias.

Uma coisa que agora parece bastante clara é que a maioria dos usuários de Go não considera a verbosidade do tratamento de erros como um problema e acho que isso é algo com o qual o resto de nós terá que conviver, mesmo que isso afaste novos usuários em potencial .

Perdi a conta de quantas propostas alternativas li e, embora algumas sejam muito boas, não vi nenhuma que achasse que valesse a pena adotar se try mordesse o pó. Assim, a chance de alguma proposta de meio-termo emergir agora parece remota para mim.

Em uma nota mais positiva, a discussão atual apontou maneiras pelas quais todos os erros potenciais em uma função podem ser decorados da mesma maneira e no mesmo lugar (usando defer ou mesmo goto ) que eu não havia considerado anteriormente e espero que a equipe Go pelo menos considere alterar go fmt para permitir que uma única instrução if seja escrita em uma linha que pelo menos fará tratamento de erros _look_ mais compacto, mesmo que não remova nenhum clichê.

@pierrec

1/ O "tratamento de erros" ainda é uma área de pesquisa?

Tem sido, por mais de 50 anos! Não parece haver uma teoria geral ou mesmo um guia prático para tratamento de erros consistente e sistemático. Na terra do Go (como em outras línguas) há até confusão sobre o que é um erro. Por exemplo, um EOF pode ser uma condição excepcional quando você tenta ler um arquivo, mas por que é um erro? Se isso é um erro real ou não, realmente depende do contexto. E há outras questões semelhantes.

Talvez seja necessária uma discussão de nível superior (não aqui, no entanto).

Obrigado @griesemer @rsc e todos os outros envolvidos com a proposta. Muitos outros já disseram isso acima, e vale a pena repetir que seus esforços em pensar sobre o problema, escrever a proposta e discuti-la de boa fé são apreciados. Obrigada.

@ianlancetaylor

Obrigado pela citação de Steve Klabnik. Embora eu aprecie e concorde com o sentimento, vale a pena considerar que, como linguagem, o Rust parece um pouco mais disposto a confiar em detalhes sintáticos do que o Go.

Concordo em geral sobre o Rust depender mais do que o Go em detalhes sintáticos, mas não acho que isso se aplique a essa discussão específica sobre a verbosidade do tratamento de erros.

Erros são valores em Rust como em Go. Você pode lidar com eles usando o fluxo de controle padrão, como em Go. Nas primeiras versões do Rust, era a única maneira de lidar com erros, como no Go. Em seguida, eles introduziram a macro try! , que é surpreendentemente semelhante à proposta de função interna try . Eles eventualmente adicionaram o operador ? , que é uma variação sintática e uma generalização da macro try! , mas isso não é necessário para demonstrar a utilidade de try , e o fato que a comunidade Rust não se arrepende de tê-lo adicionado.

Estou bem ciente das enormes diferenças entre Go e Rust, mas no tópico de detalhamento de tratamento de erros, acho que a experiência deles é transponível para Go. Vale a pena ler as RFCs e discussões relacionadas a try! e ? . Fiquei realmente surpreso com o quão semelhantes são as questões e argumentos a favor e contra as mudanças de linguagem.

@griesemer , você anunciou a decisão de recusar a proposta try , em sua forma atual, mas não disse o que a equipe Go planeja fazer a seguir.

Você ainda planeja abordar a verbosidade do tratamento de erros, com outra proposta que resolveria os problemas levantados nesta discussão (depuração de impressões, cobertura de código, melhor decoração de erros etc.)?

Concordo em geral sobre o Rust depender mais do que o Go em detalhes sintáticos, mas não acho que isso se aplique a essa discussão específica sobre a verbosidade do tratamento de erros.

Como outros ainda estão somando seus dois centavos, acho que ainda há espaço para eu fazer o mesmo.

Embora eu esteja programando desde 1987, só trabalho com Go há cerca de um ano. Cerca de 18 meses atrás, quando eu estava procurando por um novo idioma para atender a certas necessidades, olhei para Go e Rust. Eu decidi em Go porque senti que o código Go era muito mais fácil de aprender e usar, e que o código Go era muito mais legível porque Go parece preferir palavras para transmitir significado em vez de símbolos concisos.

Então, eu ficaria muito infeliz em ver Go se tornar mais parecido com Rust , incluindo o uso de pontos de exclamação ( ! ) e pontos de interrogação ( ? ) para implicar significado.

Na mesma linha, acho que a introdução de macros mudaria a natureza do Go e resultaria em milhares de dialetos do Go, como é efetivamente o caso do Ruby. Portanto, espero que as macros também nunca sejam adicionadas ao Go, embora meu palpite seja que há pouca chance de isso acontecer, felizmente IMO.

jmtcw

@ianlancetaylor

O que seria muito útil é uma compreensão mais clara de quantas pessoas evitam o Go devido ao tratamento detalhado de erros. Tenho certeza que o número é diferente de zero, mas é difícil de medir.

Não é uma "medida" em si, mas esta discussão do Hacker News fornece dezenas de comentários de desenvolvedores insatisfeitos com o tratamento de erros do Go devido à sua verbosidade (e alguns comentários explicam seu raciocínio e fornecem exemplos de código): https://news.ycombinator. com/item?id=20454966.

Em primeiro lugar, obrigado a todos pelo feedback de apoio sobre a decisão final, mesmo que essa decisão não tenha sido satisfatória para muitos. Este foi realmente um esforço de equipe, e estou muito feliz que todos nós conseguimos passar pelas intensas discussões de uma forma geral civil e respeitosa.

@ngrilly Falando só por mim, ainda acho que seria bom abordar a verbosidade do tratamento de erros em algum momento. Dito isto, dedicamos bastante tempo e energia a isso no último semestre e especialmente nos últimos 3 meses, e ficamos muito felizes com a proposta, mas obviamente subestimamos a possível reação em relação a ela. Agora faz muito sentido dar um passo para trás, digerir e destilar o feedback e depois decidir os melhores próximos passos.

Além disso, realisticamente, já que não temos recursos ilimitados, vejo pensar em suporte de linguagem para tratamento de erros ficar um pouco em segundo plano em favor de mais progresso em outras frentes, principalmente trabalhar em genéricos, pelo menos para o próximos meses. if err != nil pode ser irritante, mas não é motivo para ação urgente.

Se você quiser continuar a discussão, gostaria de sugerir gentilmente a todos que saiam daqui e continuem a discussão em outro lugar, em uma questão separada (se houver uma proposta clara) ou em outros fóruns mais adequados para uma discussão aberta. Esta questão está encerrada, afinal. Obrigado.

Receio que haja um viés de auto-seleção.

Eu gostaria de cunhar um novo termo aqui e agora: "viés do criador". Se alguém está disposto a colocar o trabalho, deve ser dado o benefício da dúvida.

É muito fácil para a galeria de amendoim gritar alto e alto em fóruns não relacionados como eles não gostam de uma solução proposta para um problema. Também é muito fácil para todos escreverem uma tentativa incompleta de 3 parágrafos para uma solução diferente (sem nenhum trabalho real apresentado na linha lateral). Se alguém concorda com o status quo, ok. Ponto justo. Apresentar qualquer outra coisa como solução sem uma proposta completa lhe dá -10k pontos.

Não apoio ou sou contra tentar, mas confio no julgamento do Go Teams sobre o assunto, até agora o julgamento deles forneceu uma excelente linguagem, então acho que o que eles decidirem funcionará para mim, tente ou não tente, considero precisamos entender como outsiders, que os mantenedores têm maior visibilidade sobre o assunto. sintaxe que podemos discutir o dia todo. Gostaria de agradecer a todos que trabalharam ou estão tentando melhorar o go no momento por seus esforços, estamos agradecidos e esperamos novas melhorias (sem retrocesso) nas bibliotecas de idiomas e no tempo de execução, se houver útil por vocês.

Também é muito fácil para todos escreverem uma tentativa incompleta de 3 parágrafos para uma solução diferente (sem nenhum trabalho real apresentado na linha lateral).

A única coisa que eu (e vários outros) queria tornar try útil era um argumento opcional para permitir que ele retornasse uma versão encapsulada do erro em vez do erro inalterado. Eu não acho que isso precisava de uma grande quantidade de trabalho de design.

Ah não.

Entendo. Vá querer fazer algo diferente de outras linguagens.

Talvez alguém deve bloquear este problema? A discussão é provavelmente mais adequada em outro lugar.

Esse problema já é tão longo que bloqueá-lo parece inútil.

Todos, por favor, estejam cientes de que este assunto está encerrado, e os comentários que você fizer aqui quase certamente serão ignorados para sempre. Se estiver tudo bem com você, comente.

Caso alguém odeie a palavra try que os deixa pensar em Java, linguagem C*, aconselho não usar 'try', mas outras palavras como 'help' ou 'must' ou 'checkError'.. (ignore-me)

Caso alguém odeie a palavra try que os deixa pensar em Java, linguagem C*, aconselho não usar 'try', mas outras palavras como 'help' ou 'must' ou 'checkError'.. (ignore-me)

Sempre haverá palavras-chave e conceitos sobrepostos que têm diferenças semânticas pequenas ou grandes em linguagens razoavelmente próximas umas das outras (como linguagens da família C). Um recurso de idioma não deve causar confusão dentro do próprio idioma, as diferenças entre os idiomas sempre ocorrerão.

mau. isso é anti padrão, desrespeito autor dessa proposta

@alersenkevich Por favor, seja educado. Consulte https://golang.org/conduct.

Acho que estou feliz com a decisão de não ir mais longe com isso. Para mim, isso parecia um truque rápido para resolver um pequeno problema sobre se err != nil estar em várias linhas. Não queremos inchar o Go com palavras-chave menores para resolver coisas menores como essa, não é? É por isso que a proposta com macros higiênicas https://github.com/golang/go/issues/32620 parece melhor. Ele tenta ser uma solução mais genérica para abrir mais flexibilidade com mais coisas. A discussão sobre sintaxe e uso está em andamento, então não pense apenas em macros C/C++. O ponto aqui é discutir uma maneira melhor de fazer macros. Com ele, você pode implementar sua própria tentativa.

Eu adoraria receber comentários sobre uma proposta semelhante que resolva um problema com o tratamento de erros atual https://github.com/golang/go/issues/33161.

Honestamente, isso deveria ser reaberto, de todas as propostas de tratamento de erros, essa é a mais sã.

@OneOfOne respeitosamente, discordo que isso deva ser reaberto. Este segmento estabeleceu que existem limitações reais com a sintaxe. Talvez você esteja certo de que esta é a proposta mais "sã": mas acredito que o status quo é ainda mais são.

Eu concordo que if err != nil é escrito com muita frequência em Go - mas ter uma maneira singular de retornar de uma função melhora enormemente a legibilidade. Embora eu geralmente possa apoiar propostas que reduzam o código clichê, o custo nunca deve ser a legibilidade IMHO.

Eu sei que muitos desenvolvedores lamentam o erro "à mão" ao fazer o check-in go, mas honestamente a concisão geralmente está em desacordo com a legibilidade. Go tem muitos padrões estabelecidos aqui e em outros lugares que encorajam uma maneira particular de fazer as coisas e, na minha experiência, o resultado é um código confiável que envelhece bem. Isso é fundamental: o código do mundo real precisa ser lido e entendido muitas vezes ao longo de sua vida, mas só é escrito uma vez. A sobrecarga cognitiva é um custo real, mesmo para desenvolvedores experientes.

Ao invés de:

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

Eu esperaria:

f := try os.Open(filename)

Todos, por favor, estejam cientes de que este assunto está encerrado, e os comentários que você fizer aqui quase certamente serão ignorados para sempre. Se estiver tudo bem com você, comente.
—@ianlancetaylor

Seria bom se pudéssemos usar try para um bloco de códigos junto com a maneira atual de lidar com erros.
Algo assim:

// Generic Error Handler
handler := func(err error) error {
    return fmt.Errorf("We encounter an error: %v", err)  
}
a := "not Integer"
b := "not Integer"

try(handler){
    f := os.Open(filename)
    x := strconv.Atoi(a)
    y, err := strconv.Atoi(b) // <------ If you want a specific error handler
    if err != nil {
        panic("We cannot covert b to int")   
    }
}

O código acima parece mais limpo que o comentário inicial. Eu gostaria de poder propor isso.

Fiz uma nova proposta #35179

val := tente f() (err){
pânico (err)
}

Espero que sim:

i, err := strconv.Atoi("1")
if err {
    println("ERROR")
} else {
    println(i)
}

ou

i, err := strconv.Atoi("1")
if err {
    io.EOF:
        println("EOF")
    io.ErrShortWrite:
        println("ErrShortWrite")
} else {
    println(i)
}

@myroid Eu não me importaria de ter seu segundo exemplo um pouco mais genérico em uma forma de declaração switch-else :

```vai
i, err := strconv.Atoi("1")
alternar erro != nil; errar {
caso io.EOF:
println("EOF")
case io.ErrShortWrite:
println("ErrShortWrite")
} outro {
println(i)
}

@piotrkowalczuk Seu código parece muito melhor que o meu. Acho que o código pode ser mais conciso.

i, err := strconv.Atoi("1")
switch err {
case io.EOF:
    println("EOF")
case io.ErrShortWrite:
    println("ErrShortWrite")
} else {
    println(i)
}

Isso não considera a opção haverá um olho de tipo diferente

Precisa haver
Erro de caso!=nil

Para erros, o desenvolvedor não capturou explicitamente

Em sex, 8 de novembro de 2019, 12:06 Yang Fan, [email protected] escreveu:

@piotrkowalczuk https://github.com/piotrkowalczuk Seu código parece muito
Melhor que o meu. Acho que o código pode ser mais conciso.

i, err := strconv.Atoi("1")switch err {case io.EOF:
println("EOF")case io.ErrShortWrite:
println("ErrShortWrite")
} outro {
println(i)
}


Você está recebendo isso porque foi mencionado.
Responda a este e-mail diretamente, visualize-o no GitHub
https://github.com/golang/go/issues/32437?email_source=notifications&email_token=ABNEY4VH4KS2OX4M5BVH673QSU24DA5CNFSM4HTGCZ72YY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOEDPTTYY#comment-552ZLOORPWSZGOEDPTTYY#comment-552ZLOORPWSZGOEDPTTYY
ou cancelar
https://github.com/notifications/unsubscribe-auth/ABNEY4W4XIIHGUGIW2KXRPTQSU24DANCNFSM4HTGCZ7Q
.

switch não precisa de else , tem default .

Abri https://github.com/golang/go/issues/39890 que propõe algo semelhante ao guard de Swift deve abordar algumas das preocupações com esta proposta:

  • controle de fluxo
  • localidade de tratamento de erros
  • legibilidade

Não ganhou muita força, mas pode ser de interesse para quem comentou aqui.

Esta página foi útil?
0 / 5 - 0 avaliações