Go: proposta: especificação: adicionar tipo de resultado integrado (como Rust, OCaml)

Criado em 15 abr. 2017  ·  79Comentários  ·  Fonte: golang/go

Esta é uma proposta para adicionar um tipo de resultado para ir. Os tipos de resultado normalmente contêm um valor retornado ou um erro e podem fornecer encapsulamento de primeira classe do padrão comum (value, err) onipresente em todos os programas Go.

Minhas desculpas se algo como isso foi enviado antes, mas espero que este seja um texto bastante abrangente da ideia.

Fundo

Alguns antecedentes sobre essa ideia podem ser encontrados na postagem Tratamento de erros no Go , embora onde aquela postagem sugira a implementação de genéricos de alavancagem, eu proponho que não é necessário, e que de fato os tipos de resultado poderiam (com algum cuidado) ser adaptado para Go sem adicionar genéricos e sem fazer alterações significativas na linguagem em si.

Dito isso, estou autoaplicando o rótulo "Go 2" não porque essa seja uma alteração importante, mas porque espero que seja controverso e, em certo grau, vai contra a natureza da linguagem.

O tipo Rust Result fornece alguns precedentes. Uma ideia semelhante pode ser encontrada em muitas linguagens funcionais, incluindo Haskell's Either , OCaml's result e Scala's Either . Rust gerencia erros de maneira bastante semelhante a Go: erros são apenas valores, borbulhá-los é tratado em cada site de chamada, em oposição à ação fantasmagórica à distância de exceções usando saltos não locais, e algum trabalho pode ser necessário para converter tipos de erros ou agrupar erros em cadeias de erros .

Onde Rust usa tipos de soma (consulte a proposta de tipos de soma Go 2 ) e genéricos para implementar tipos de resultado, como um recurso de linguagem de núcleo de caso especial, acho que um tipo de resultado Go não precisa de qualquer um e pode simplesmente aproveitar a mágica do compilador de caso especial. Isso envolveria sintaxe especial e nós AST especiais, muito parecidos com os tipos de coleção de Go usados ​​atualmente.

Metas

Acredito que a adição de um tipo de resultado para ir poderia ter os seguintes resultados positivos:

  1. Reduzir o boilerplate do tratamento de erros: esta é uma reclamação extremamente comum sobre o Go. O "padrão" if err != nil { return nil, err } (ou pequenas variações) pode ser visto em todos os programas Go. Este clichê não agrega valor e serve apenas para tornar os programas muito mais longos.
  2. Permite que o compilador raciocine sobre os resultados: em Rust, os resultados não consumidos emitem um aviso. Embora existam ferramentas de linting para o Go realizarem a mesma coisa, acho que seria muito mais valioso se esse fosse um recurso de primeira classe do compilador. Também é razoavelmente simples de implementar e não deve afetar negativamente o desempenho do compilador.
  3. Erro ao manipular combinadores (essa é a parte que acho que vai contra a natureza da linguagem): se houvesse um tipo de resultados, ele poderia oferecer suporte a vários métodos de manipulação, transformação e consumo de resultados. Admito que essa abordagem vem com um pouco de curva de aprendizado e, como tal, pode impactar negativamente a clareza dos programas para pessoas que não estão familiarizadas com expressões idiomáticas de combinador. Embora pessoalmente eu adore combinadores para tratamento de erros, posso definitivamente ver como, culturalmente, eles podem ser uma escolha inadequada para Go.

Exemplos de sintaxe

Em primeiro lugar, uma nota rápida: por favor, não deixe a ideia ficar muito atolada na sintaxe. A sintaxe é uma coisa muito fácil de superar, e não acho que nenhum desses exemplos sirva como a Sintaxe Verdadeira Única, por isso estou apresentando várias alternativas.

Em vez disso, prefiro que as pessoas prestem atenção à "forma" geral do problema e apenas olhem para esses exemplos para entender melhor a ideia.

Assinatura de tipo de resultado

A coisa mais simples que funciona: basta adicionar "resultado" na frente da tupla do valor de retorno:

func f1(arg int) result(int, error) {

Mais típica é uma sintaxe "genérica", mas provavelmente deve ser reservada para se / quando Go realmente adicionar genéricos (um recurso de tipo de resultado pode ser adaptado para aproveitá-los se isso acontecer):

func f1(arg int) result<int, error> {

Ao retornar resultados, precisaremos de uma sintaxe para agrupar valores ou erros em um tipo de resultado. Isso poderia ser apenas uma invocação de método:

return result.Ok(value)

`` `vá
resultado de retorno.Err (erro)

If we allow "result" to be shadowed here, it should avoid breaking any code that already uses "result".

Perhaps "Go 2" could add syntax sugar similar to Rust (although it would be a breaking change, I think?):

```go
return Ok(value)

`` `vá
return Err (valor)

### Propagating errors

Rust recently added a `?` operator for propagating errors (see [Rust RFC 243](https://github.com/rust-lang/rfcs/blob/master/text/0243-trait-based-exception-handling.md)). A similar syntax could enable replacing `if err != nil { return _, err }` boilerplate with a shorthand syntax that bubbles the error up the stack.

Here are some prospective examples. I have only done some cursory checking for syntactic ambiguity. Apologies if these are either ambiguous or breaking changes: I assume with a little work you can find a syntax for this which isn't at breaking change.

First, an example with present-day Go syntax:

```go
count, err = fd.Write(bytes)
if err != nil {
    return nil, err
}

Agora com uma nova sintaxe que consome um resultado e borbulha o erro na pilha para você. Lembre-se de que esses exemplos são apenas para fins ilustrativos:

count := fd.Write!(bytes)

`` `vá
contagem: = fd.Write (bytes)!

```go
count := fd.Write?(bytes)

`` `vá
contagem: = fd.Write (bytes)?

```go
count := try(fd.Write(bytes))

NOTA: Anteriormente, o Rust suportava o último, mas geralmente se afastou dele porque não pode ser encadeado.

Em todos os meus exemplos subsequentes, usarei essa sintaxe, mas observe que é apenas um exemplo, pode ser ambíguo ou ter outros problemas e certamente não sou casado com ela:

count := fd.Write(bytes)!

Compatibilidade com versões anteriores

Todas as propostas de sintaxe usam uma palavra-chave result para identificar o tipo. Acredito (mas não tenho certeza) que regras de sombreamento poderiam ser desenvolvidas que permitiriam o código existente usando "resultado" para, por exemplo, um nome de variável continuar a funcionar como está, sem problemas.

Idealmente, deve ser possível "atualizar" o código existente para usar tipos de resultado de uma maneira completamente contínua. Para fazer isso, podemos permitir que os resultados sejam consumidos como duas tuplas, ou seja, dados:

func f1(arg int) result(int, error) {

Deve ser possível consumi-lo como:

result := f1(42)

ou:

(value, err) := f1(42)

Ou seja, se o compilador vê uma atribuição de result(T, E) a (T, E) , ele deve coagir automaticamente. Isso deve permitir que as funções alternem perfeitamente para o uso de tipos de resultado.

Combinadores

Normalmente, o tratamento de erros será muito mais complicado do que if err != nil { return _, err } . Esta proposta seria lamentavelmente incompleta se esse fosse o único caso em que ajudasse.

Os tipos de resultado são conhecidos por serem uma espécie de faca suíça de tratamento de erros em linguagens funcionais devido aos "combinadores" que eles suportam. Na verdade, esses combinadores são apenas um conjunto de métodos que nos permitem transformar e se comportar seletivamente com base em um tipo de resultado, normalmente em "combinação" com um fechamento.

Then() : encadear chamadas de função que retornam o mesmo tipo de resultado

Digamos que temos algum código parecido com este:

resp, err := doThing(a)
if err != nil {
    return nil, err
}
resp, err = doAnotherThing(b, resp.foo())
if err != nil {
    return nil, err
}
resp, err = FinishUp(c, resp.bar())
if err != nil {
    return nil, err
}

Com um tipo de resultado, podemos criar uma função que recebe um fechamento como parâmetro e só chama o fechamento se o resultado for bem-sucedido, caso contrário, entrar em curto-circuito e retornar a si mesmo representa um erro. Chamaremos essa função de Then (é descrita dessa forma na postagem do blog Tratamento de erros em Go ) e conhecida como and_then em Rust). Com uma função como esta, podemos reescrever o exemplo acima como algo como:

result := doThing(a).
    Then(func(resp) { doAnotherThing(b, resp.foo()) }).
    Then(func(resp) { FinishUp(c, resp.bar()) })

if result.isError() {
    return result.Error()
}

ou usando uma das sintaxes propostas acima (irei escolher ! como o operador mágico):

final_value := doThing(a).
    Then(func(resp) { doAnotherThing(b, resp.foo()) }).
    Then(func(resp) { FinishUp(c, resp.bar()) })!

Isso reduz as 12 linhas de código em nosso exemplo original para três e nos deixa com o valor final que realmente buscamos e o próprio tipo de resultado desaparecido da imagem. Nunca tivemos que dar um nome ao tipo de resultado neste caso.

Agora concedido, a sintaxe de encerramento nesse caso parece um pouco complicada / semelhante a JavaScript. Provavelmente, ele poderia se beneficiar de uma sintaxe de encerramento mais leve. Eu pessoalmente adoraria algo assim:

final_value := doThing(a).
    Then(|resp| doAnotherThing(b, resp.foo())).
    Then(|resp| FinishUp(c, resp.bar()))!

... mas algo assim provavelmente merece uma proposta separada.

Map() e MapErr() : converta entre os valores de sucesso e erro

Freqüentemente, ao fazer a dança if err != nil { return nil, err } você desejará realmente lidar com o erro ou transformá-lo em um tipo diferente. Algo assim:

resp, err := doThing(a)
if err != nil {
    return nil, myerror.Wrap(err)
}

Nesse caso, podemos realizar a mesma coisa usando MapErr() (usarei novamente a sintaxe ! para retornar o erro):

resp := doThing(a).
    MapErr(func(err) { myerror.Wrap(err) })!

Map faz a mesma coisa, apenas transformando o valor de sucesso ao invés do erro.

E mais!

Existem muitos mais combinadores do que os que mostrei aqui, mas acredito que esses são os mais interessantes. Para uma ideia melhor de como é um tipo de resultado com todos os recursos, sugiro verificar o Rust:

https://doc.rust-lang.org/std/result/enum.Result.html

Go2 LanguageChange NeedsInvestigation Proposal

Comentários muito úteis

final_value := doThing(a).
    Then(func(resp) { doAnotherThing(b, resp.foo()) }).
    Then(func(resp) { FinishUp(c, resp.bar()) })!

Acho que essa não seria a direção certa para Go. ()) })! , sério? O principal objetivo do Go deve ser facilidade de aprendizado, legibilidade e facilidade de uso. Isso não ajuda.

Todos 79 comentários

As propostas de mudança de idioma não estão sendo consideradas durante o processo de revisão da proposta, pois o idioma do Go 1.x está congelado (e este é o Go2, como você observou). Informamos você que não deve esperar uma decisão sobre isso tão cedo.

final_value := doThing(a).
    Then(func(resp) { doAnotherThing(b, resp.foo()) }).
    Then(func(resp) { FinishUp(c, resp.bar()) })!

Acho que essa não seria a direção certa para Go. ()) })! , sério? O principal objetivo do Go deve ser facilidade de aprendizado, legibilidade e facilidade de uso. Isso não ajuda.

Como alguém disse no tópico do reddit : definitivamente preferiria tipos de soma adequados e genéricos em vez de novos builtins especiais.

Talvez eu não tenha sido claro no post: eu certamente preferiria que um tipo de resultado fosse composto de tipos de soma e genéricos.

Eu estava tentando especificar isso de tal forma que a adição de ambos (que eu pessoalmente considero extremamente improvável) não seria um bloqueador para adicionar este recurso, e poderia ser adicionado de tal forma que, quando disponível, esse recurso pode ser alternado para eles (eu até dei um exemplo de como seria uma sintaxe genérica tradicional, e também vinculado ao problema do tipo Go sum).

Não entendo a conexão entre o tipo de resultado e os objetivos. Suas idéias sobre propagação de erros e combinadores parecem funcionar tão bem com o suporte atual para vários parâmetros de resultado.

@ianlancetaylor você pode dar um exemplo de como definir um combinador que funciona genericamente nas tuplas de resultados atuais? Se fosse possível, ficaria curioso para ver, mas não acho que seja ( por esta postagem )

@tarcieri Essa postagem é significativamente diferente, pois error não aparece em seu uso sugerido de Result<A> . Este problema, ao contrário da postagem, parece sugerir result<int, error> , o que para mim implica que os combinadores propostos estão reconhecendo error . Minhas desculpas se eu entendi mal.

A intenção não é acoplar result a error , mas sim result carregar dois valores, semelhantes ao tipo Result em Rust ou Either em Haskell. Em ambos os idiomas, por convenção, o segundo valor é geralmente um tipo error (embora não precise ser).

Este problema, ao contrário da postagem, parece sugerir resultado

A postagem sugere:

type Result<A> struct {
    // fields
}

func (r Result<A>) Value() A {…}
func (r Result<A>) Error() error {…}

... então, ao contrário, essa postagem é especializada em torno de error , enquanto esta proposta aceita um tipo especificado pelo usuário para o segundo valor.

Reconhecidamente, coisas como result.Err() e result.MapErr() dão um aceno para este valor sendo sempre um error .

@tarcieri O que há de errado com uma estrutura? https://play.golang.org/p/mTqtaMbgIF

@griesemer, conforme abordado no artigo Tratamento de erros em Go , essa estrutura não é genérica. Você teria que definir um para cada combinação de tipos de sucesso e erro que você sempre quis usar.

@tarcieri compreendido. Mas se esse (não genericidade, ou talvez não ter um tipo de soma) for o problema aqui, então devemos abordar essas questões. Lidar apenas com tipos de resultados é apenas adicionar mais casos especiais.

Se Go tem ou não genéricos é ortogonal se um tipo de resultado de primeira classe é útil. Isso tornaria a implementação mais próxima de algo que você mesmo implementa, mas, conforme abordado na proposta, permitir que o compilador raciocine sobre isso de uma maneira de primeira classe permite, por exemplo, alertar sobre resultados não consumidos. Ter um único tipo de resultado também é o que torna os combinadores da proposta composíveis.

@tarcieri A composição conforme você sugeriu também seria possível com um único tipo de estrutura de resultado.

Não entendo por que você não usaria um tipo de estrutura incorporado ou definido. Por que existem métodos especializados e sintaxe para verificação de erros? Go já tem meios de fazer tudo isso. Parece que isso é apenas adicionar recursos que não definem a linguagem Go, eles definem Rust. Seria um erro implementar tais mudanças.

Não entendo por que você não usaria um tipo de estrutura incorporado ou definido. Por que existem métodos especializados e sintaxe para verificação de erros?

Para me repetir: porque ter um tipo de resultado genérico requer ... genéricos. Go não tem genéricos. Na falta de Go obtendo genéricos, ele precisa de suporte para casos especiais da linguagem.

Talvez você esteja sugerindo algo assim?

type Result struct {
    value interface{}
    err error
}

Sim, isso "funciona" ... à custa da segurança de tipo. Agora, para consumir qualquer resultado, temos que fazer uma declaração de tipo para garantir que o valor digitado em interface{} seja o que esperamos. Se não, agora se tornou um erro de tempo de execução (ao contrário de um erro de tempo de compilação como é atualmente).

Isso seria uma grande regressão em relação ao que Go tem agora.

Para que esse recurso seja realmente útil, ele precisa ter segurança de tipos. O sistema de tipos de Go não é expressivo o suficiente para implementá-lo de maneira segura para tipos sem suporte de linguagem de casos especiais. Seria necessário, no mínimo, genéricos e, de preferência, tipos de soma também.

Parece que isso é apenas adicionar recursos que não definem a linguagem Go [...]. Seria um erro implementar tais mudanças.

Abordei tanto na proposta original:

"Admito que essa abordagem vem com um pouco de curva de aprendizado e, como tal, pode impactar negativamente a clareza dos programas para pessoas que não estão familiarizadas com expressões idiomáticas de combinadores. Embora pessoalmente eu adore combinadores para tratamento de erros, posso definitivamente ver quão culturalmente eles podem não ser adequados para Go. "

Sinto que confirmei minhas suspeitas e que um recurso como esse não é facilmente compreendido pelos desenvolvedores de Go e vai contra a natureza orientada para a simplicidade da linguagem. É o aproveitamento de paradigmas de programação que, claramente, os desenvolvedores de Go parecem não entender ou querer e, nesse caso, parece um recurso inadequado.

eles definem a ferrugem

Os tipos de resultado não são um recurso específico do Rust. Eles são encontrados em muitas linguagens funcionais (por exemplo, de Haskell Either de e OCaml result ). Dito isso, apresentá-los ao Go parece uma ponte longe demais.

Obrigado por compartilhar suas idéias, mas acho que os exemplos usados ​​acima não são convincentes. Para mim, A é melhor do que B:

UMA
`` `resp, err: = doThing (a)
se errar! = nulo {
retornar nulo errar
}
se resp, err = doAnotherThing (b, resp.foo ()); err! = nulo {
return err
}
se resp, err = Concluir (c, resp.bar ()); err! = nulo {
return err
}


resultado: = fazer (a).
Então (func (resp) {doAnotherThing (b, resp.foo ())}).
Então (func (resp) {FinishUp (c, resp.bar ())})

if result.isError () {
resultado de retorno. Erro ()
}
`` `

  • A é mais legível, em voz alta e mentalmente.
  • A não requer formatação / quebra de linha
  • Em A, as condições de erro encerram a execução, explicitamente; exigindo nenhuma negação mental. B não é semelhante.
  • Em B, a palavra-chave "Então" não indica causalidade condicional. A palavra-chave "se" tem, e já está no idioma.
  • Em B, não quero desacelerar meu ramo de execução mais provável, compactando-o em um lambda

Não acho que A seja mais legível. Na verdade, as ações nem são perceptíveis. Em vez disso, a primeira vista revela que vários erros estão sendo obtidos e retornados.

Se B fosse formatado de forma que os corpos de fechamento estivessem em novas linhas, esse seria o formato mais legível.

Além disso, o último ponto parece um pouco bobo. Se o desempenho da chamada de função é tão importante, então, por suposto, use uma sintaxe mais tradicional.

A De @ como eu acho que o fluxo normal não deve recuar.

if err != nil {
    return err
}

resp, err = doAnotherThing(b, resp.foo());
if  err != nil {
    return err
}

resp, err = FinishUp(c, resp.bar());
if  err != nil {
    return err
}

Uma observação interessante deste tópico: o exemplo original que dei e que as pessoas continuam copiando e colando continha alguns erros (o primeiro if retornou nil, err em erro, os dois subsequentes retornaram apenas err ). Esses erros não foram deliberados da minha parte, mas acho que é um estudo de caso interessante.

Embora essa classe particular de erro seja o tipo que teria sido detectado pelo compilador Go, acho interessante notar que, como com tantos clichês sintáticos, torna-se muito fácil ignorar esses erros ao copiar e colar.

Isso não torna a proposta melhor. É uma suposição que a falha em retornar vários valores é o resultado do tratamento explícito de erros. Você também poderia ter cometido os mesmos erros dentro das funções, mas não os teria visto devido ao seu encapsulamento desnecessário.

Discordo, acho que é um ponto forte desse tipo de proposta. Se tudo o que um programa está fazendo é retornar o erro e não processá-lo, então ele está desperdiçando código e sobrecarga cognitiva e tornando as coisas menos legíveis. Adicionar um recurso como esse significaria que (em projetos que optam por usá-lo) o código que lida com erros está, na verdade, fazendo algo que vale a pena entender.

Vamos ter que concordar em discordar. Os tokens mágicos na proposta são fáceis de escrever, mas difíceis de entender. Só porque o tornamos mais curto, não significa que o tornamos mais simples.

Tornar as coisas menos legíveis é subjetivo, então aqui está minha opinião. Tudo o que vejo nesta proposta é um código mais complexo e obscuro com funções e símbolos mágicos (que são muito fáceis de perder). E tudo o que eles fazem é esconder um código muito simples e fácil de entender no caso A. Para mim, eles não agregam nenhum valor, não encurtam o código onde é importante ou simplificam as coisas. Não vejo nenhum valor em tratá-los no nível da linguagem.

O único problema que a proposta resolve, que pude ver claramente, é clichê no tratamento de erros. Se esse é o único motivo, então não vale a pena para mim. O argumento sobre o boilerplate sintático está, na verdade, funcionando contra a proposta. É muito mais complexo a esse respeito - todos aqueles símbolos mágicos e colchetes que são tão fáceis de perder. O exemplo A tem clichê, mas não causa erros de lógica. Nesse contexto, não há nada a ganhar com essa proposta, novamente, o que a torna não muito útil.

Vamos deixar os recursos do Rust para o Rust.

Para esclarecer, não gosto muito de adicionar o sufixo ! como um atalho, mas gosto da ideia de criar uma sintaxe simples que simplifique

err = foo()
if err != nil {
  return err
}

Mesmo que essa sintaxe seja uma palavra-chave em vez de um símbolo especial. É minha maior reclamação sobre a linguagem (ainda maior do que os Genéricos pessoalmente), e acho que a confusão desse padrão no código torna a leitura mais difícil e barulhenta.

Eu também adoraria ver algo que possibilite o tipo de encadeamento que @tarcieri traz à tona, pois acho que é mais legível no código. Acho que a complexidade à qual @creker alude é balanceada pela melhor relação sinal-ruído no código.

Não entendo totalmente como esta proposta alcançaria seus objetivos declarados.

  1. Reduza o boilerplate de manipulação de erros: a proposta tem algum código Go hipotético:

    result := doThing(a).
    Then(func(resp) { doAnotherThing(b, resp.foo()) }).
    Then(func(resp) { FinishUp(c, resp.bar()) })
    
    if result.isError() {
    return result.Error()
    }
    

    Não tenho certeza de como func(resp) { expr } deve funcionar sem mudanças mais extensas na maneira como os literais de função funcionam. Acho que o código resultante ficaria mais parecido com este:

    result := doThing(a).
    Then(func(resp T) result(T, error) { return doAnotherThing(b, resp.foo()) }).
    Then(func(resp T) result(T, error) { return FinishUp(c, resp.bar()) })
    
    if result.isError() {
    return result.Error()
    }
    

    No código Go realista, também é bastante comum que as expressões intermediárias sejam mais longas do que isso e precisem ser colocadas em suas próprias linhas. Isso acontece naturalmente no código Go real hoje; sob esta proposta, seria:

    result := doThing(a).
    Then(func(resp T) result(T, error) {
        return doAnotherThing(b, resp.foo())
    }).
    Then(func(resp T) result(T, error) {
        return FinishUp(c, resp.bar())
    })
    
    if result.isError() {
    return result.Error()
    }
    

    De qualquer forma, isso me parece bom , mas não ótimo, assim como o código Go real acima dele na proposta. Seu combinador 'Então' é essencialmente o oposto de 'retorno'. (Se você está familiarizado com mônadas, isso não será uma surpresa.) Isso remove a necessidade de escrever uma instrução 'if', mas introduz a necessidade de escrever uma função. No geral, não é substancialmente melhor ou pior; é a mesma lógica clichê com uma nova grafia.

  2. Permite ao compilador raciocinar sobre os resultados: se esse recurso é desejável (e não estou expressando nenhuma opinião sobre isso aqui), não vejo como esta proposta o torna substancialmente mais ou menos viável. Eles me parecem ortogonais.

  3. Combinadores de tratamento de erros: esse objetivo é certamente alcançado pela proposta, mas não está totalmente claro se valeria a pena o custo das mudanças necessárias para alcançá-lo, no contexto da linguagem Go tal como se apresenta hoje. (Acho que este é o principal ponto de discórdia na discussão até agora.)

Na maioria dos Go bem escritos, esse tipo de clichê de tratamento de erros constitui uma pequena fração do código. Foi uma porcentagem de linhas de um dígito em minha breve olhada em algumas bases de código de Go que considero bem escritas. Sim, às vezes é apropriado, mas geralmente é um sinal de que alguma reformulação é necessária. Em particular, simplesmente retornar um erro sem adicionar qualquer contexto acontece com mais frequência do que deveria hoje. Pode ser chamado de "anti-idioma". Há uma discussão a ser feita em torno do que Go deve ou poderia fazer para desencorajar esse anti-idioma, se é que algo, tanto no design da linguagem, ou nas bibliotecas, ou nas ferramentas, ou puramente socialmente, ou em alguma combinação daqueles . Eu estaria igualmente interessado em ter essa discussão, quer esta proposta seja ou não aprovada. Na verdade, tornar esse anti-idioma mais fácil de expressar, como acredito ser o objetivo desta proposta, pode criar incentivos errados.

No momento, essa proposta está sendo tratada em grande parte como uma questão de gosto. O que o tornaria mais convincente, em minha opinião, seriam as evidências de que sua adoção reduziria a quantidade total de bugs. Um bom primeiro passo pode ser converter um pedaço representativo do corpus Go para demonstrar que alguns tipos de bugs são impossíveis ou improváveis ​​de serem expressos no novo estilo - que x bugs por linha no código Go real em estado selvagem seriam corrigidos usando o novo estilo. (Parece muito mais difícil demonstrar que o novo estilo não compensa nenhuma melhoria, tornando outros tipos de bugs mais prováveis. Nesse caso, podemos ter que nos contentar com argumentos abstratos sobre legibilidade e complexidade, como nos velhos tempos antes do Go corpus ganhou destaque.)

Com evidências de apoio como essa em mãos, pode-se fazer um caso mais forte.

Simplesmente retornar um erro sem adicionar qualquer contexto acontece com mais frequência do que deveria hoje. Pode ser chamado de "anti-idioma".

Eu gostaria de repetir esse sentimento. Isto

if err := foo(x); err != nil {
    return err
}

não deve ser simplificado, deve ser desencorajado, em favor de, por exemplo,

if err := foo(x); err != nil {
    return errors.Wrapf(err, "fooing %s", x)
}

@peterbourgon

meu maior problema com isso não é que o erro seja retornado às cegas. É o fato de que a ação: foo(x) ; não é tão visível, e imho torna tudo um pouco menos legível do que soluções "funcionais" alternativas, onde a ação em si é um simples retorno em uma nova linha.

mesmo que a atribuição e a ação sejam mantidas separadas da própria instrução if, a instrução resultante ainda colocaria um acento no resultado, ao invés da ação. Isso é perfeitamente válido, especialmente se o resultado for a parte importante. Mas se você tiver um monte de instruções, onde cada uma obtém uma tupla (resultado, erro), verifica o erro / retornos e, em seguida, passa a fazer outra ação enquanto obtém uma nova tupla, os resultados em si obviamente não são os personagens principais no enredo.

@urandom Acho que o resultado é um par de (val, erro), então acho que verificações de erro / retornos são os personagens principais do enredo também.

Que tal uma palavra reservada (algo como reterr ) para evitar todos os if err != nil { return err } ?

Então, é isso

resp, err := doThing(a)
if err != nil {
    return nil, err
}
resp, err = doAnotherThing(b, resp.foo())
if err != nil {
    return nil, err
}
resp, err = FinishUp(c, resp.bar())
if err != nil {
    return nil, err
}

Se tornaria:

resp, _ := reterr doThing(a)
resp, _ = reterr doAnotherThing(b, resp.foo())
resp, _ = reterr FinishUp(c, resp.bar())

reterr verificaria basicamente os valores de retorno da função chamada e retornaria se algum deles fosse um erro e não fosse nulo (e retornaria nulo em qualquer valor de retorno sem erro).

Soa cada vez mais como # 18721

@tarcieri Use apenas alguns dos pacotes reflect . Posso simular algo como sua proposta.
Mas acho que não vale a pena fazer.

https://play.golang.org/p/CC5txvAc0e

func main() {

    result := Do(func() (int, error) {
        return doThing(1000)
    }).Then(func(resp int) (int, error) {
        return doAnotherThing(200000, resp)
    }).Then(func(resp int) (int, error) {
        return finishUp(1000000, resp)
    })

    if result.err != nil {
        log.Fatal(result.err)
    }

    val := result.val.(int)
    fmt.Println(val)
}

@iporsut, existem dois problemas com reflexão que a tornam uma solução inadequada para este problema específico, embora possa parecer "resolver" o problema na superfície:

  1. Sem segurança de tipo : com reflexão, não podemos determinar em tempo de compilação se o fechamento está devidamente tipado. Em vez disso, nosso programa irá compilar independentemente dos tipos, e encontraremos uma falha de tempo de execução se eles forem incompatíveis.
  2. Enorme sobrecarga de desempenho : a abordagem que você está sugerindo não está muito longe da oferecida por go-linq . Eles afirmam que o uso de reflexão para esta finalidade é "5x-10x mais lento". Agora imagine essa quantidade de sobrecarga em cada site de chamada.

Para mim, qualquer um desses problemas é um grande retrocesso em relação ao que Go já fez e, em conjunto, eles são um completo obstáculo.

Gosto do Go e da maneira como ele lida com os erros. No entanto, talvez pudesse ser mais simples. Aqui estão algumas das minhas idéias sobre o tratamento de erros no Go.

Do jeito que está agora:

resp, err := doThing(a)
if err != nil {
    return nil, err
}

resp, err = doAnotherThing(b, resp.foo())
if err != nil {
    return nil, err
}

resp, err = FinishUp(c, resp.bar())
if err != nil {
    return nil, err
}

UMA:

resp, _ := doThing(a) 
resp, _ = doAnotherThing(b, resp.foo())
resp, _ = FinishUp(c, resp.bar())
// return if error is omited, otherwise deal with it as usual (if err != nil { return err })
//However, this breaks semantics of Go and may mislead due to the usa of _ (__ or !_ could be used to avoid such misleading)

B:

resp, err := doThing(a)?
resp, err = doAnotherThing(b, resp.foo())?
resp, err = FinishUp(c, resp.bar())?
// ? indicates that it will return in case of error (more explicit)
// or any other indication could be used
// this approach is preferred for its explicitness

C:

resp, err := doThing(a)
return if err

resp, err = doAnotherThing(b, resp.foo())
return if err

resp, err = FinishUp(c, resp.bar())
return if err
// if err return err
// or if err return (similar to javascript return)
// this one is my favorite, almost no changes to the language, very readable and less SLOC

D:

resp, _ := return doThing(a)
resp, _ = return doAnotherThing(b, resp.foo())
resp, _ = return FinishUp(c, resp.bar())
// or 
resp = throw FinishUp(c, resp.bar())
// this one is also very readable (although maybe a litle less than option **C**) and even less SLOC than **C**
// at this point I'm not sure whether C or D is my favorite )) 

//This applies to all approaches above
// if the function that contains any of these options has no value to return, exit the function. E.g.:
func test() {
    resp, _ := return doThing(a) // or any of other approaches
    // exit function
}

func test() ([]byte, error) {
    resp, _ := return doThing(a) // or any of other approaches
    // return whatever is returned by doThing(a) (this function of course must return ([]byte, error))
}

Desculpe meu inglês e não tenho certeza se tais mudanças são possíveis e se resultarão em sobrecarga de desempenho.

Se você gosta de qualquer uma dessas abordagens, por favor, siga as próximas regras:

A = 👍
B = 😄
C = ❤️
D = 🎉

E 👎 se você não gostar da ideia))

Desta forma, podemos ter algumas estatísticas e evitar comentários desnecessários como "+1"

Eloborando minhas "propostas" ...

// no need to explicitely define error in return statement, much like throw, try {} catch in java
func test() int {
     resp := throw doThing() // "returns" error if doThing returns (throws) an error
     return resp // yep, resp is int
}

func main() {
     resp, err := test() // the last variable is always error type
     if err != nil {
          os.Exit(0)
     }
}

Novamente, não tenho certeza se algo assim é possível))

Aqui está outra opção maluca, tornar a palavra error um pouco mais mágica. Ele se torna utilizável no lado esquerdo de uma atribuição (ou declaração curta) e funciona como uma função mágica:

res, error() := doThing()
// Shorthand for
res, err := doThing()
if err != nil {
  return 0, ..., 0, err
}

Especificamente, o comportamento de error() é o seguinte:

  1. É tratado como se tivesse o tipo error para fins de atribuição.
  2. Se nil for atribuído a ele, nada acontecerá.
  3. Se um nil for atribuído a ele, a função envolvente retorna imediatamente. Todos os valores de retorno são definidos como 0, exceto o último, que deve ser do tipo error e ao qual é atribuído o valor atribuído a error() .

Se quiser aplicar alguma mutação ao erro, você pode fazer:

res, error(func (e error) error { return fmt.Errorf("foo: %s", error)})
  := doThing()

Nesse caso, o fechamento é aplicado ao valor atribuído antes do retorno da função.

Isso é um pouco feio, em grande parte devido ao inchaço sintático de ter que lidar com encerramentos. A biblioteca padrão pode consertar isso bem, por exemplo, error(errors.Wrapper("foo")) que irá gerar o fechamento de wrapper correto para você.

Como alternativa, se a sintaxe nula error() tiver muita probabilidade de ser perdida, eu sugeriria error(return) como alternativa; o uso da palavra-chave reduz o risco de interpretação incorreta. Não se estende bem para a caixa de fechamento, no entanto.

Todos que escreveram Go encontraram a infeliz proliferação de clichês de manipulação de erros que desviam do propósito central de seu código. É por isso que Rob Pike abordou o assunto em 2015 . Como Martin Kühl aponta , a proposta de Rob para simplificar o tratamento de erros:

nos deixa ter que implementar mônadas artesanais únicas para cada interface para a qual queremos lidar com erros, o que eu acho que ainda é tão prolixo e repetitivo

É por isso que há tanto envolvimento neste tópico ainda hoje.

Idealmente, podemos encontrar uma solução que:

  1. Reduz o clichê de manipulação de erros repetitivos e maximiza o foco na intenção primária do caminho do código.
  2. Encoraja o tratamento adequado de erros, incluindo o agrupamento de erros ao propagá-los adiante.
  3. Obedece aos princípios de design Go de clareza e simplicidade.
  4. É aplicável na mais ampla gama possível de situações de tratamento de erros.

Proponho a introdução de uma nova palavra-chave catch: que funciona da seguinte maneira:

Em vez do formulário atual:

res, err := doThing()
if err != nil {
  return 0, ..., 0, err
}

nós escreveríamos:

res, err := doThing() catch: 0, ..., 0, err

que se comportaria exatamente da mesma maneira que o código de formulário atual acima. Mais especificamente, a função e as atribuições à esquerda de catch: são executadas primeiro. Então, se e somente se exatamente um dos argumentos de retorno for do tipo error E esse valor não for nulo, o catch: atua como uma instrução return com os valores para o certo. Se houver zero ou mais de um tipo error retornado de doThing() , é um erro de sintaxe usar catch: . Se o valor de erro retornado de doThing() for nil , então tudo de catch: até o final da instrução será ignorado e não avaliado.

Para dar um exemplo mais complexo da recente postagem do blog de Nemanja Mijailovic intitulada, Padrões de tratamento de erros em Go :

func parse(r io.Reader) (*point, error) {
  var p point

  if err := binary.Read(r, binary.BigEndian, &p.Longitude); err != nil {
    return nil, err
  }

  if err := binary.Read(r, binary.BigEndian, &p.Latitude); err != nil {
    return nil, err
  }

  if err := binary.Read(r, binary.BigEndian, &p.Distance); err != nil {
    return nil, err
  }

  if err := binary.Read(r, binary.BigEndian, &p.ElevationGain); err != nil {
    return nil, err
  }

  if err := binary.Read(r, binary.BigEndian, &p.ElevationLoss); err != nil {
    return nil, err
  }

  return &p, nil
}

Em vez disso, torna-se:

func parse(input io.Reader) (*point, error) {
  var p point

  err := read(&p.Longitude) catch: nil, errors.Wrap(err, "Failed to read longitude")
  err = read(&p.Latitude) catch: nil, errors.Wrap(err, "Failed to read Latitude")
  err = read(&p.Distance) catch: nil, errors.Wrap(err, "Failed to read Distance")
  err = read(&p.ElevationGain) catch: nil, errors.Wrap(err, "Failed to read ElevationGain")
  err = read(&p.ElevationLoss) catch: nil, errors.Wrap(err, "Failed to read ElevationLoss")

  return &p, nil
}

Vantagens:

  1. Perto do padrão mínimo adicional para tratamento de erros.
  2. Melhora o foco na intenção primária do código com o mínimo de bagagem de manuseio de erros no lado esquerdo da instrução e manuseio de erros localizado no lado direito.
  3. Funciona em muitas situações diferentes, dando ao programador flexibilidade no caso de vários valores de retorno (por exemplo, se você quiser retornar um indicador da contagem de itens que tiveram sucesso além do erro).
  4. A sintaxe é simples e pode ser facilmente compreendida e adotada por usuários Go, tanto novos quanto antigos.
  5. Parcialmente consegue encorajar o tratamento adequado de erros, tornando o código de erro mais sucinto. Pode tornar o código de erro um pouco menos provável de ser copiado e colado e, assim, reduzir a introdução de erros comuns de copiar e colar.

Desvantagens:

  1. Essa abordagem não é totalmente bem-sucedida em encorajar o tratamento adequado de erros porque não faz nada para promover erros de agrupamento antes de propagá-los. Em meu mundo ideal, essa nova sintaxe exigiria que o erro retornado por catch: fosse um novo erro ou um erro agrupado, mas não idêntico ao erro retornado pela função à esquerda de catch: . Go foi descrito como "opinativo" e tal rigor no tratamento de erros por uma questão de clareza e confiabilidade teria se encaixado nisso. No entanto, faltou criatividade para incorporar esse objetivo.
  2. Alguns podem argumentar que tudo isso é açúcar sintático e não é necessário na linguagem. Um contra-argumento pode ser que o tratamento de erros atual no Go é gordura trans sintática, e esta proposta apenas a elimina. Para ser amplamente adotada, uma linguagem de programação deve ser agradável de usar. Largely Go tem sucesso nisso, mas o boilerplate de tratamento de erros é uma exceção particularmente abundante.
  3. Estamos "pegando" o erro da função que chamamos ou estamos "jogando" um erro para quem nos chamou? É apropriado ter um catch: sem um lançamento explícito? A palavra reservada não precisa ser necessariamente catch: . Outros podem ter ideias melhores. Pode até ser um operador em vez de uma palavra reservada.

Todos que escreveram Go encontraram a infeliz proliferação de clichês de manipulação de erros que desviam do propósito central de seu código.

Isso não é verdade. Eu programo bastante em Go e não tenho nenhum problema com qualquer erro de manipulação de boilerplate. Escrever código de tratamento de erros consome uma fração microscópica de tempo no desenvolvimento de um projeto que eu quase não noto e IMHO isso não justifica qualquer mudança na linguagem.

Todos que escreveram Go encontraram a infeliz proliferação de clichês de manipulação de erros que desviam do propósito central de seu código.

Isso não é verdade. Eu programo bastante em Go e não tenho nenhum problema com qualquer erro de manipulação de boilerplate. Escrever código de tratamento de erros consome uma fração microscópica de tempo no desenvolvimento de um projeto que eu quase não noto e IMHO isso não justifica qualquer mudança na linguagem.

Eu não disse nada sobre quanto tempo leva para escrever código de tratamento de erros. Eu apenas disse que isso me desvia do propósito central do código. Talvez eu devesse ter dito "Todos que leram Go encontraram a infeliz proliferação de tratamento de erros ...".

Então, @cznic , acho que a pergunta para você é se você leu o código Go que você sentiu que continha uma quantidade excessiva de clichês de manipulação de erros ou que o distraiu do código que você estava tentando entender.

Ninguém gosta das minhas propostas 😅
De qualquer forma, devemos ter alguma sintaxe e votar na melhor (algum sistema de enquete) e incluir o link aqui ou no leia-me

Talvez eu devesse ter dito "Todos que leram Go encontraram a infeliz proliferação de tratamento de erros ...".

Isso não é verdade. Eu prefiro a clareza e a localidade apropriada do atual estado da arte do tratamento de erros. A proposta, como qualquer outra que já vi, torna o código IMHO menos legível e pior de manter.

Então, @cznic , acho que a pergunta para você é se você leu o código Go que você sentiu que continha uma quantidade excessiva de clichês de manipulação de erros ou que o distraiu do código que você estava tentando entender.

Não. Em minha experiência, Go é uma linguagem de programação excepcionalmente bem legível. Metade desse crédito vai para gofmt, é claro.

Minha própria experiência é que realmente começa a se arrastar quando você tem um monte de instruções dependentes, cada uma das quais pode gerar um erro, o tratamento de erros aumenta e envelhece rapidamente. O que poderia ser 5 linhas de código se torna 20.

@cznic
Em minha experiência, ter tantos clichês de manipulação de erros torna o código muito menos legível. Como o tratamento de erros em si é basicamente idêntico (sem qualquer quebra de erro que possa ocorrer), ele produz uma espécie de efeito de cerca, em que se você examinar rapidamente um trecho de código, acabará vendo uma grande quantidade de tratamento de erros. Portanto, o maior problema, o código real, a parte mais importante do programa, está oculto por trás dessa ilusão de ótica, tornando muito difícil ver do que se trata um trecho de código.

O tratamento de erros não deve ser a parte principal de nenhum código. Infelizmente, muitas vezes acaba sendo exatamente isso.
Há um motivo pelo qual a composição de instruções em outras línguas é tão popular.

Porque o tratamento de erros em si é quase idêntico (sem qualquer erro
envolvimento que pode ocorrer), ele produz uma espécie de efeito de cerca, onde se
você examina rapidamente um trecho de código e, na maioria das vezes, acaba vendo uma massa
de tratamento de erros.

Esta é uma posição altamente subjetiva. É como argumentar que as declarações if
tornar o código ilegível ou que as chaves de estilo K&R tornam as coisas ilegíveis.

Do meu ponto de vista, a clareza do tratamento de erros de go desaparece rapidamente
no fundo da familiaridade até que você perceba o padrão quebrado;
algo que o olho humano é muito bom em fazer; falta de tratamento de erros,
variáveis ​​de erro atribuídas a _, etc.

É um fardo digitar, não se engane. Mas Go não otimiza para o
autor de código, otimiza explicitamente para o leitor.

Na terça-feira, 16 de maio de 2017 às 17:45, Viktor Kojouharov < [email protected]

escreveu:

@cznic https://github.com/cznic
Na minha experiência, ter tantos padrões de tratamento de erros torna o código
muito menos legível. Porque o tratamento de erros em si é basicamente idêntico
(sem qualquer empacotamento de erro que possa ocorrer), ele produz uma espécie de cerca
efeito, em que se você examinar rapidamente um trecho de código, acabará quase sempre
vendo uma massa de tratamento de erros. Portanto, o maior problema, o real
código, a parte mais importante do programa, está oculto por trás desta óptica
ilusão, tornando muito difícil ver o que é um pedaço de
código é sobre.

O tratamento de erros não deve ser a parte principal de nenhum código. Infelizmente, bastante
muitas vezes acaba sendo exatamente isso.

-
Você está recebendo isto porque está inscrito neste tópico.
Responda a este e-mail diretamente, visualize-o no GitHub
https://github.com/golang/go/issues/19991#issuecomment-301702623 ou mudo
o segmento
https://github.com/notifications/unsubscribe-auth/AAAcA4ydpBFiapYBOBUyUjg6du5Dnjs5ks5r6VQjgaJpZM4M-dud
.

se você examinar rapidamente um trecho de código, acabará vendo, na maioria das vezes, uma massa
de tratamento de erros.

Esta é uma posição altamente subjetiva.

Altamente subjetivo, mas amplamente compartilhado.

Como o próprio Rob disse,

Um ponto comum de discussão entre os programadores Go, especialmente aqueles novos na linguagem, é como lidar com os erros. A conversa muitas vezes se transforma em um lamento pelo número de vezes que a sequência

if err != nil {
    return err
}

Aparece.

Para ser justo, Rob disse que essa percepção sobre o tratamento de erros Go é "infeliz, enganosa e facilmente corrigida". No entanto, ele gasta a maior parte desse artigo explicando seu método recomendado para corrigir a percepção. Infelizmente, a prescrição de Rob é problemática em si mesma, conforme explicado tão bem por Martin Kühl. Além da crítica de Martin, a sugestão de Rob também reduz a localidade que @cznic diz valorizar no tratamento de erros Go.

Talvez a questão seja se tínhamos a capacidade de substituir

res, err := doThing()
if err != nil {
  return nil, err
}

com algo semelhante a:

res, err := doThing() catch: nil, err

Você o usaria ou continuaria com a versão de quatro linhas? Independentemente de sua preferência pessoal, você acha que uma alternativa como essa seria amplamente adotada pela comunidade Go e se tornaria idiomática? Dada a subjetividade de qualquer argumento de que a versão mais curta afeta negativamente a legibilidade, minha experiência com programadores diz que eles gravitariam fortemente em torno da versão de linha única.

Falar sério: go 1 é fixo e não mudará, especialmente desta forma fundamental.

É inútil propor algum tipo de opção até que Go 2 implemente algum tipo de modelo. Nesse ponto, tudo muda.

Em 16 de maio de 2017, às 23:46, Billy Hinners [email protected] escreveu:

se você examinar rapidamente um trecho de código, acabará vendo, na maioria das vezes, uma massa
de tratamento de erros.

Esta é uma posição altamente subjetiva.

Altamente subjetivo, mas amplamente compartilhado.

Como o próprio Rob disse,

Um ponto comum de discussão entre os programadores Go, especialmente aqueles novos na linguagem, é como lidar com os erros. A conversa muitas vezes se transforma em um lamento pelo número de vezes que a sequência

se errar! = nulo {
return err
}
Aparece.

Para ser justo, Rob disse que essa percepção sobre o tratamento de erros Go é "infeliz, enganosa e facilmente corrigida". No entanto, ele gasta a maior parte desse artigo explicando seu método recomendado para corrigir a percepção. Infelizmente, a prescrição de Rob é problemática em si mesma, conforme explicado tão bem por Martin Kühl. Além da crítica de Martin, a sugestão de Rob também reduz a localidade que @cznic diz valorizar no tratamento de erros Go.

Talvez a questão seja se tínhamos a capacidade de substituir

res, err: = doThing ()
se errar! = nulo {
retornar nulo errar
}
com algo semelhante a:

res, err: = doThing () catch: nil, err

Você o usaria ou continuaria com a versão de quatro linhas? Independentemente de sua preferência pessoal, você acha que uma alternativa como essa seria amplamente adotada pela comunidade Go e se tornaria idiomática? Dada a subjetividade de qualquer argumento de que a versão mais curta afeta negativamente a legibilidade, minha experiência com programadores diz que eles gravitariam fortemente em torno da versão de linha única.

-
Você está recebendo isto porque comentou.
Responda a este e-mail diretamente, visualize-o no GitHub ou ignore a conversa.

É inútil propor algum tipo de opção até que Go 2 implemente algum tipo de modelo. Nesse ponto, tudo muda.

Presumi que estávamos falando sobre Go 2 como implícito no título deste tópico e com plena convicção de que "Go 2" não é um eufemismo para "nunca". Na verdade, como o Go 1 está fixo, provavelmente devemos dedicar uma parte muito maior de nossas discussões sobre Go ao Go 2.

Com isso dito, acho que todos que reclamam da verbosidade do Go's
tratamento de erros está faltando o ponto fundamental de que o propósito do erro
lidar com Go _não_ é fazer o caso de não erro tão breve e discreto
que possível. Em vez disso, o objetivo da estratégia de tratamento de erros de Go é forçar
o escritor do código deve considerar, em todos os momentos, o que acontece quando o
falha na função e, o mais importante, como limpar, desfazer e recuperar
antes de retornar ao chamador.

Todas as estratégias para ocultar a placa de caldeira de manipulação de erros parecem-me ser
ignorando isso.

Na terça- feira, 16 de maio de 2017, 23:51 Dave Cheney

Falar sério: go 1 é fixo e não vai mudar, especialmente neste
forma fundamental.

É inútil propor algum tipo de opção até que Go 2 implemente
alguns para tipo de modelo. Nesse ponto, tudo muda.

Em 16 de maio de 2017, às 23:46, Billy Hinners [email protected] escreveu:

se você examinar rapidamente um trecho de código, acabará vendo, na maioria das vezes, um
massa
de tratamento de erros.

Esta é uma posição altamente subjetiva.

Altamente subjetivo, mas amplamente compartilhado.

Como o próprio Rob disse,

Um ponto comum de discussão entre os programadores Go, especialmente aqueles novos em
a linguagem, é como lidar com os erros. A conversa muitas vezes se transforma em um
lamento pelo número de vezes que a sequência

se errar! = nulo {
return err
}

Aparece.

Para ser justo, Rob disse que essa percepção sobre o tratamento de erros Go é
"infeliz, enganoso e facilmente corrigido." No entanto, ele gasta a maior parte desse
artigo https://blog.golang.org/errors-are-values explicando seu
método recomendado para corrigir a percepção. Infelizmente, Rob's
a prescrição é problemática em si, conforme explicado
https://www.innoq.com/en/blog/golang-errors-monads/ tão bem por Martin
Kühl. Além da crítica de Martin, a sugestão de Rob também reduz o
localidade que @cznic https://github.com/cznic diz valorizar em Go
Manipulação de erros.

Talvez a questão seja se tínhamos a capacidade de substituir

res, err: = doThing ()
se errar! = nulo {
retornar nulo errar
}

com algo semelhante a:

res, err: = doThing () catch: nil, err

Você o usaria ou continuaria com a versão de quatro linhas?
Independentemente de sua preferência pessoal, você acha que uma alternativa como
isso seria amplamente adotado pela comunidade Go e se tornaria idiomático?
Dada a subjetividade de qualquer argumento de que a versão mais curta adversamente
afeta a legibilidade, minha experiência com programadores diz que eles iriam
gravitam fortemente em direção à versão de linha única.

-
Você está recebendo isto porque comentou.
Responda a este e-mail diretamente, visualize-o no GitHub
https://github.com/golang/go/issues/19991#issuecomment-301787215 ou mudo
o segmento
https://github.com/notifications/unsubscribe-auth/AAAcAwATgoJwL5WV-0nffLjLB9L86GYOks5r6ai3gaJpZM4M-dud
.

Em vez disso, o objetivo da estratégia de tratamento de erros de Go é forçar
o escritor do código deve considerar, em todos os momentos, o que acontece quando o
falha na função e, o mais importante, como limpar, desfazer e recuperar
antes de retornar ao chamador.

Bem, então Go não atingiu esse objetivo. Por padrão, Go permite que você ignore os erros retornados e, em muitos casos, você nem saberia disso até que algo em algum lugar não funcionasse como deveria. Ao contrário, as exceções da comunidade Go, muito odiadas (isso é apenas um exemplo para provar o ponto), forçam você a considerá-las porque, caso contrário, o aplicativo travará. Isso geralmente nos leva ao problema de pegar tudo e ignorar, mas isso é culpa do programador.

Basicamente, o tratamento de erros no Go é opcional. É mais sobre a convenção falada que todo erro deve ser tratado. A meta seria alcançada se realmente o forçasse a lidar com os erros. Por exemplo, com erros ou avisos em tempo de compilação.

Com isso em mente, esconder a placa de aquecimento não faria mal a ninguém. A convenção falada ainda se manteria e os programadores ainda optariam pelo tratamento de erros como está agora.

o objetivo da estratégia de tratamento de erros de Go é forçar
o escritor do código deve considerar, em todos os momentos, o que acontece quando o
falha na função e, o mais importante, como limpar, desfazer e recuperar
antes de retornar ao chamador.

Esse é um objetivo indiscutivelmente nobre. É uma meta, porém, que deve ser balanceada com a legibilidade do fluxo primário e a intenção do código.

Como um programador Go, posso dizer a você que não encontro a verbosidade de
Tratamento de erros de Go para prejudicar sua legibilidade. Não vejo nada
negociar, porque não sinto desconforto em _ler_ código escrito por outro
Vá programadores.

Na quarta-feira, 17 de maio de 2017 às 12h10, Billy Hinners [email protected]
escreveu:

o objetivo da estratégia de tratamento de erros de Go é forçar
o escritor do código deve considerar, em todos os momentos, o que acontece quando o
falha na função e, o mais importante, como limpar, desfazer e recuperar
antes de retornar ao chamador.

Esse é um objetivo indiscutivelmente nobre. É uma meta, porém, que deve ser
equilibrado em relação à legibilidade do fluxo primário e intenção do código.

-
Você está recebendo isto porque comentou.
Responda a este e-mail diretamente, visualize-o no GitHub
https://github.com/golang/go/issues/19991#issuecomment-301794653 ou mudo
o segmento
https://github.com/notifications/unsubscribe-auth/AAAcAzfcu5hq86xxVj85qfOquVawHh44ks5r6a5zgaJpZM4M-dud
.

@davecheney , Considerando que eu concordo com você que o tratamento de erros deve ser explícito e não adiado para depois (o que você, é claro, pode fazer com _), há também a estratégia de "borbulhar" erros para lidar com eles em uma função, de para adicionar informações extras ou remover (antes de enviá-las ao cliente). Meu problema pessoal é que tenho que escrever as mesmas 4 linhas de código repetidamente

Por exemplo:

getNewToken (id int64) (Token, erro) {

user := &User{ID:id}

u, err := user.Get();
if err != nil {
    return Token{}, err
}

token, err := token.New(u);
if err != nil {
    return Token{}, err
}
return token, nil

}
Não estou tratando do erro aqui, estou apenas retornando. e quando leio esse tipo de código, tenho que pular o "tratamento" de erros e é difícil encontrar o propósito principal do código

e o código acima poderia ser facilmente substituído por algo assim:

getNewToken (id int64) (Token, erro) {

user := &User{ID:id}

u, err := throw user.Get(); //throw should also wrap the error

token, err := throw token.New(u);

return token, nil

}
Um código como esse é mais legível e menos desnecessário (IMHO). E o erro pode e deve ser tratado na função onde esta função é usada.

Como um programador Go, posso dizer a você que não considero o detalhamento do tratamento de erros do Go prejudicando sua legibilidade.

Eu concordo.

Em uma nota não relacionada:

Também me parece que um tipo de "resultado" é um pouco específico demais para uma proposta; talvez os tipos sejam apenas tipos enumerados de duas variantes. Se houvesse um conceito de enums, um resultado ou pacote de opções poderia ser criado fora da árvore e experimentado antes de adicioná-lo à linguagem e sem adicionar muita sintaxe ou métodos extras que não podem ser realmente reutilizados e são apenas bons para tipos de resultado. Não sei se enums seria útil em Go ou não, mas se você pode argumentar o caso mais geral, provavelmente também tornará seu caso mais forte para o tipo de resultado mais específico (eu suspeito; talvez eu esteja errado).

func getNewToken (id int64) (Token, erro) {
usuário: = & Usuário {ID: id}

u, err := user.Get()
if err != nil {
    return Token{}, err
}

return token.New(u)

}

Parece equivalente.

Na quarta-feira, 17 de maio de 2017 às 12h34, Kiura [email protected] escreveu:

@davecheney https://github.com/davecheney , Considerando que concordo com você
que o tratamento de erros deve ser explícito e não adiado para mais tarde (o que
você, é claro, pode fazer com _), há também a estratégia de "borbulhar"
erros para lidar com eles em uma função, para adicionar informações extras ou
remover (antes de enviá-lo ao cliente). Meu problema pessoal é que eu tenho
escrever as mesmas 4 linhas de código repetidamente

Por exemplo:

getNewToken (id int64) (Token, erro) {

usuário: = & Usuário {ID: id}

u, errar: = usuário.Get ();
se errar! = nulo {
return token {}, err
}

token, err: = token.New (u);
se errar! = nulo {
return token {}, err
}
token de retorno, nulo

}
Não estou tratando do erro aqui, estou apenas retornando. e quando eu leio
este tipo de código, tenho que pular o "tratamento" de erros e é difícil encontrar
o objetivo principal do código

e o código acima poderia ser facilmente substituído por algo assim:

getNewToken (id int64) (Token, erro) {

usuário: = & Usuário {ID: id}

u, errar: = lançar usuário.Get (); // lançar também deve encerrar o erro

token, err: = lançar token.New (u);

token de retorno, nulo

}
Um código como esse é mais legível e menos desnecessário (IMHO). E a
o erro pode e deve ser tratado na função onde esta função é
usava.

-
Você está recebendo isso porque foi mencionado.
Responda a este e-mail diretamente, visualize-o no GitHub
https://github.com/golang/go/issues/19991#issuecomment-301802010 ou mudo
o segmento
https://github.com/notifications/unsubscribe-auth/AAAcA9sIRXX7RSdDcUOidpe-qLTR7unNks5r6bP3gaJpZM4M-dud
.

Também me parece que um tipo de "resultado" é um pouco específico demais para uma proposta; talvez os tipos sejam apenas tipos enumerados de duas variantes. Se houvesse um conceito de enums, um resultado ou pacote de opções poderia ser criado fora da árvore e experimentado antes de adicioná-lo à linguagem e sem adicionar muita sintaxe ou métodos extras que não podem ser realmente reutilizados e são apenas bons para tipos de resultado. Não sei se enums seria útil em Go ou não, mas se você pode argumentar o caso mais geral, provavelmente também tornará seu caso mais forte para o tipo de resultado mais específico (eu suspeito; talvez eu esteja errado).

Conforme declarado na proposta original, um tipo de resultado seria idealmente implementado como um tipo de soma (por exemplo, enums ala Rust's), e há uma proposta aberta para adicioná-los à linguagem.

No entanto, os tipos de soma por si só não são suficientes para implementar uma biblioteca de tipo de resultado reutilizável sem suporte de linguagem adicional. Eles também requerem genéricos.

Esta proposta explorava a ideia de implementar um tipo de resultado que não dependa de genéricos, mas sim de um caso especial de ajuda do compilador.

Vou apenas acrescentar que, agora que postou, concordaria que a melhor maneira de fazer isso (se é que o faria) seria com suporte a genéricos no nível de linguagem.

@davecheney , Na verdade, neste caso quase nenhuma diferença, mas e se você tiver 3-4 chamadas na função que retornam erro?

PS: Não sou contra a forma como a estrutura Go1 lida com erros, só acho que poderia ser melhor.

Conforme declarado na proposta original, um tipo de resultado seria idealmente implementado como um tipo de soma (por exemplo, enums ala Rust's), e há uma proposta aberta para adicioná-los à linguagem.

Desculpe, eu deveria ter sido mais claro: eu estava argumentando que esta afirmação:

Acho que um tipo de resultado Go também não precisa e pode simplesmente aproveitar a mágica do compilador de caso especial.

parece uma má ideia para mim.

No entanto, os tipos de soma por si só não são suficientes para implementar uma biblioteca de tipo de resultado reutilizável sem suporte de linguagem adicional. Eles também requerem genéricos.

Esta proposta explorava a ideia de implementar um tipo de resultado que não dependa de genéricos, mas sim de um caso especial de ajuda do compilador.

Vou apenas acrescentar que, agora que postou, concordaria que a melhor maneira de fazer isso (se é que o faria) seria com suporte a genéricos no nível de linguagem.

Sim, é justo; Estou concordando com sua última declaração então. Se tivermos que esperar pelo Go 2 de qualquer maneira, podemos também resolver o problema mais geral primeiro (presumindo que seja realmente um problema) :)

Além disso, Rob Pike escreveu um artigo sobre o tratamento de erros mencionado acima. Enquanto essa abordagem parece estar "consertando" o problema, ela introduz outro: mais inchaço de código com interfaces.

Acho importante não confundir "tratamento de erros explícito" com "tratamento de erros detalhado". Go quer forçar o usuário a considerar o tratamento de erros em cada etapa, em vez de delegá-lo. Para cada função que você chama que pode gerar um erro, você precisa decidir de alguma forma se deseja ou não tratar o erro e como. Às vezes, significa que você ignora o erro, às vezes significa que você tenta novamente, muitas vezes significa que você simplesmente passa para o autor da chamada para lidar com ele.

O artigo de Rob é ótimo e realmente deveria fazer parte do Effective Go 2, mas é uma estratégia que só pode levar você até certo ponto. Especialmente ao lidar com callees heterogêneos, você tem um monte de tratamento de erros para gerenciar

Eu não acho que seja razoável considerar o açúcar sintático ou algum outro recurso para ajudar no tratamento de erros. Acho importante que isso não prejudique os fundamentos do tratamento de erros do Go. Por exemplo, estabelecer um manipulador de erros em nível de função que trate todos os erros que ocorrem seria ruim; significa que estamos permitindo que o programador faça o que o tratamento de exceções normalmente faz: mover a consideração de erros de um problema de nível de instrução para um problema de nível de bloco ou função. Isso definitivamente é contra a filosofia.

@billyh Com relação ao artigo "Padrões de tratamento de erros em Go", existem outras soluções:

@egonelbre
Essas soluções são adequadas apenas para quem está fazendo o mesmo tipo de operação repetidamente. Normalmente não é esse o caso. Portanto, isso dificilmente pode ser aplicado na prática.

@urandom, por favor, mostre um exemplo realista, então?

Claro que posso dar um exemplo mais complicado :

func (conversion *PageConversion) Convert() (page *kb.Page, errs []error, fatal error)

Eu entendo que eles não se aplicam a todos os lugares, mas sem uma lista adequada de exemplos que queremos melhorar, não há como ter uma discussão decente.

@egonelbre

https://github.com/juju/juju/blob/01b24551ecdf20921cf620b844ef6c2948fcc9f8/cloudconfig/providerinit/providerinit.go

Isenção de responsabilidade: eu não usei juju, nem li o código. É apenas um produto de 'produção' que conheço no topo da minha cabeça. Estou razoavelmente certo de que esse tipo de tratamento de erros (em que os erros são verificados entre as operações independentes) prevalece no mundo go, e duvido muito que haja alguém por aí que não tenha tropeçado nisso.

@urandom eu concordo. O principal problema com a discussão sem código do mundo real é que as pessoas se lembram da "essência" do problema, não do problema real - o que geralmente leva a uma declaração de problema simplificada demais. _PS: Lembrei-me de um bom exemplo em go ._

Por exemplo, a partir desses exemplos do mundo real, podemos ver que há várias outras coisas que precisam ser consideradas:

  • boas mensagens de erro
  • recuperação / caminhos alternativos com base no valor de erro
  • substitutos
  • execução de melhor esforço com erros
  • registro de casos felizes
  • log de falha
  • rastreamento de falha
  • múltiplos erros sendo retornados
  • _Claro, alguns deles serão usados ​​juntos_
  • ... provavelmente alguns que eu perdi ...

Não apenas o caminho "feliz" e "fracasso". Não estou dizendo que eles não possam ser resolvidos, apenas que precisam ser mapeados e discutidos.

@egonelbre aqui está outro exemplo do Golang Weekly desta semana, no artigo de Mario Zupan intitulado "Escrevendo um gerador de blog estático em Go":

func (ds *GitDataSource) Fetch(from, to string) ([]string, error) {
    fmt.Printf("Fetching data from %s into %s...\n", from, to)
    if err := createFolderIfNotExist(to); err != nil {
        return nil, err
    }
    if err := clearFolder(to); err != nil {
        return nil, err
    }
    if err := cloneRepo(to, from); err != nil {
        return nil, err
    }
    dirs, err := getContentFolders(to)
    if err != nil {
        return nil, err
    }
    fmt.Print("Fetching complete.\n")
    return dirs, nil
}

Observação: não estou sugerindo nenhuma crítica ao código de Mario. Na verdade, gostei bastante de seu artigo.
Infelizmente, exemplos como esse são muito comuns no código-fonte Go. O código Go gravita em torno desse padrão de trilhos de trem de uma linha de interesse seguida por três linhas de clichês idênticos ou quase idênticos repetidos várias vezes. Combinar a atribuição e a condicional sempre que possível, como faz Mario, ajuda um pouco.

Não tenho certeza se alguma linguagem de programação foi projetada com o objetivo principal de minimizar linhas de código, mas a) a proporção de código significativo para clichê pode ser uma (de muitas) medidas válidas da qualidade de uma linguagem de programação, eb) como grande parte da programação envolve manipulação de erros, esse padrão permeia o código Go e, portanto, torna esse caso particular de excesso de clichê e mérito de racionalização.

Se conseguirmos identificar uma boa alternativa, acredito que ela será rapidamente adotada e tornará o Go ainda mais agradável de ler, escrever e manter.

Rebecca Skinner (@cercerilla) compartilhou um excelente artigo sobre as deficiências de tratamento de erros de Go, juntamente com uma análise do uso de mônadas como uma solução em seu conjunto de slides Monadic Error Handling in Go . Gostei particularmente de suas conclusões no final.

Agradeço a @davecheney por se referir ao deck de Rebecca em seu artigo, Simplicity Debt Redux, que me permitiu encontrá-lo. (Agradeço também a Dave por fundamentar meu otimismo rosado para o Go 2 com as realidades mais corajosas.)

O código Go gravita em torno desse padrão de trilhos de trem de uma linha de interesse seguida por três linhas de clichês idênticos ou quase idênticos repetidos várias vezes.

Cada declaração de controle de fluxo de controle é importante. As linhas de tratamento de erros são extremamente importantes do ponto de vista da correção.

a proporção de código significativo para boilerplate pode ser uma (de muitas) medidas válidas da qualidade de uma linguagem de programação

Se alguém considerar que as instruções de tratamento de erros não são significativas, boa sorte com a codificação e espero ficar longe dos resultados.

Para abordar um dos pontos abordados no Simplicity Debt Redux de @davecheney (que eu vale a

A próxima pergunta é: essa forma monádica se tornaria a única maneira de lidar com os erros?

Para que algo assim se torne a forma "única" de tratamento dos erros, seria necessária uma alteração significativa em toda a biblioteca padrão e em todos os projetos compatíveis com "Go2". Acho isso imprudente: o desastre do Python2 / 3 mostra como cismas como essa podem ser prejudiciais aos ecossistemas da linguagem.

Conforme mencionado nesta proposta, se um tipo de resultado pudesse forçar automaticamente a forma de tupla equivalente, você poderia ter seu bolo e comê-lo também em termos de uma biblioteca padrão Go2 hipotética adotando essa abordagem em todas as áreas, mantendo a compatibilidade retroativa com o código existente . Isso permitiria que os interessados ​​tirassem proveito disso, mas as bibliotecas que ainda desejam trabalhar no Go1 funcionarão prontamente. Os autores de bibliotecas podem ter sua escolha: escrever bibliotecas que funcionem em Go1 e Go2 usando o estilo antigo ou Go2 apenas usando o estilo monádico.

A "maneira antiga" e a "nova maneira" de tratamento de erros poderiam ser compatíveis a ponto de os usuários da linguagem não precisarem nem mesmo pensar sobre isso e continuar fazendo as coisas da "maneira antiga" se quisessem. Embora falte uma certa pureza conceitual, acho que é muito menos importante do que permitir que o código existente continue funcionando sem modificações e também permitir que as pessoas desenvolvam bibliotecas que funcionem com todas as versões da linguagem, não apenas a mais recente.

Parece confuso e fornece orientação pouco clara para os recém-chegados ao Go 2.0, para continuar a oferecer suporte ao modelo de interface de erro e a um novo tipo talvez monádico.

Eles são os freios: ou deixe a linguagem congelada como está, ou evolua a linguagem, adicionando complexidade incidental e relegando maneiras anteriores de fazer as coisas a verrugas legadas. Eu realmente acho que essas são as únicas duas opções, já que adicionar um novo recurso que substitui um antigo, se o recurso antigo está obsoleto, mas compatível ou fora da porta na forma de uma mudança significativa, é algo que eu acho que os usuários da linguagem terá que aprender independentemente.

Não acho que seja possível mudar a linguagem, mas faça com que os novatos evitem aprender tanto a "maneira antiga" quanto a "nova" de fazer as coisas, mesmo que o Go2 hipoteticamente adotasse isso de uma vez. Você ainda ficaria com uma cisão Go1 e Go2, e os novatos se perguntarão quais são as diferenças e, inevitavelmente, acabarão tendo que aprender "Go1" de qualquer maneira.

Acho que a compatibilidade com versões anteriores é útil tanto para o ensino da linguagem quanto para a compatibilidade do código: Todos os materiais existentes que ensinam Go continuarão sendo válidos, mesmo se a sintaxe estiver desatualizada. Não haverá necessidade de percorrer cada pedacinho do material didático Go e invalidar a sintaxe antiga: o material didático poderia, à sua vontade, acrescentar um aviso de que há uma nova sintaxe.

Eu entendo que "Há mais de uma maneira de fazer isso" geralmente vai contra a filosofia Go de simplicidade e minimalismo, mas é o preço que deve ser pago para adicionar novos recursos de linguagem. Os novos recursos de linguagem irão, por sua natureza, tornar as abordagens mais antigas obsoletas.

Certamente estou disposto a admitir que pode haver uma maneira de resolver o mesmo problema central de uma forma que seja mais natural para os Esquilos, e não uma mudança tão chocante em relação à abordagem existente.

Mais uma coisa a se considerar: embora Go tenha feito um trabalho exemplar em manter o idioma fácil de aprender, esse não é o único obstáculo envolvido em integrar as pessoas a um idioma. Acho que é seguro dizer que há várias pessoas que olham para a verbosidade do tratamento de erros de Go e ficam desanimadas, algumas a ponto de se recusarem a adotar a linguagem.

Acho que vale a pena perguntar se as melhorias no idioma poderiam atrair as pessoas que atualmente estão desanimadas e como isso se equilibra com a dificuldade de aprender o idioma.

No entanto, fazer algo como tratamento de erros monádico vai contra a filosofia de Go de fazer você pensar sobre os erros. O tratamento de erros monádicos e o tratamento de exceções no estilo Java são muito próximos na semântica (embora difiram na sintaxe). Go adotou uma filosofia deliberadamente diferente de esperar que o programador lide explicitamente com cada erro, em vez de apenas adicionar o código de tratamento de erros ao pensar nisso. Na verdade, o idioma return nil, err é, estritamente falando, o ideal porque você provavelmente pode adicionar contexto útil adicional.

Acho que qualquer tentativa de lidar com o erro Go deve ter isso em mente, e não tornar mais fácil evitar pensar sobre os erros.

@alercah, eu praticamente tenho que discordar de tudo que você acabou de dizer ...

Fazer algo como tratamento de erros monádico vai contra a filosofia de Go de fazer você pensar sobre os erros

Vindo do Rust, acho que o Rust (ou melhor, o compilador Rust) realmente me faz pensar mais em erros do que em Go. Rust tem um atributo # [must_use] em seu tipo Result que significa que os resultados não utilizados geram um aviso do compilador. Isso não é verdade em Go (Rebecca Skinner aborda isso em sua palestra): o compilador Go não avisará para, por exemplo, valores error não manipulados.

O sistema de tipo Rust impõe que cada caso de erro seja abordado em seu código e, se não, é um erro de tipo ou, na melhor das hipóteses, um aviso.

O tratamento de erros monádicos e o tratamento de exceções no estilo Java são muito próximos na semântica (embora difiram na sintaxe).

Deixe-me explicar por que isso não é verdade:

Estratégia de Propagação de Erro

  • Go: valor de retorno, propagado explicitamente
  • Java: salto não local, propagado implicitamente
  • Ferrugem: valor de retorno, propagado explicitamente

Tipos de Erro

  • Go: um valor de retorno por função, geralmente 2 tuplas de (success, error)
  • Java: exceções verificadas que consistem em muitos tipos de exceção, representando a união do conjunto de todas as exceções potencialmente lançadas de um método. Além disso, exceções não verificadas que não são declaradas e podem acontecer em qualquer lugar a qualquer momento.
  • Ferrugem: um valor de retorno por função, geralmente Result sum type, por exemplo Result<Success, Error>

Em suma, sinto que Go está muito mais próximo do Rust do que do Java quando se trata de tratamento de erros: erros em Go e Rust são apenas valores, não são exceções. Você deve aceitar a propagação explicitamente. Você deve converter erros de um tipo diferente para aquele que uma determinada função retorna, por exemplo, por meio de empacotamento. Ambos representam, em última análise, um par de valor / erro de sucesso, apenas usando recursos de sistema de tipos diferentes (tuplas versus tipos de soma genéricos).

Existem algumas exceções onde o Rust fornece algumas abstrações que podem ser usadas eletivamente em uma base de caixa por caixa para fazer o tratamento de erro implícito (ou melhor, conversão de erro explícita, você ainda tem que propagar manualmente o erro). Por exemplo, o traço From pode ser usado para converter automaticamente erros de um tipo para outro. Pessoalmente, acho que ser capaz de definir uma política totalmente voltada para um determinado pacote que permite a conversão automática de erros de um tipo explícito em outro é uma vantagem, não uma desvantagem. O sistema de características do Rust só permite que você defina From para tipos em sua própria caixa, evitando qualquer tipo de ação fantasmagórica à distância.

Isso está bem fora do escopo desta proposta e envolve vários recursos de linguagem que o Go não tem funcionando em conjunto, então não acho que haja qualquer tipo de ladeira escorregadia onde o Go esteja "em risco" de suportar esses tipos de conversões implícitas , pelo menos não até Go adicionar genéricos e traits / typeclasses.

Para jogar meus dois centavos sobre este assunto. Acho que esse tipo de funcionalidade seria muito útil para empresas (como meu próprio empregador) onde aplicativos únicos conversam com um grande número de fontes de dados subsidiárias e compõem resultados de maneira direta.

Aqui está um exemplo de dados representativo de algum fluxo de código que teríamos

func generateUser(userID : string) (User, error) {
      siteProperties, err := clients.GetSiteProperties()
      if err != nil {
           return nil, err
     }
     chatProperties, err := clients.GetChatProperties()
      if err != nil {
           return nil, err
     }

     followersProperties, err := clients.GetFollowersProperties()
      if err != nil {
           return nil, err
     }


// ... (repeat X5)
     return createUser(siteProperties, ChatProperties, followersProperties, ... /*other properties here */), nil
}

Eu entendo muito do retrocesso projetado pelo Go para forçar um usuário a pensar sobre os erros em cada ponto, mas em bases de código onde a grande maioria das funções retorna T, err , isso leva a um aumento substancial do código do mundo real e realmente levou a falhas de produção porque alguém se esquece de adicionar o código de tratamento de erros após fazer uma chamada de função adicional, e o erro silenciosamente fica desmarcado. Além disso, não é raro, na verdade, que alguns de nossos serviços mais tagarelas tenham aproximadamente 20% + tratamento de erros, sendo muito pouco interessante.

Além disso, a grande maioria dessa lógica de tratamento de erros é idêntica e, paradoxalmente, a grande quantidade de tratamento de erros explícito em nossas bases de código torna difícil encontrar o código onde o caso excepcional é realmente interessante porque há um pouco de 'agulha no palheiro 'fenômenos em jogo.

Posso ver definitivamente por que essa proposta em particular pode não ser a solução, mas acredito que deve haver _alguma_ maneira de reduzir esse clichê.

Mais alguns pensamentos ociosos:

Rust's trailing ? é uma sintaxe agradável. Para Go, dada a importância do contexto de erro, no entanto, eu talvez sugerisse a seguinte variação:

  • O final de ? funciona como Rust, modificado para Go. Especificamente: ele só pode ser usado em uma função cujo último valor de retorno é do tipo error , e deve aparecer imediatamente após uma chamada de função cujo último valor de retorno também é do tipo error (observação: poderíamos permitir qualquer tipo que implemente error também, mas exigir error evita que o problema de interface nula apareça, o que é um bom bônus). O efeito é que se o valor do erro não for nulo, a função em que ? aparece retorna da função, definindo o último parâmetro como o valor do erro. Para funções que usam valores de retorno nomeados, ele pode retornar zeros para os outros valores ou qualquer valor armazenado atualmente; para funções que não o fazem, os outros valores de retorno são sempre zero.
  • O rastreamento .?("opening %s", file) funciona como o descrito acima, exceto que, em vez de retornar o erro sem modificações, ele é passado por uma função que compõe os erros; grosso modo, .?(str, vals...) altera o erro como fmt.Errorf(str + ": %s", vals..., err)
  • Possivelmente deve haver uma versão, uma variante da sintaxe .? ou outra diferente, cobrindo o caso em que um pacote deseja exportar um tipo de erro distinto.

Relacionado a # 19412 (tipos de soma) e # 21161 (tratamento de erros) e # 15292 (genéricos).

Relacionado:

"Projetos de rascunho" para novos recursos de tratamento de erros:
https://go.googlesource.com/proposal/+/master/design/go2draft.md

Feedback sobre o design dos erros:
https://github.com/golang/go/wiki/Go2ErrorHandlingFeedback

Eu gosto da sugestão do @alercah para resolver apenas esse recurso irritante do go-lang de que o @LegoRemix está falando, em vez de criar um tipo de retorno separado.

Eu apenas sugeriria seguir o RFC de Rust ainda mais para evitar adivinhar valores zero e introduzir catch expression para permitir que a função especifique explicitamente o que é retornado caso o corpo principal retorne um erro:

Então, é isso:

func generateUser(userID string) (*User, error) {
    siteProperties, err := clients.GetSiteProperties()
    if err != nil {
         return nil, errors.Wrapf(err, "error generating user: %s", userID)
    }

    chatProperties, err := clients.GetChatProperties()
    if err != nil {
         return nil, errors.Wrapf(err, "error generating user: %s", userID)
    }

    followersProperties, err := clients.GetFollowersProperties()
    if err != nil {
         return nil, errors.Wrapf(err, "error generating user: %s", userID)
    }

    return createUser(siteProperties, ChatProperties, followersProperties), nil
}

Torna-se este código DRY:

func generateUser(userID string) (*User, error) {
    siteProperties := clients.GetSiteProperties()?
    chatProperties := clients.GetChatProperties()?
    followersProperties := clients.GetFollowersProperties()?

    return createUser(siteProperties, ChatProperties, followersProperties), nil
} catch (err error) {
    return nil, errors.Wrapf(err, "error generating user: %s", userID)
}

E exigir que a função que está usando o operador ? também defina catch

@bradfitz @peterbourgon @SamWhited Talvez devesse haver outro problema para isso?

@sheerun Seu ? e sua instrução catch parecem muito semelhantes ao operador check e à instrução handle no novo design de rascunho de tratamento de erros (https: //go.googlesource.com/proposal/+/master/design/go2draft.md).

Parece ainda melhor, para pessoas curiosas, esta é a aparência do meu código com check e handle :

func generateUser(userID string) (*User, error) {
    handle err { return nil, errors.Wrapf(err, "error generating user: %s", userID) }

    siteProperties := check clients.GetSiteProperties()
    chatProperties := check clients.GetChatProperties()
    followersProperties := check clients.GetFollowersProperties()

    return createUser(siteProperties, chatProperties, followersProperties), nil
}

A única coisa que eu mudaria é me livrar dos handle implícitos e exigir que seja definido se o cheque for usado. Isso impedirá que os desenvolvedores usem preguiçosamente a verificação e pensem mais em como lidar ou encerrar o erro. O retorno implícito deve ser um recurso separado e pode ser usado como proposto antes:

func generateUser(userID string) (*User, error) {
    handle err { return _, errors.Wrapf(err, "error generating user: %s", userID) }

    siteProperties := check clients.GetSiteProperties()
    chatProperties := check clients.GetChatProperties()
    followersProperties := check clients.GetFollowersProperties()

    return createUser(siteProperties, chatProperties, followersProperties), nil
}

Como autor desta proposta, acho que vale a pena notar que ela foi efetivamente invalidada por # 15292 e funciona como https://go.googlesource.com/proposal/+/master/design/go2draft-contracts.md , conforme esta proposta foi escrito assumindo que não há recursos de programação genéricos disponíveis. Como tal, sugere uma nova sintaxe para permitir o polimorfismo de tipo para o caso especial de result() , e se isso pode ser evitado usando, por exemplo, contratos, acho que esta proposta não faz mais sentido.

Visto que parece que pelo menos um desses provavelmente acabará no Go 2, estou me perguntando se esta proposta em particular deve ser encerrada e se as pessoas ainda estão interessadas em um tipo de resultado como alternativa a handle , que seja reescrito, assumindo, por exemplo, que os contratos estão disponíveis.

(Observe que provavelmente não tenho tempo para fazer esse trabalho, mas se alguém estiver interessado em ver essa ideia adiante, vá em frente)

@sheerun o local para enviar comentários e ideias sobre o tratamento de erros do Go 2 é esta página wiki:
https://github.com/golang/go/wiki/Go2ErrorHandlingFeedback

e / ou esta lista abrangente de _Requisitos a considerar para tratamento de erros Go 2: _
https://gist.github.com/networkimprov/961c9caa2631ad3b95413f7d44a2c98a

Esta página foi útil?
0 / 5 - 0 avaliações