Go: proposition : spec : ajouter un type de résultat intégré (comme Rust, OCaml)

Créé le 15 avr. 2017  ·  79Commentaires  ·  Source: golang/go

Il s'agit d'une proposition pour ajouter un type de résultat à emporter. Les types de résultats contiennent généralement soit une valeur renvoyée, soit une erreur, et pourraient fournir une encapsulation de première classe du modèle (value, err) commun omniprésent dans les programmes Go.

Mes excuses si quelque chose comme cela a déjà été soumis, mais j'espère que c'est une description assez complète de l'idée.

Fond

Des informations sur cette idée peuvent être trouvées dans l'article Gestion des erreurs dans Go , bien que là où cet article suggère les génériques de levier de mise en œuvre, je proposerai que ce n'est pas nécessaire, et qu'en fait les types de résultats pourraient (avec un certain soin) être rétrofit à Go à la fois sans ajouter de génériques et sans apporter de modifications importantes à la langue elle-même.

Cela dit, j'applique moi-même le label "Go 2" non pas parce qu'il s'agit d'un changement décisif, mais parce que je m'attends à ce qu'il soit controversé et, dans une certaine mesure, aille à l'encontre du langage.

Le type Rust Result fournit un précédent. Une idée similaire peut être trouvée dans de nombreux langages fonctionnels, y compris l'un ou l' résultat d'OCaml et l'un ou l' chaînes d'erreurs .

Là où Rust utilise des types de somme (voir la proposition de types de somme Go 2 ) et des génériques pour implémenter des types de résultats, en tant que fonctionnalité de langage de base de cas spécial, je pense qu'un type de résultat Go n'en a pas besoin non plus et peut simplement tirer parti de la magie du compilateur de cas particulier. Cela impliquerait une syntaxe spéciale et des nœuds AST spéciaux tout comme les types de collection de Go utilisent actuellement.

Buts

Je pense que l'ajout d'un type de résultat à emporter pourrait avoir les résultats positifs suivants :

  1. Réduire la gestion des erreurs standard : il s'agit d'une plainte extrêmement courante à propos de Go. Le "motif" if err != nil { return nil, err } (ou des variations mineures de celui-ci) peut être vu partout dans les programmes Go. Ce passe-partout n'ajoute aucune valeur et ne sert qu'à allonger les programmes.
  2. Permet au compilateur de raisonner sur les résultats : dans Rust, les résultats non consommés émettent un avertissement. Bien qu'il existe des outils de linting pour Go pour accomplir la même chose, je pense qu'il serait beaucoup plus utile que ce soit une fonctionnalité de première classe du compilateur. C'est également une méthode assez simple à implémenter et ne devrait pas affecter négativement les performances du compilateur.
  3. Les combinateurs de gestion d'erreurs (c'est la partie qui, selon moi, va à l'encontre du langage) : s'il y avait un type pour les résultats, il pourrait prendre en charge un certain nombre de méthodes pour gérer, transformer et consommer les résultats. J'admets que cette approche s'accompagne d'une courbe d'apprentissage et, en tant que telle, peut avoir un impact négatif sur la clarté des programmes pour les personnes qui ne sont pas familiarisées avec les idiomes du combinateur. Bien que personnellement j'aime les combinateurs pour la gestion des erreurs, je peux certainement voir à quel point ils peuvent être culturellement mal adaptés au Go.

Exemples de syntaxe

Tout d'abord, une petite note : s'il vous plaît, ne laissez pas l'idée s'enliser dans la syntaxe. La syntaxe est une chose très facile à bikeshed, et je ne pense pas qu'aucun de ces exemples ne serve de One True Syntax, c'est pourquoi je donne plusieurs alternatives.

Au lieu de cela, je préférerais que les gens prêtent attention à la "forme" générale du problème et ne regardent que ces exemples pour mieux comprendre l'idée.

Type de résultat signature

La chose la plus simple qui fonctionne : ajoutez simplement "result" devant le tuple de la valeur de retour :

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

Plus typique est une syntaxe "générique", mais cela devrait probablement être réservé pour si/quand Go ajoute réellement des génériques (une fonctionnalité de type résultat pourrait être adaptée pour les exploiter si cela se produisait):

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

Lors du retour des résultats, nous aurons besoin d'une syntaxe pour envelopper les valeurs ou les erreurs dans un type de résultat. Cela pourrait simplement être une invocation de méthode :

return result.Ok(value)

``` va
renvoyer le résultat.Err(erreur)

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)

``` va
return Err(valeur)

### 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
}

Maintenant, avec une nouvelle syntaxe qui consomme un résultat et fait remonter l'erreur dans la pile pour vous. Veuillez garder à l'esprit que ces exemples sont uniquement à des fins d'illustration :

count := fd.Write!(bytes)

``` va
count := fd.Write(bytes) !

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

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

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

REMARQUE : Rust prenait auparavant en charge ce dernier, mais s'en est généralement éloigné car il n'est pas chaînable.

Dans tous mes exemples suivants, j'utiliserai cette syntaxe, mais veuillez noter que ce n'est qu'un exemple, qu'il peut être ambigu ou poser d'autres problèmes, et je n'y suis certainement pas marié :

count := fd.Write(bytes)!

Rétrocompatibilité

Les propositions de syntaxe utilisent toutes un mot-clé result pour identifier le type. Je crois (mais je ne suis certainement pas certain) que des règles d'observation pourraient être développées qui permettraient au code existant utilisant "résultat" pour, par exemple, un nom de variable de continuer à fonctionner tel quel sans problème.

Idéalement, il devrait être possible de "mettre à niveau" le code existant pour utiliser les types de résultats de manière totalement transparente. Pour ce faire, nous pouvons permettre que les résultats soient consommés sous forme de 2-tuple, c'est-à-dire donnés :

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

Il devrait être possible de le consommer soit comme :

result := f1(42)

ou:

(value, err) := f1(42)

C'est-à-dire que si le compilateur voit une affectation de result(T, E) à (T, E) , il devrait automatiquement forcer. Cela devrait permettre aux fonctions de passer de manière transparente à l'utilisation de types de résultats.

Combinateurs

Généralement, la gestion des erreurs sera beaucoup plus complexe que if err != nil { return _, err } . Cette proposition serait terriblement incomplète si c'était le seul cas qu'elle aidait.

Les types de résultats sont connus pour être en quelque sorte un couteau suisse de gestion des erreurs dans les langages fonctionnels en raison des "combinateurs" qu'ils prennent en charge. En réalité, ces combinateurs ne sont qu'un ensemble de méthodes qui nous permettent de transformer et de se comporter de manière sélective en fonction d'un type de résultat, généralement en "combinaison" avec une fermeture.

Then() : enchaîner les appels de fonction qui renvoient le même type de résultat

Disons que nous avons un code qui ressemble à ceci :

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
}

Avec un type de résultat, nous pouvons créer une fonction qui prend une fermeture en paramètre et n'appelle la fermeture que si le résultat a réussi, sinon court-circuiter et se retourner cela représente une erreur. Nous appellerons cette fonction Then (elle est décrite de cette façon dans le billet de blog sur la gestion des erreurs dans Go ), et connue sous le nom de and_then dans Rust). Avec une fonction comme celle-ci, nous pouvons réécrire l'exemple ci-dessus comme quelque chose comme :

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

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

ou en utilisant l'une des syntaxes proposées ci-dessus (je choisirai ! comme opérateur magique) :

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

Cela réduit les 12 lignes de code de notre exemple d'origine à trois, et nous laisse avec la valeur finale que nous recherchons réellement et le type de résultat lui-même disparu de l'image. Nous n'avons même jamais eu à donner un nom au type de résultat dans ce cas.

Maintenant accordé, la syntaxe de fermeture dans ce cas semble un peu lourde/JavaScript-ish. Il pourrait probablement bénéficier d'une syntaxe de fermeture plus légère. J'aimerais personnellement quelque chose comme ça :

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

... mais quelque chose comme ça mérite probablement une proposition séparée.

Map() et MapErr() : conversion entre les valeurs de réussite et d'erreur

Souvent, lorsque vous faites la danse if err != nil { return nil, err } vous voudrez réellement gérer l'erreur ou la transformer en un type différent. Quelque chose comme ça:

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

Dans ce cas, nous pouvons accomplir la même chose en utilisant MapErr() (j'utiliserai à nouveau la syntaxe ! pour renvoyer l'erreur) :

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

Map fait la même chose, en transformant simplement la valeur de réussite au lieu de l'erreur.

Et plus!

Il y a beaucoup plus de combinateurs que ceux que j'ai montrés ici, mais je pense que ce sont les plus intéressants. Pour une meilleure idée de ce à quoi ressemble un type de résultat complet, je suggère de consulter Rust :

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

Go2 LanguageChange NeedsInvestigation Proposal

Commentaire le plus utile

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

Je pense que ce ne serait pas la bonne direction pour Go. ()) })! , sérieusement ? L'objectif principal de Go devrait être la facilité d'apprentissage, la lisibilité et la facilité d'utilisation. Cela n'aide pas.

Tous les 79 commentaires

Les propositions de changement de langue ne sont actuellement pas prises en compte pendant le processus d'examen des propositions, car la langue Go 1.x est gelée (et il s'agit de Go2, comme vous l'avez noté). Je vous dis simplement de ne pas vous attendre à une décision à ce sujet de si tôt.

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

Je pense que ce ne serait pas la bonne direction pour Go. ()) })! , sérieusement ? L'objectif principal de Go devrait être la facilité d'apprentissage, la lisibilité et la facilité d'utilisation. Cela n'aide pas.

Comme quelqu'un l'a dit dans le fil de discussion reddit : préférerait certainement les types de somme et les génériques appropriés plutôt que de nouvelles fonctions intégrées spéciales.

Peut-être que je n'étais pas clair dans le post : je préférerais certainement qu'un type de résultat soit composé de types de somme et de génériques.

J'essayais de spécifier cela de manière à ce que l'ajout des deux (que je considère personnellement comme extrêmement improbable) ne soit pas un obstacle à l'ajout de cette fonctionnalité, et il pourrait être ajouté de telle manière que, lorsqu'il est disponible, cette fonctionnalité pourrait basculer sur eux (j'ai même donné un exemple de ce à quoi cela ressemblerait avec une syntaxe générique traditionnelle, et également liée au problème de type Go sum).

Je ne comprends pas le lien entre le type de résultat et les objectifs. Vos idées sur la propagation des erreurs et les combinateurs semblent fonctionner aussi bien avec la prise en charge actuelle de plusieurs paramètres de résultat.

@ianlancetaylor pouvez-vous donner un exemple de la façon de définir un combinateur qui fonctionne de manière générique sur les tuples de résultat actuels ? Si c'est possible, je serais curieux de le voir, mais je ne pense pas que ce soit le cas ( par ce post )

@tarcieri Ce message est très différent, en ce sens que error n'apparaît pas dans son utilisation suggérée de Result<A> . Ce problème, contrairement à l'article, semble suggérer result<int, error> , ce qui pour moi implique que les combinateurs proposés reconnaissent spécialement error . Mes excuses si j'ai mal compris.

L'intention n'est pas de coupler result à error , mais pour result de porter deux valeurs, similaire au type Result dans Rust ou Either en Haskell. Dans les deux langages, par convention, la deuxième valeur est généralement de type error (même si ce n'est pas obligatoire).

Ce problème, contrairement au message, semble suggérer un résultat

Le post suggère :

type Result<A> struct {
    // fields
}

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

...donc, au contraire, cet article se spécialise autour de error , alors que cette proposition accepte un type spécifié par l'utilisateur pour la deuxième valeur.

Certes, des choses comme result.Err() et result.MapErr() indiquent que cette valeur est toujours un error .

@tarcieri Quel est le problème avec une structure ? https://play.golang.org/p/mTqtaMbgIF

@griesemer, comme publication sur la gestion des erreurs dans Go , cette structure n'est pas générique. Vous devrez en définir un pour chaque combinaison de types de réussite et d'erreur que vous avez toujours voulu utiliser.

@tarcieri Compris. Mais si cela (la non-généricité, ou peut-être ne pas avoir de type de somme) est le problème ici, alors nous devrions plutôt nous attaquer à ces problèmes. La gestion des types de résultats uniquement ne fait qu'ajouter des cas particuliers.

Que Go ait ou non des génériques est orthogonal à l'utilité d'un type de résultat de première classe. Cela rendrait l'implémentation plus proche de quelque chose que vous implémentez vous-même, mais comme indiqué dans la proposition, permettre au compilateur de raisonner à ce sujet d'une manière de première classe lui permet, par exemple, d'avertir des résultats non consommés. Le fait d'avoir un seul type de résultat est également ce qui rend les combinateurs de la proposition composables.

@tarcieri La composition comme vous l'avez suggéré serait également possible avec un seul type de structure de résultat.

Je ne comprends pas pourquoi vous n'utiliseriez pas un type de structure intégré ou défini. Pourquoi avoir des méthodes et une syntaxe spécialisées pour vérifier les erreurs ? Go a déjà les moyens de faire tout cela. Il semble que cela ajoute simplement des fonctionnalités qui ne définissent pas le langage Go, elles définissent Rust. Ce serait une erreur de mettre en œuvre de tels changements.

Je ne comprends pas pourquoi vous n'utiliseriez pas un type de structure intégré ou défini. Pourquoi avoir des méthodes et une syntaxe spécialisées pour vérifier les erreurs ?

Pour me répéter encore : Parce qu'avoir un type de résultat générique nécessite... des génériques. Go n'a pas de génériques. À moins que Go n'obtienne des génériques, il a besoin d'une prise en charge des cas particuliers de la part du langage.

Peut-être que vous suggérez quelque chose comme ça?

type Result struct {
    value interface{}
    err error
}

Oui, cela "fonctionne"... au détriment de la sécurité de type. Maintenant, pour consommer un résultat, nous devons faire une assertion de type pour nous assurer que la valeur typée interface{} est celle que nous attendons. Sinon, c'est maintenant devenu une erreur d'exécution (par opposition à une erreur de compilation comme c'est le cas actuellement).

Ce serait une régression majeure par rapport à ce que Go a maintenant.

Pour que cette fonctionnalité soit réellement utile, elle doit être sécurisée. Le système de types de Go n'est pas assez expressif pour l'implémenter de manière sécurisée sans prise en charge des langages de cas particuliers. Il faudrait au minimum des génériques et, idéalement, des types de somme également.

Il semble que cela ajoute simplement des fonctionnalités qui ne définissent pas le langage Go [...]. Ce serait une erreur de mettre en œuvre de tels changements.

J'ai couvert autant dans la proposition originale:

« J'admets que cette approche comporte une certaine courbe d'apprentissage et, en tant que telle, peut avoir un impact négatif sur la clarté des programmes pour les personnes qui ne sont pas familiarisées avec les idiomes des combinateurs. Bien que personnellement j'aime les combinateurs pour la gestion des erreurs, je peux certainement voir à quel point culturellement ils peuvent être un mauvais choix pour Go."

J'ai l'impression d'avoir confirmé mes soupçons et qu'une fonctionnalité comme celle-ci à la fois n'est pas facilement comprise par les développeurs de Go et va à l'encontre de la nature orientée vers la simplicité du langage. Il s'agit d'exploiter des paradigmes de programmation que, très clairement, les développeurs de Go ne semblent pas comprendre ou ne veulent pas, et dans un tel cas, cela semble être un défaut.

ils définissent la rouille

Les types de résultats ne sont pas une fonctionnalité spécifique à Rust. On les retrouve dans de nombreuses langues fonctionnelles (par exemple Haskell de Either et OCaml de result ). Cela dit, les introduire dans Go ressemble à un pont trop loin.

Merci de partager vos idées, mais je pense que les exemples utilisés ci-dessus ne sont pas convaincants. Pour moi, A est meilleur que B :

UNE
```resp, err := doThing(a)
si erreur != nil {
retour nul, err
}
si resp, err = doAnotherThing(b, resp.foo()); erreur != néant {
retour erreur
}
si resp, err = FinishUp(c, resp.bar()); erreur != néant {
retour erreur
}


résultat := faire une chose(a).
Alors(func(resp) { faireAnotherThing(b, resp.foo()) }).
Alors(func(resp) { FinishUp(c, resp.bar()) })

si result.isError() {
renvoyer le résultat.Error()
}
```

  • A est plus lisible, à haute voix et mentalement.
  • A ne nécessite pas de formatage/emballage de ligne
  • Dans A, les conditions d'erreur terminent l'exécution, explicitement ; ne nécessitant aucune négation mentale. B n'est pas similaire.
  • En B, le mot-clé "Alors" n'indique pas de causalité conditionnelle. Le mot-clé "if" le fait, et il est déjà dans la langue.
  • En B, je ne veux pas ralentir ma branche d'exécution la plus probable en la compressant dans un lambda

Je ne pense pas que A soit plus lisible. En fait, les actions ne sont pas du tout perceptibles. Au lieu de cela, le premier coup d'œil révèle qu'un tas d'erreurs sont obtenues et renvoyées.

Si B avait été formaté de manière à ce que les corps de fermeture soient sur de nouvelles lignes, cela aurait été le format le plus lisible.

Aussi, le dernier point semble un peu idiot. Si les performances des appels de fonction sont si importantes, alors optez pour une syntaxe plus traditionnelle.

A De @as, je pense que le flux normal ne devrait pas être indenté.

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
}

Une observation intéressante de ce fil : l'exemple original que j'ai donné que les gens continuent de copier et coller contenait des erreurs (le premier if renvoyé nil, err en cas d'erreur, les deux suivants ne renvoient que err ). Ces erreurs n'étaient pas délibérées de ma part, mais je pense que c'est une étude de cas intéressante.

Bien que cette classe particulière d'erreurs soit du genre qui aurait été détectée par le compilateur Go, je pense qu'il est intéressant de noter qu'avec autant de passe-partout syntaxique, il devient très facile de passer outre de telles erreurs lors du copier-coller.

Cela n'améliore pas la proposition. C'est une hypothèse selon laquelle le fait de ne pas renvoyer plusieurs valeurs est le résultat d'une gestion explicite des erreurs. Vous auriez pu également faire les mêmes erreurs dans les fonctions, vous ne les auriez tout simplement pas vues en raison de leur encapsulation inutile.

Je ne suis pas d'accord, je pense que c'est un point fort de ce genre de proposition. Si tout ce qu'un programme fait est de retourner l'erreur et de ne pas la traiter, alors il gaspille la surcharge cognitive et le code et rend les choses moins lisibles. L'ajout d'une fonctionnalité comme celle-ci signifierait que (dans les projets qui choisissent de l'utiliser) le code qui traite les erreurs fait en fait quelque chose qui vaut la peine d'être compris.

Nous devrons accepter de ne pas être d'accord. Les jetons magiques de la proposition sont faciles à écrire, mais difficiles à comprendre. Ce n'est pas parce que nous l'avons raccourci que nous l'avons simplifié.

Rendre les choses moins lisibles est subjectif, alors voici mon opinion. Tout ce que je vois dans cette proposition est un code plus complexe et obscur avec des fonctions et des symboles magiques (qui sont très faciles à manquer). Et tout ce qu'ils font, c'est cacher un code très simple et facile à comprendre dans le cas A. Pour moi, ils n'ajoutent aucune valeur, ne raccourcissent pas le code là où cela compte ou ne simplifient pas les choses. Je ne vois aucune valeur à les traiter au niveau de la langue.

Le seul problème que la proposition résout, que j'ai pu voir clairement, est le passe-partout dans la gestion des erreurs. Si c'est la seule raison, alors ça n'en vaut pas la peine pour moi. L'argument sur le passe-partout syntaxique va en fait contre la proposition. C'est beaucoup plus complexe à cet égard - tous ces symboles et crochets magiques qui sont si faciles à manquer. L'exemple A a un passe-partout mais il ne provoque pas d'erreurs logiques. Dans ce contexte, il n'y a rien à gagner de cette proposition, encore une fois, ce qui la rend peu utile.

Laissons les fonctionnalités de Rust à Rust.

Pour clarifier, je n'aime pas ajouter le suffixe ! comme raccourci, mais j'aime l'idée de proposer une syntaxe simple qui simplifie

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

Même si cette syntaxe est un mot-clé au lieu d'un symbole spécial. C'est ma plus grande plainte au sujet de la langue (encore plus grande que Generics personnellement), et je pense que l'éparpillement de ce modèle à travers le code le rend plus difficile à lire et bruyant.

J'aimerais aussi voir quelque chose qui permette le genre de chaînage que @tarcieri propose, car je le trouve plus lisible dans le code. Je pense que la complexité à laquelle @creker fait allusion est compensée par le meilleur rapport signal/bruit dans le code.

Je ne comprends pas parfaitement comment cette proposition atteindrait ses objectifs déclarés.

  1. Réduire la gestion des erreurs standard : la proposition contient un code Go hypothétique :

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

    Je ne sais pas vraiment comment func(resp) { expr } est censé fonctionner sans des changements plus importants dans la façon dont fonctionnent les littéraux de fonction. Je pense que le code résultant ressemblerait plutôt à ceci:

    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()
    }
    

    Dans le code Go réaliste, il est également assez courant que les expressions intermédiaires soient plus longues que cela et doivent être placées sur leurs propres lignes. Cela se produit naturellement dans le vrai code Go aujourd'hui ; selon cette proposition, ce serait :

    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()
    }
    

    Quoi qu'il en soit, cela me semble correct , mais pas génial, tout comme le vrai code Go au-dessus dans la proposition. Son combinateur « Alors » est essentiellement le contraire de « retour ». (Si vous êtes familier avec les monades, cela ne sera pas une surprise.) Cela supprime l'exigence d'écrire une instruction 'if', mais introduit l'exigence d'écrire une fonction. Dans l'ensemble, ce n'est pas sensiblement meilleur ou pire ; c'est la même logique passe-partout avec une nouvelle orthographe.

  2. Permet au compilateur de raisonner sur les résultats : si cette fonctionnalité est souhaitable (et je n'exprime pas d'opinion à ce sujet ici), je ne vois pas en quoi cette proposition la rend sensiblement plus ou moins faisable. Ils me paraissent orthogonaux.

  3. Combinateurs de gestion d'erreurs : cet objectif est certainement atteint par la proposition, mais il n'est pas tout à fait clair que cela vaudrait le coût des changements nécessaires pour y parvenir, dans le contexte du langage Go tel qu'il existe aujourd'hui. (Je pense que c'est le principal point de discorde dans la discussion jusqu'à présent.)

Dans la plupart des Go bien écrits, ce type de passe-partout de gestion des erreurs constitue une petite fraction du code. Il s'agissait d'un pourcentage de lignes à un chiffre dans mon bref aperçu de certaines bases de code Go que je considère comme bien écrites. Oui, c'est parfois approprié, mais c'est souvent le signe qu'une refonte s'impose. En particulier, le simple renvoi d'une erreur sans ajouter le moindre contexte se produit plus souvent qu'il ne le devrait aujourd'hui. Cela pourrait être appelé un « anti-idiome ». Il y a une discussion à avoir sur ce que, le cas échéant, Go devrait ou pourrait faire pour décourager cet anti-idiome, que ce soit dans la conception du langage, ou dans les bibliothèques, ou dans l'outillage, ou purement socialement, ou dans une combinaison de ces . Je serais également intéressé d'avoir cette discussion, que cette proposition soit adoptée ou non. En fait, rendre cet anti-idiome plus facile à exprimer, comme je pense que c'est le but de cette proposition, pourrait créer de mauvaises incitations.

À l'heure actuelle, cette proposition est traitée en grande partie comme une question de goût. Ce qui le rendrait plus convaincant à mon avis, ce serait des preuves démontrant que son adoption réduirait le nombre total de bogues. Une bonne première étape pourrait consister à convertir une partie représentative du corpus Go pour démontrer que certaines sortes de bogues sont impossibles ou peu susceptibles d'être exprimés dans le nouveau style - que x bogues par ligne dans le code Go réel dans la nature seraient corrigés en utilisant le nouveau style. (Il semble beaucoup plus difficile de démontrer que le nouveau style ne compense aucune amélioration en rendant d'autres types de bogues plus probables. Là, nous pourrions avoir à nous contenter d'arguments abstraits sur la lisibilité et la complexité, comme dans le mauvais vieux temps avant le Go corpus a pris de l'importance.)

Avec des preuves à l'appui comme celle-là en main, on pourrait faire un argument plus solide.

Le simple fait de renvoyer une erreur sans ajouter de contexte se produit plus souvent qu'il ne le devrait aujourd'hui. Cela pourrait être appelé un « anti-idiome ».

J'aimerais faire écho à ce sentiment. Cette

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

ne doit pas être simplifiée, elle doit être découragée, en faveur par exemple

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

@peterbourgon

mon plus gros problème avec ce n'est pas que l'erreur est renvoyée aveuglément. C'est le fait que l'action : foo(x) ; n'est-ce pas visible, et à mon humble avis, le tout est un peu moins lisible que les solutions «fonctionnelles» alternatives, où l'action elle-même est un simple retour sur une nouvelle ligne.

même si l'affectation et l'action sont séparées de l'instruction if elle-même, l'instruction résultante mettrait toujours l'accent sur le résultat plutôt que sur l'action. C'est parfaitement valable, surtout si le résultat est la partie importante. Mais si vous avez un tas d'instructions, où chacune obtient un tuple (résultat, erreur), vérifie les erreurs/retours, puis procède à une autre action tout en obtenant un nouveau tuple, les résultats eux-mêmes ne sont évidemment pas les personnages principaux du parcelle.

@urandom Je pense que le résultat est une paire de (val, erreur) donc je pense que les vérifications que l'erreur/les retours sont également les personnages principaux de l'intrigue.

Qu'en est-il d'un mot réservé (quelque chose comme reterr ) pour éviter tous les if err != nil { return err } ?

Donc ça

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
}

Deviendrait:

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

reterr vérifierait essentiellement les valeurs de retour de la fonction appelée et renverrait si l'une d'entre elles est une erreur et n'est pas nulle (et renverrait nil dans toute valeur de retour sans erreur).

Sonne de plus en plus comme #18721

@tarcieri Utilisez simplement une partie du package reflect . Je peux simuler quelque chose comme votre proposition.
Mais je pense que ça ne vaut pas la peine de le faire.

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, il y a deux problèmes de réflexion qui en font une solution inadaptée à ce problème particulier, bien qu'il puisse sembler "résoudre" le problème en surface :

  1. Pas de sécurité de type : avec réflexion, nous ne pouvons pas déterminer au moment de la compilation si la fermeture est correctement typée. Au lieu de cela, notre programme compilera quels que soient les types, et nous rencontrerons un crash d'exécution s'ils ne correspondent pas.
  2. Surcharge de performances énorme : l'approche que vous proposez n'est pas très éloignée de celle proposée par go-linq . Ils prétendent que l'utilisation de la réflexion à cette fin est "5x-10x plus lente". Imaginez maintenant cette quantité de frais généraux sur chaque site d'appel.

Pour moi, l'un ou l'autre de ces problèmes est un énorme pas en arrière par rapport à ce que Go a déjà, et en tandem, ils sont un non-starter complet.

J'aime Go et la façon dont il gère les erreurs. Cependant, peut-être que cela pourrait être plus simple. Voici quelques-unes de mes idées concernant la gestion des erreurs dans Go.

La façon dont c'est maintenant:

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
}

UNE:

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

RÉ:

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))
}

Excusez mon anglais et je ne sais pas si de tels changements sont possibles et s'ils entraîneront une surcharge de performances.

Si vous aimez l'une de ces approches, veuillez l'aimer en suivant les règles suivantes :

A =
B =
C = ❤️
D =

Et si vous n'aimez pas l'idée dans son ensemble ))

De cette façon, nous pouvons avoir des statistiques et éviter les commentaires inutiles comme "+1"

Elaborer sur mes "propositions"...

// 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)
     }
}

Encore une fois, je ne sais pas si quelque chose comme ça est possible))

Voici une autre option folle, rendez le mot error un peu plus magique. Il devient utilisable sur le côté gauche d'une affectation (ou d'une déclaration courte) et fonctionne un peu comme une fonction magique :

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

Plus précisément, le comportement de error() est le suivant :

  1. Il est traité comme s'il était de type error aux fins de l'affectation.
  2. Si nil est assigné, rien ne se passe.
  3. Si une nil non error et à laquelle est affectée la valeur affectée à error() .

Si vous souhaitez appliquer une mutation à l'erreur, vous pouvez faire :

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

Dans ce cas, la fermeture est appliquée à la valeur attribuée avant le retour de la fonction.

C'est un peu moche, en grande partie à cause du gonflement syntaxique des fermetures. La bibliothèque standard pourrait bien résoudre ce problème, avec par exemple error(errors.Wrapper("foo")) qui générera la bonne fermeture de wrapper pour vous.

Comme alternative, si la syntaxe nullaire error() est trop susceptible d'être manquée, je suggérerais error(return) comme alternative ; l'utilisation du mot-clé réduit le risque de mauvaise interprétation. Cependant, cela ne s'étend pas bien au boîtier de fermeture.

Tous ceux qui ont écrit Go ont été confrontés à la prolifération malheureuse de la gestion des erreurs passe-partout qui détourne l'attention de l'objectif principal de leur code. C'est pourquoi Rob Pike a abordé le sujet en 2015 . Comme le souligne Martin Kühl , la proposition de Rob pour simplifier la gestion des erreurs :

nous oblige à implémenter des monades artisanales ponctuelles pour chaque interface pour laquelle nous voulons gérer les erreurs, ce qui, je pense, est toujours aussi verbeux et répétitif

C'est pourquoi il y a encore tant d'engagement sur ce sujet aujourd'hui.

Idéalement, nous pouvons trouver une solution qui :

  1. Réduit la gestion des erreurs répétitives et maximise la concentration sur l'intention principale du chemin de code.
  2. Encourage une gestion appropriée des erreurs, y compris l'encapsulation des erreurs lors de leur propagation.
  3. Adhère aux principes de conception Go de clarté et de simplicité.
  4. Est applicable dans la gamme la plus large possible de situations de gestion des erreurs.

Je propose l'introduction d'un nouveau mot clé catch: qui fonctionne comme suit :

Au lieu du formulaire actuel :

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

on écrirait :

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

qui se comporterait exactement de la même manière que le code de formulaire actuel ci-dessus. Plus précisément, la fonction et les affectations à gauche de catch: sont exécutées en premier. Ensuite, si et seulement si exactement l'un des arguments de retour est de type error ET que cette valeur est non nulle, le catch: agit comme une instruction return avec les valeurs à la droite. S'il y a zéro ou plus d'un type error renvoyé par doThing() , c'est une erreur de syntaxe d'utiliser catch: . Si la valeur d'erreur renvoyée par doThing() est nil , alors tout ce qui va de catch: à la fin de l'instruction est ignoré et non évalué.

Pour donner un exemple plus complexe du récent article de blog de Nemanja Mijailovic intitulé, Error handling patterns in 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
}

Cela devient à la place :

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
}

Avantages :

  1. Un passe-partout supplémentaire proche du minimum pour la gestion des erreurs.
  2. Améliore la concentration sur l'intention principale du code avec un minimum d'erreurs de gestion des bagages sur le côté gauche de l'instruction et la gestion des erreurs localisées sur le côté droit.
  3. Fonctionne dans de nombreuses situations différentes donnant au programmeur une flexibilité dans le cas de plusieurs valeurs de retour (par exemple, si vous souhaitez renvoyer un indicateur du nombre d'éléments qui ont réussi en plus de l'erreur).
  4. La syntaxe est simple et serait facilement comprise et adoptée par les utilisateurs de Go, qu'ils soient nouveaux ou anciens.
  5. Réussit en partie à encourager une gestion correcte des erreurs en rendant le code d'erreur plus succinct. Peut rendre le code d'erreur légèrement moins susceptible d'être copié-collé et ainsi réduire l'introduction d'erreurs courantes de copier-coller.

Désavantages:

  1. Cette approche ne réussit pas pleinement à encourager une gestion appropriée des erreurs car elle ne fait rien pour promouvoir les erreurs d'encapsulation avant de les propager. Dans mon monde idéal, cette nouvelle syntaxe aurait exigé que l'erreur renvoyée par catch: soit une nouvelle erreur ou une erreur encapsulée, mais pas identique à l'erreur renvoyée par la fonction à gauche du catch: . Go a été décrit comme "opiniâtre" et une telle sévérité sur la gestion des erreurs dans un souci de clarté et de fiabilité aurait correspondu à cela. Je manquais de créativité pour intégrer cet objectif, cependant.
  2. Certains peuvent soutenir que tout cela est du sucre syntaxique et n'est pas nécessaire dans la langue. Un contre-argument pourrait être que la gestion actuelle des erreurs dans Go est la graisse trans syntaxique, et cette proposition l'élimine simplement. Pour être largement adopté, un langage de programmation doit être agréable à utiliser. Go largement réussit à cela, mais le passe-partout de gestion des erreurs est une exception particulièrement abondante.
  3. Est-ce que nous "attrapons" l'erreur de la fonction que nous appelons, ou sommes-nous en train de "jeter" une erreur à celui qui nous a appelés ? Est-il approprié d'avoir un catch: sans lancer explicite ? Le mot réservé ne doit pas nécessairement être catch: . D'autres peuvent avoir de meilleures idées. Il pourrait même s'agir d'un opérateur au lieu d'un mot réservé.

Tous ceux qui ont écrit Go ont été confrontés à la prolifération malheureuse de la gestion des erreurs passe-partout qui détourne l'attention de l'objectif principal de leur code.

Ce n'est pas vrai. Je programme beaucoup en Go et je n'ai aucun problème avec la gestion des erreurs standard. L'écriture de code de gestion des erreurs consomme une fraction de temps microscopique pour développer un projet que je le remarque à peine et à mon humble avis, cela ne justifie aucun changement de langage.

Tous ceux qui ont écrit Go ont été confrontés à la prolifération malheureuse de la gestion des erreurs passe-partout qui détourne l'attention de l'objectif principal de leur code.

Ce n'est pas vrai. Je programme beaucoup en Go et je n'ai aucun problème avec la gestion des erreurs standard. L'écriture de code de gestion des erreurs consomme une fraction de temps microscopique pour développer un projet que je le remarque à peine et à mon humble avis, cela ne justifie aucun changement de langage.

Je n'ai rien dit sur le temps que prend l'écriture du code de gestion des erreurs. J'ai seulement dit que cela détourne l'attention de l'objectif principal du code. J'aurais peut-être dû dire "Tous ceux qui ont lu Go ont rencontré la malheureuse prolifération de la gestion des erreurs...".

Donc, @cznic , je suppose que la question pour vous est de savoir si vous avez lu du code Go qui, selon vous,

Personne n'aime mes propositions 😅
Quoi qu'il en soit, nous devrions avoir une certaine syntaxe et voter pour le meilleur (un système de sondage) et inclure le lien ici ou dans le fichier readme

J'aurais peut-être dû dire "Tous ceux qui ont lu Go ont rencontré la malheureuse prolifération de la gestion des erreurs...".

Ce n'est pas vrai. Je préfère l'explicitation et la localisation appropriée de l'état de l'art actuel de la gestion des erreurs. La proposition, comme toutes les autres que j'ai jamais vues, rend le code à mon humble avis moins lisible et pire à maintenir.

Donc, @cznic , je suppose que la question pour vous est de savoir si vous avez lu du code Go qui, selon vous,

Non. D'après mon expérience, Go est un langage de programmation exceptionnellement bien lisible. La moitié de ce crédit va à gofmt, bien sûr.

Ma propre expérience est que cela commence vraiment à traîner lorsque vous avez un tas d'instructions dépendantes, dont chacune peut générer une erreur, la gestion des erreurs s'additionne et vieillit rapidement. Ce qui pourrait être 5 lignes de code devient 20.

@cznic
D'après mon expérience, le fait d'avoir autant d'erreurs de traitement standard rend le code beaucoup moins lisible. Parce que la gestion des erreurs elle-même est pour la plupart identique (sans aucun emballage d'erreur qui pourrait se produire), elle produit une sorte d'effet de clôture, où si vous parcourez rapidement un morceau de code, vous finissez par voir une masse de gestion d'erreurs. Ainsi, le plus gros problème, le code réel, la partie la plus importante du programme, est caché derrière cette illusion d'optique, ce qui rend très difficile de voir en quoi consiste un morceau de code.

La gestion des erreurs ne devrait pas être la partie principale d'un code. Malheureusement, cela finit souvent par être exactement cela.
Il y a une raison pour laquelle la composition dans d'autres langues est si populaire.

Parce que la gestion des erreurs elle-même est pratiquement identique (sans aucune erreur
emballage qui pourrait se produire), il produit une sorte d'effet de clôture, où si
vous parcourez rapidement un morceau de code, vous finissez généralement par voir une masse
de gestion des erreurs.

C'est une position très subjective. C'est comme prétendre que si les déclarations
rendre le code illisible, ou que les accolades de style K&R rendent les choses illisibles.

De mon point de vue, le caractère explicite de la gestion des erreurs de go s'estompe rapidement
dans l'arrière-plan de la familiarité jusqu'à ce que vous remarquiez le motif brisé ;
quelque chose que l'œil humain fait très bien ; gestion des erreurs manquantes,
variables d'erreur affectées à _, etc.

C'est un fardeau à taper, ne vous y trompez pas. Mais Go n'optimise pas pour le
auteur de code, il optimise explicitement pour le lecteur.

Le mar. 16 mai 2017 à 17h45, Viktor Kojouharov < [email protected]

a écrit:

@cznic https://github.com/cznic
D'après mon expérience, le fait d'avoir autant d'erreurs dans la gestion du passe-partout rend le code
beaucoup moins lisible. Parce que la gestion des erreurs elle-même est pratiquement identique
(sans aucune erreur d'emballage qui pourrait se produire), il produit une sorte de clôture
effet, où si vous parcourez rapidement un morceau de code, vous terminez la plupart du temps
en voyant une masse de gestion des erreurs. Ainsi, le plus gros problème, le réel
le code, la partie la plus importante du programme, est caché derrière cette optique
illusion, ce qui rend d'autant plus difficile de voir ce qu'est un morceau de
le code est sur.

La gestion des erreurs ne devrait pas être la partie principale d'un code. Malheureusement, assez
souvent, cela finit par être exactement cela.

-
Vous recevez ceci parce que vous êtes abonné à ce fil.
Répondez directement à cet e-mail, consultez-le sur GitHub
https://github.com/golang/go/issues/19991#issuecomment-301702623 , ou couper le son
le fil
https://github.com/notifications/unsubscribe-auth/AAAcA4ydpBFiapYBOBUyUjg6du5Dnjs5ks5r6VQjgaJpZM4M-dud
.

si vous parcourez rapidement un morceau de code, vous finissez généralement par voir une masse
de gestion des erreurs.

C'est une position très subjective.

Très subjectif mais largement partagé.

Comme Rob lui-même l'a dit,

Un point de discussion commun parmi les programmeurs Go, en particulier ceux qui découvrent le langage, est de savoir comment gérer les erreurs. La conversation tourne souvent à la complainte au nombre de fois où la séquence

if err != nil {
    return err
}

révéler.

En toute honnêteté, Rob a poursuivi en disant que cette perception de la gestion des erreurs Go est "malheureuse, trompeuse et facilement corrigée". Cependant, il passe la majeure partie de cet article à expliquer sa méthode recommandée pour corriger la perception. Malheureusement, la prescription de Rob est problématique en soi comme l'a si bien expliqué Martin Kühl. En plus de la critique de Martin, la suggestion de Rob réduit également la localité que @cznic dit qu'il valorise dans la gestion des erreurs Go.

Peut-être que la question est de savoir si nous avions la capacité de remplacer

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

avec quelque chose de similaire à :

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

L'utiliseriez-vous ou resteriez-vous avec la version à quatre lignes ? Quelles que soient vos préférences personnelles, pensez-vous qu'une alternative comme celle-ci serait largement adoptée par la communauté Go et deviendrait idiomatique ? Compte tenu de la subjectivité de tout argument selon lequel la version plus courte affecte négativement la lisibilité, mon expérience avec les programmeurs indique qu'ils graviteraient fortement vers la version à une seule ligne.

Vrai discours : go 1 est fixe, et ne changera pas, surtout de cette manière fondamentale.

Il est inutile de proposer une sorte de type d'option jusqu'à ce que Go 2 en implémente pour de type templates. À ce moment-là, tout change.

Le 16 mai 2017, à 23h46, Billy Hinners [email protected] a écrit :

si vous parcourez rapidement un morceau de code, vous finissez généralement par voir une masse
de gestion des erreurs.

C'est une position très subjective.

Très subjectif mais largement partagé.

Comme Rob lui-même l'a dit,

Un point de discussion commun parmi les programmeurs Go, en particulier ceux qui découvrent le langage, est de savoir comment gérer les erreurs. La conversation tourne souvent à la complainte au nombre de fois où la séquence

si erreur != nil {
retour erreur
}
révéler.

En toute honnêteté, Rob a poursuivi en disant que cette perception de la gestion des erreurs Go est "malheureuse, trompeuse et facilement corrigée". Cependant, il passe la majeure partie de cet article à expliquer sa méthode recommandée pour corriger la perception. Malheureusement, la prescription de Rob est problématique en soi comme l'a si bien expliqué Martin Kühl. En plus de la critique de Martin, la suggestion de Rob réduit également la localité que @cznic dit qu'il valorise dans la gestion des erreurs Go.

Peut-être que la question est de savoir si nous avions la capacité de remplacer

res, err := doThing()
si erreur != nil {
retour nul, err
}
avec quelque chose de similaire à :

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

L'utiliseriez-vous ou resteriez-vous avec la version à quatre lignes ? Quelles que soient vos préférences personnelles, pensez-vous qu'une alternative comme celle-ci serait largement adoptée par la communauté Go et deviendrait idiomatique ? Compte tenu de la subjectivité de tout argument selon lequel la version plus courte affecte négativement la lisibilité, mon expérience avec les programmeurs indique qu'ils graviteraient fortement vers la version à une seule ligne.

-
Vous recevez ceci parce que vous avez commenté.
Répondez directement à cet e-mail, affichez-le sur GitHub ou coupez le fil de discussion.

Il est inutile de proposer une sorte de type d'option jusqu'à ce que Go 2 en implémente pour de type templates. À ce moment-là, tout change.

J'ai supposé que nous parlions de Go 2 comme suggéré par le titre de ce fil et dans la pleine conviction que "Go 2" n'est pas un euphémisme pour "jamais". En fait, étant donné que le Go 1 est fixe, nous devrions probablement consacrer une bien plus grande partie de nos discussions sur le Go au Go 2.

Cela dit, je pense que tous ceux qui se plaignent de la verbosité de Go
la gestion des erreurs manque le point fondamental que le but de l'erreur
la gestion dans Go n'est _pas_ pour rendre le cas de non-erreur aussi bref et discret
que possible. L'objectif de la stratégie de gestion des erreurs de Go est plutôt de forcer
l'auteur du code de considérer, à tout moment, ce qui se passe lorsque le
fonction échoue et, surtout, comment nettoyer, annuler et récupérer
avant de revenir vers l'appelant.

Toutes les stratégies pour cacher l'erreur de manipulation de plaque de chaudière me semblent être
en ignorant cela.

Le mar. 16 mai 2017, 23:51 Dave Cheney [email protected] a écrit :

Real talk : go 1 est fixe, et ne changera pas, surtout dans ce
manière fondamentale.

Il est inutile de proposer une sorte de type d'option jusqu'à ce que Go 2 implémente
certains pour le type de modèles. À ce moment-là, tout change.

Le 16 mai 2017, à 23h46, Billy Hinners [email protected] a écrit :

si vous parcourez rapidement un morceau de code, vous finissez généralement par voir un
Masse
de gestion des erreurs.

C'est une position très subjective.

Très subjectif mais largement partagé.

Comme Rob lui-même l'a dit,

Un point de discussion commun parmi les programmeurs Go, en particulier ceux qui découvrent
la langue, c'est comment gérer les erreurs. La conversation se transforme souvent en
se lamenter au nombre de fois que la séquence

si erreur != nil {
retour erreur
}

révéler.

En toute honnêteté, Rob a poursuivi en disant que cette perception de la gestion des erreurs Go est
"malheureux, trompeur et facilement corrigé." Cependant, il passe la majeure partie de ce
article https://blog.golang.org/errors-are-values expliquant son
méthode recommandée pour corriger la perception. Malheureusement, Rob
la prescription est problématique en soi comme expliqué
https://www.innoq.com/en/blog/golang-errors-monads/ si bien par Martin
Kühl. En plus de la critique de Martin, la suggestion de Rob réduit également la
localité que @cznic https://github.com/cznic dit qu'il valorise dans Go
la gestion des erreurs.

Peut-être que la question est de savoir si nous avions la capacité de remplacer

res, err := doThing()
si erreur != nil {
retour nul, err
}

avec quelque chose de similaire à :

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

L'utiliseriez-vous ou resteriez-vous avec la version à quatre lignes ?
Quelles que soient vos préférences personnelles, pensez-vous qu'une alternative comme
cela serait largement adopté par la communauté Go et deviendrait idiomatique ?
Étant donné la subjectivité de tout argument selon lequel la version plus courte
affecte la lisibilité, mon expérience avec les programmeurs dit qu'ils
graviter fortement vers la version monoligne.

-
Vous recevez ceci parce que vous avez commenté.
Répondez directement à cet e-mail, consultez-le sur GitHub
https://github.com/golang/go/issues/19991#issuecomment-301787215 , ou couper le son
le fil
https://github.com/notifications/unsubscribe-auth/AAAcAwATgoJwL5WV-0nffLjLB9L86GYOks5r6ai3gaJpZM4M-dud
.

L'objectif de la stratégie de gestion des erreurs de Go est plutôt de forcer
l'auteur du code de considérer, à tout moment, ce qui se passe lorsque le
fonction échoue et, surtout, comment nettoyer, annuler et récupérer
avant de revenir vers l'appelant.

Eh bien, alors Go n'a pas atteint cet objectif. Par défaut, Go vous permet d'ignorer les erreurs renvoyées et dans de nombreux cas, vous ne le sauriez même pas jusqu'à ce que quelque chose quelque part ne fonctionne pas comme il se doit. Au contraire, des exceptions très détestées dans la communauté Go (ce n'est qu'un exemple pour prouver le point) vous obligent à les considérer car sinon l'application plantera. Cela nous amène souvent à des problèmes de capture et d'ignorance, mais c'est la faute du programmeur.

Fondamentalement, la gestion des erreurs dans Go est opt-in. Il s'agit plus d'une convention parlée selon laquelle chaque erreur doit être traitée. L'objectif serait atteint si cela vous obligeait réellement à gérer les erreurs. Par exemple, avec des erreurs ou des avertissements au moment de la compilation.

Dans cet esprit, cacher la plaque de chaudière ne ferait de mal à personne. La convention orale tiendrait toujours et les programmeurs choisiraient toujours la gestion des erreurs comme c'est le cas actuellement.

le but de la stratégie de gestion des erreurs de Go est de forcer
l'auteur du code de considérer, à tout moment, ce qui se passe lorsque le
fonction échoue et, surtout, comment nettoyer, annuler et récupérer
avant de revenir vers l'appelant.

C'est un objectif incontestablement noble. C'est un objectif, cependant, qui doit être mis en balance avec la lisibilité du flux principal et de l'intention du code.

En tant que programmeur Go, je peux vous dire que je ne trouve pas la verbosité de
La gestion des erreurs de Go nuit à sa lisibilité. je ne vois rien
à troquer, car je ne ressens aucune gêne _lecture_ du code écrit par d'autres
Allez les programmeurs.

Le mercredi 17 mai 2017 à 00h10, Billy Hinners [email protected]
a écrit:

le but de la stratégie de gestion des erreurs de Go est de forcer
l'auteur du code de considérer, à tout moment, ce qui se passe lorsque le
fonction échoue et, surtout, comment nettoyer, annuler et récupérer
avant de revenir vers l'appelant.

C'est un objectif incontestablement noble. C'est un objectif, cependant, qui doit être
contrebalancé par la lisibilité du flux principal et l'intention du code.

-
Vous recevez ceci parce que vous avez commenté.
Répondez directement à cet e-mail, consultez-le sur GitHub
https://github.com/golang/go/issues/19991#issuecomment-301794653 , ou couper le son
le fil
https://github.com/notifications/unsubscribe-auth/AAAcAzfcu5hq86xxVj85qfOquVawHh44ks5r6a5zgaJpZM4M-dud
.

@davecheney , Alors que je suis d'accord avec vous pour dire que la gestion des erreurs doit être explicite et non reportée à plus tard (ce que vous pouvez bien sûr faire avec _), il y a aussi la stratégie de "bouillonner" les erreurs pour les traiter en une seule fonction, pour ajouter des informations supplémentaires ou supprimer (avant de l'envoyer au client). Mon problème personnel est que je dois écrire les mêmes 4 lignes de code encore et encore

Par exemple:

getNewToken(id int64) (Jeton, erreur) {

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

}
Je ne gère pas l'erreur ici, je la renvoie simplement. et quand je lis ce genre de code, je dois sauter l'erreur "manipulation", et difficile de trouver le but principal du code

et le code ci-dessus pourrait facilement être remplacé par quelque chose comme ça :

getNewToken(id int64) (Jeton, erreur) {

user := &User{ID:id}

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

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

return token, nil

}
Un code comme celui-ci est un code plus lisible et moins inutile (IMHO). Et l'erreur peut être et doit être gérée dans la fonction où cette fonction est utilisée.

En tant que programmeur Go, je peux vous dire que je ne trouve pas que la verbosité de la gestion des erreurs de Go nuise à sa lisibilité.

Je suis d'accord.

Sur une note sans rapport :

Il me semble aussi qu'un type "résultat" est un peu trop spécifique d'une proposition ; peut-être que les types ne sont en réalité que des types énumérés à deux variantes. S'il y avait un concept d'énumérations, un résultat ou un package d'options pourrait être créé à partir d'un arbre et expérimenté avant de s'engager à l'ajouter au langage et sans ajouter beaucoup de syntaxe ou de méthodes supplémentaires qui ne peuvent pas vraiment être réutilisées et ne sont que bonnes pour les types de résultats. Je ne sais pas si les énumérations seraient utiles ou non dans Go, mais si vous pouvez argumenter le cas le plus général, cela rendra probablement également votre cas plus fort pour le type de résultat plus spécifique (je suppose, peut-être que je me trompe).

func getNewToken(id int64) (jeton, erreur) {
user := &User{ID:id}

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

return token.New(u)

}

Semble équivalent.

Le mercredi 17 mai 2017 à 00h34, Kiura [email protected] a écrit :

@davecheney https://github.com/davecheney , Considérant que je suis d'accord avec vous
que la gestion des erreurs doit être explicite et non reportée à plus tard (ce qui
vous, bien sûr, pouvez faire avec _), il y a aussi la stratégie de "bouillonner"
erreurs pour les traiter dans une fonction, pour ajouter des informations supplémentaires ou
remove (avant de l'envoyer au client). Mon problème personnel est que j'ai
écrire les mêmes 4 lignes de code encore et encore

Par exemple:

getNewToken(id int64) (Jeton, erreur) {

user := &User{ID:id}

u, err := user.Get();
si erreur != nil {
retourner le jeton{}, err
}

jeton, err := jeton.Nouveau(u);
si erreur != nil {
retourner le jeton{}, err
}
jeton de retour, nul

}
Je ne gère pas l'erreur ici, je la renvoie simplement. et quand je lis
ce genre de code, je dois sauter l'erreur "manipulation", et difficile à trouver
le but principal du code

et le code ci-dessus pourrait facilement être remplacé par quelque chose comme ça :

getNewToken(id int64) (Jeton, erreur) {

user := &User{ID:id}

u, err := throw user.Get(); // jeter devrait également envelopper l'erreur

jeton, err := lancer le jeton.Nouveau(u);

jeton de retour, nul

}
Un code comme celui-ci est un code plus lisible et moins inutile (IMHO). Et le
l'erreur peut être et doit être gérée dans la fonction où cette fonction est
utilisé.

-
Vous recevez ceci parce que vous avez été mentionné.
Répondez directement à cet e-mail, consultez-le sur GitHub
https://github.com/golang/go/issues/19991#issuecomment-301802010 , ou couper le son
le fil
https://github.com/notifications/unsubscribe-auth/AAAcA9sIRXX7RSdDcUOidpe-qLTR7unNks5r6bP3gaJpZM4M-dud
.

Il me semble aussi qu'un type "résultat" est un peu trop spécifique d'une proposition ; peut-être que les types ne sont en réalité que des types énumérés à deux variantes. S'il y avait un concept d'énumérations, un résultat ou un package d'options pourrait être créé à partir d'un arbre et expérimenté avant de s'engager à l'ajouter au langage et sans ajouter beaucoup de syntaxe ou de méthodes supplémentaires qui ne peuvent pas vraiment être réutilisées et ne sont que bonnes pour les types de résultats. Je ne sais pas si les énumérations seraient utiles ou non dans Go, mais si vous pouvez argumenter le cas le plus général, cela rendra probablement également votre cas plus fort pour le type de résultat plus spécifique (je suppose, peut-être que je me trompe).

Comme indiqué dans la proposition originale, un type de résultat serait idéalement implémenté en tant que type somme (par exemple, enums ala Rust), et il existe une proposition ouverte pour les ajouter au langage.

Cependant, les types de somme seuls ne sont pas suffisants pour implémenter une bibliothèque de types de résultats réutilisable sans prise en charge de langage supplémentaire. Ils nécessitent également des génériques.

Cette proposition explorait l'idée d'implémenter un type de résultat qui ne dépend pas de génériques, mais repose à la place sur l'aide de cas particuliers du compilateur.

J'ajouterai juste que maintenant après l'avoir posté, je conviendrais que la meilleure façon de poursuivre (le cas échéant) serait avec la prise en charge des génériques au niveau du langage.

@davecheney , En effet, dans ce cas presque aucune différence, mais que se passe-t-il si vous avez 3-4 appels en fonction qui renvoient une erreur ?

PS Je ne suis pas contre la façon dont la structure Go1 gère les erreurs, je pense juste que cela pourrait être mieux.

Comme indiqué dans la proposition originale, un type de résultat serait idéalement implémenté en tant que type somme (par exemple, enums ala Rust), et il existe une proposition ouverte pour les ajouter au langage.

Désolé, j'aurais dû être plus clair : je disais que cette déclaration :

Je pense qu'un type de résultat Go n'en a pas besoin non plus et peut simplement tirer parti de la magie du compilateur de cas particulier.

me semble être une mauvaise idée.

Cependant, les types de somme seuls ne sont pas suffisants pour implémenter une bibliothèque de types de résultats réutilisable sans prise en charge de langage supplémentaire. Ils nécessitent également des génériques.

Cette proposition explorait l'idée d'implémenter un type de résultat qui ne dépend pas de génériques, mais repose à la place sur l'aide de cas particuliers du compilateur.

J'ajouterai juste que maintenant après l'avoir posté, je conviendrais que la meilleure façon de poursuivre (le cas échéant) serait avec la prise en charge des génériques au niveau du langage.

Oui, assez juste; Je suis d'accord avec ta dernière affirmation alors. Si nous devons quand même attendre Go 2, nous pourrions aussi bien résoudre le problème plus général d'abord (en supposant qu'il s'agisse réellement d'un problème) :)

De plus, Rob Pike a écrit un article sur la gestion des erreurs, comme mentionné ci-dessus. Alors que cette approche semble "réparer" le problème, elle en introduit un autre : plus de surcharge de code avec les interfaces.

Je pense qu'il est important de ne pas confondre « traitement explicite des erreurs » avec « traitement détaillé des erreurs ». Go veut forcer l'utilisateur à considérer la gestion des erreurs à chaque étape plutôt que de la déléguer. Pour chaque fonction que vous appelez et qui peut générer une erreur, vous devez décider si vous souhaitez ou non gérer l'erreur et comment. Parfois, cela signifie que vous ignorez l'erreur, parfois cela signifie que vous réessayez, souvent cela signifie que vous la transmettez simplement à l'appelant pour qu'il s'en occupe.

L'article de Rob est génial et devrait vraiment faire partie d'Effective Go 2, mais c'est une stratégie qui ne peut vous mener que jusqu'à présent. Surtout lorsqu'il s'agit d'appelés hétérogènes, vous avez beaucoup de gestion des erreurs à gérer

Je ne pense pas qu'il soit déraisonnable d'envisager du sucre syntaxique ou une autre facilité pour aider à la gestion des erreurs. Je pense qu'il est important que cela ne sape pas les principes fondamentaux de la gestion des erreurs Go. Par exemple, établir un gestionnaire d'erreurs au niveau des fonctions qui gère toutes les erreurs qui se produisent serait une mauvaise chose ; cela signifie que nous permettons au programmeur de faire ce que fait généralement la gestion des exceptions : déplacer la prise en compte des erreurs d'un problème au niveau de l'instruction vers un problème au niveau du bloc ou de la fonction. C'est définitivement contre la philosophie.

@billyh En ce qui concerne l'article « Modèles de gestion des erreurs dans Go », il existe d'autres solutions :

@egonelbre
Ces solutions ne conviennent que si vous effectuez le même type d'opération à plusieurs reprises. Ce n'est généralement pas le cas. Ainsi, cela ne peut presque jamais être appliqué dans la pratique.

@urandom s'il vous plaît montrez un exemple réaliste alors?

Bien sûr, je peux prendre un exemple plus compliqué :

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

Je comprends que ceux-ci ne sont pas applicables partout, mais sans une liste appropriée d'exemples que nous voulons améliorer, il n'y a aucun moyen d'avoir une discussion décente.

@egonelbre

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

Avis de non-responsabilité : je n'ai pas utilisé de juju et je n'ai pas lu le code. C'est juste un produit de « production » que je connais par cœur. Je suis raisonnablement sûr qu'un tel type de gestion des erreurs (où les erreurs sont vérifiées entre des opérations indépendantes) est répandu dans le monde du go, et je doute fortement qu'il y ait quelqu'un qui n'y soit pas tombé.

@urandom je suis d'accord. Le principal problème avec la discussion sans code du monde réel est que les gens se souviennent de "l'essentiel" du problème, pas du problème réel - ce qui conduit souvent à un énoncé du problème trop simplifié. _PS : je me suis souvenu d'un bel exemple au go ._

Par exemple, à partir de ces exemples du monde réel, nous pouvons voir qu'il y a plusieurs autres choses qui doivent être prises en compte :

  • bons messages d'erreur
  • récupération / chemins alternatifs basés sur la valeur d'erreur
  • solutions de repli
  • exécution au mieux avec des erreurs
  • journalisation des cas heureux
  • journal des échecs
  • traçage des pannes
  • plusieurs erreurs renvoyées
  • _bien sûr, certains d'entre eux seront utilisés ensemble_
  • ... probablement certains que j'ai ratés ...

Pas seulement le chemin « heureux » et « l'échec ». Je ne dis pas que ceux-ci ne peuvent pas être résolus, juste qu'ils doivent être cartographiés et discutés.

@egonelbre voici un autre exemple du Golang Weekly de cette semaine, dans l'article de Mario Zupan intitulé "Writing a Static Blog Generator in 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
}

Remarque : je n'implique aucune critique du code de Mario. En fait, j'ai bien aimé son article.
Malheureusement, des exemples comme celui-ci sont trop courants dans la source Go. Le code Go gravite vers ce modèle de voie ferrée d'une ligne d'intérêt suivie de trois lignes de passe-partout identiques ou presque identiques répétées encore et encore. Combiner l'affectation et le conditionnel lorsque cela est possible, comme le fait Mario, aide un peu.

Je ne suis pas sûr qu'un langage de programmation ait été conçu dans le but principal de minimiser les lignes de code, mais a) le rapport entre code significatif et passe-partout pourrait être l'une (des nombreuses) mesures valides de la qualité d'un langage de programmation, et b) parce qu'une grande partie de la programmation implique la gestion des erreurs, ce modèle imprègne le code Go et rend donc ce cas particulier de l'excès de passe-partout mérite une rationalisation.

Si nous pouvons identifier une bonne alternative, je pense qu'elle sera rapidement adoptée et rendra Go encore plus agréable à lire, à écrire et à entretenir.

Rebecca Skinner (@cercerilla) a partagé un excellent article sur les lacunes de Go en matière de gestion des erreurs, ainsi qu'une analyse de l'utilisation des monades comme solution dans son jeu de diapositives Monadic Error Handling in Go . J'ai particulièrement aimé ses conclusions à la fin.

Merci à @davecheney d' avoir fait référence au deck de Rebecca dans son article, Simplicity Debt Redux qui m'a permis de le retrouver. (Merci également à Dave d'avoir fondé mon optimisme rose pour Go 2 avec les réalités les plus dures.)

Le code Go gravite vers ce modèle de voie ferrée d'une ligne d'intérêt suivie de trois lignes de passe-partout identiques ou presque identiques répétées encore et encore.

Chaque instruction de contrôle de flux de contrôle est importante. Les lignes de gestion des erreurs sont d'une importance cruciale du point de vue de l'exactitude.

le rapport entre code significatif et passe-partout pourrait être l'une (des nombreuses) mesures valides de la qualité d'un langage de programmation

Si quelqu'un considère que les instructions de gestion des erreurs ne sont pas significatives, alors bonne chance avec le codage et j'espère rester à l'écart des résultats.

Pour aborder l'un des points abordés dans Simplicity Debt Redux de @davecheney (que j'ai couvert, mais je pense qu'il peine de le répéter):

La question suivante est la suivante : cette forme monadique deviendrait-elle le seul moyen de gérer les erreurs ?

Pour que quelque chose comme celui-ci devienne la manière "unique" de gérer les erreurs, il faudrait qu'il s'agisse d'un changement radical effectué dans l'ensemble de la bibliothèque standard et dans chaque projet compatible "Go2". Je pense que ce n'est pas sage : la débâcle de Python2/3 montre à quel point de tels schismes peuvent nuire aux écosystèmes linguistiques.

Comme mentionné dans cette proposition, si un type de résultat pouvait automatiquement contraindre à la forme de tuple équivalente, vous pourriez avoir votre gâteau et le manger aussi en termes d'une bibliothèque standard Go2 hypothétique adoptant cette approche à tous les niveaux tout en maintenant la rétrocompatibilité avec le code existant . Cela permettrait à ceux qui sont intéressés d'en profiter, mais les bibliothèques qui souhaitent toujours travailler sur Go1 fonctionneront simplement immédiatement. Les auteurs de bibliothèques pouvaient avoir le choix : écrire des bibliothèques qui fonctionnent à la fois sur Go1 et Go2 en utilisant l'ancien style, ou sur Go2 uniquement en utilisant le style monadic.

L'« ancienne méthode » et la « nouvelle méthode » de gestion des erreurs pourraient être compatibles au point que les utilisateurs du langage n'auraient même pas à y penser et pourraient continuer à faire les choses à l'« ancienne méthode » s'ils le souhaitaient. Bien que cela manque d'une certaine pureté conceptuelle, je pense que c'est beaucoup moins important que de permettre au code existant de continuer à fonctionner sans modification et de permettre également aux gens de développer des bibliothèques qui fonctionnent avec toutes les versions du langage, pas seulement la dernière.

Cela semble déroutant et donne des indications peu claires aux nouveaux venus dans Go 2.0, pour continuer à prendre en charge à la fois le modèle d'interface d'erreur et un nouveau type peut-être monadique.

Ce sont les freins : soit laisser le langage figé tel quel, soit faire évoluer le langage, ajoutant une complexité accessoire et reléguant les anciennes façons de faire les choses à des verrues héritées. Je pense vraiment que ce sont les deux seules options car l'ajout d'une nouvelle fonctionnalité qui remplace une ancienne, que l'ancienne fonctionnalité soit obsolète mais compatible ou à la porte sous la forme d'un changement radical, est quelque chose que je pense que les utilisateurs de la langue devra apprendre quoi qu'il en soit.

Je ne pense pas qu'il soit possible de changer la langue, mais les nouveaux arrivants évitent d'apprendre à la fois "l'ancienne façon" et la "nouvelle façon" de faire les choses, même si Go2 devait hypothétiquement adopter cela d'emblée. Vous vous retrouveriez toujours avec un schisme Go1 et Go2, et les nouveaux arrivants se demanderont quelles sont les différences et finiront inévitablement par devoir apprendre "Go1" de toute façon.

Je pense que la rétrocompatibilité est utile à la fois pour l'enseignement de la langue et la compatibilité du code : tous les matériels existants d'enseignement du Go continueront d'être valides, même si la syntaxe est obsolète. Il ne sera pas nécessaire de parcourir chaque partie du matériel d'enseignement de Go et d'invalider l'ancienne syntaxe : le matériel d'enseignement pourrait, à loisir, ajouter un avis indiquant qu'il existe une nouvelle syntaxe.

Je comprends que "Il y a plus d'une façon de le faire" va généralement à l'encontre de la philosophie Go de simplicité et de minimalisme, mais c'est le prix à payer pour ajouter de nouvelles fonctionnalités linguistiques. De nouvelles fonctionnalités linguistiques rendront, de par leur nature, les anciennes approches obsolètes.

Je suis certainement prêt à admettre qu'il pourrait y avoir un moyen de résoudre le même problème de base d'une manière plus naturelle pour Gophers, cependant, et pas un changement aussi discordant par rapport à l'approche existante.

Une dernière chose à considérer : alors que Go a fait un travail exemplaire pour garder la langue facile à apprendre, ce n'est pas le seul obstacle impliqué dans l'intégration des gens à une langue. Je pense qu'il est sûr de dire qu'il y a un certain nombre de personnes qui regardent la verbosité de la gestion des erreurs de Go et sont rebutées par cela, certaines au point qu'elles refusent d'adopter la langue.

Je pense qu'il vaut la peine de se demander si les améliorations apportées à la langue pourraient attirer des personnes qui en sont actuellement rebutées, et comment cela s'équilibre avec le fait de rendre la langue plus difficile à apprendre.

Cependant, faire quelque chose comme la gestion des erreurs monadique va à l'encontre de la philosophie de Go qui consiste à vous faire réfléchir aux erreurs. La gestion des erreurs monadic et la gestion des exceptions de style Java sont assez proches en sémantique (bien que différente en syntaxe). Go a adopté une philosophie délibérément différente consistant à s'attendre à ce que le programmeur gère explicitement chaque erreur, plutôt que d'ajouter uniquement du code de gestion des erreurs lorsque vous y pensez. En fait, l'idiome return nil, err n'est pas à proprement parler optimal car vous pouvez probablement ajouter un contexte utile supplémentaire.

Je pense que toute tentative de gestion des erreurs de Go doit garder cela à l'esprit et ne pas permettre d'éviter facilement de penser aux erreurs.

@alercah, je dois à peu près supplier de ne pas être d'

Faire quelque chose comme la gestion des erreurs monadique va à l'encontre de la philosophie de Go qui consiste à vous faire réfléchir aux erreurs

Venant de Rust, je pense que Rust (ou plutôt, le compilateur Rust) me fait plus penser aux erreurs qu'à Go. Rust a un attribut #[must_use] sur son type Result ce qui signifie que les résultats inutilisés génèrent un avertissement du compilateur. Ce n'est pas le cas dans Go (Rebecca Skinner en parle dans son exposé) : le compilateur Go n'avertit pas, par exemple, les valeurs error gérées.

Le système de type Rust s'assure que chaque cas d'erreur est traité dans votre code, et sinon, il s'agit d'une erreur de type ou, au mieux, d'un avertissement.

La gestion des erreurs monadic et la gestion des exceptions de style Java sont assez proches en sémantique (bien que différente en syntaxe).

Laissez-moi expliquer pourquoi ce n'est pas vrai :

Stratégie de propagation d'erreur

  • Go : valeur de retour, propagée explicitement
  • Java : saut non local, propagé implicitement
  • Rust : valeur de retour, propagée explicitement

Types d'erreur

  • Go : une valeur de retour par fonction, généralement 2 tuples de (success, error)
  • Java : exceptions vérifiées constituées de nombreux types d'exceptions, représentant l'ensemble de toutes les exceptions potentiellement levées à partir d'une méthode. Également des exceptions non vérifiées qui ne sont pas déclarées et peuvent se produire n'importe où à tout moment.
  • Rust : une valeur de retour par fonction, généralement le type de Result par exemple Result<Success, Error>

Dans l'ensemble, j'ai l'impression que Go est beaucoup plus proche de Rust que de Java en ce qui concerne la gestion des erreurs : les erreurs dans Go et Rust ne sont que des valeurs, ce ne sont pas des exceptions. Vous devez vous inscrire explicitement à la propagation. Vous devez convertir les erreurs d'un type différent en celui qu'une fonction donnée renvoie, par exemple par encapsulation. Ils représentent tous les deux en fin de compte une paire valeur de réussite/erreur, utilisant simplement différentes caractéristiques du système de types (tuples par rapport aux types de somme génériques).

Il existe quelques exceptions où Rust fournit des abstractions qui peuvent être utilisées au choix caisse par caisse pour effectuer une gestion des erreurs implicites (ou plutôt, une conversion d' erreur explicite , vous devez toujours propager manuellement l'erreur). Par exemple, le trait From peut être utilisé pour convertir automatiquement les erreurs d'un type à un autre. Personnellement, je pense qu'être capable de définir une politique qui est complètement étendue à un package particulier qui vous permet de convertir automatiquement les erreurs d'un type explicite à un autre est un avantage, et non un inconvénient. Le système de traits de Rust vous permet uniquement de définir From pour les types dans votre propre caisse, empêchant toute sorte d'action effrayante à distance.

C'est cependant bien en dehors de la portée de cette proposition et implique plusieurs fonctionnalités de langage que Go ne fait pas fonctionner en tandem, donc je ne pense pas qu'il y ait une sorte de pente glissante où Go est "à risque" de prendre en charge ces types de conversions implicites , du moins pas jusqu'à ce que Go ajoute des génériques et des traits/typeclasses.

Pour jeter mes deux cents sur cette question. Je pense que ce type de fonctionnalité serait très utile pour les entreprises (comme mon propre employeur) où des applications uniques communiquent avec un grand nombre de sources de données subsidiaires et composent les résultats de manière simple.

Voici un échantillon de données représentatif d'un flux de code que nous aurions

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
}

Je comprends une grande partie du recul que Go est conçu pour forcer un utilisateur à penser aux erreurs à chaque point, mais dans les bases de code où la grande majorité des fonctions renvoient T, err , cela conduit à une

De plus, la grande majorité de cette logique de gestion des erreurs est identique, et paradoxalement, la quantité de gestion d'erreurs explicite dans nos bases de code rend difficile la recherche de code où le cas exceptionnel est réellement intéressant car il y a un peu d'aiguille dans la botte de foin ' phénomènes en jeu.

Je peux certainement voir pourquoi cette proposition en particulier n'est peut-être pas la solution, mais je pense qu'il doit y avoir un moyen de réduire ce passe-partout.

Quelques autres pensées vaines :

Le ? Rust est une bonne syntaxe. Pour Go, étant donné l'importance du contexte d'erreur, cependant, je suggérerais peut-être la variation suivante :

  • ? traîne fonctionne comme Rust, modifié pour Go. Concrètement : il ne peut être utilisé que dans une fonction dont la dernière valeur de retour est de type error , et il doit apparaître immédiatement après un appel de fonction dont la dernière valeur de retour est également de type error (note : on pourrait autoriser tout type qui implémente également error , mais exiger error empêche le problème d'interface nil de se poser, ce qui est un bon bonus). L'effet est que si la valeur d'erreur n'est pas nulle, la fonction dans laquelle ? apparaît revient de la fonction, définissant le dernier paramètre sur la valeur d'erreur. Pour les fonctions utilisant des valeurs de retour nommées, il peut soit renvoyer des zéros pour les autres valeurs, soit toute valeur actuellement stockée ; pour les fonctions qui ne le font pas, les autres valeurs de retour sont toujours zéro.
  • La fin de .?("opening %s", file) fonctionne comme ci-dessus, sauf qu'au lieu de renvoyer l'erreur non modifiée, elle est transmise via une fonction qui compose les erreurs ; grosso modo, .?(str, vals...) muter l'erreur comme fmt.Errorf(str + ": %s", vals..., err)
  • Il devrait éventuellement exister une version, soit une variante de la syntaxe .? soit une autre, couvrant le cas où un package souhaite exporter un type d'erreur distinct.

Lié à #19412 (types de somme) et #21161 (gestion des erreurs) et #15292 (génériques).

En rapport:

"Draft Designs" pour les nouvelles fonctionnalités de gestion des erreurs :
https://go.googlesource.com/proposal/+/master/design/go2draft.md

Commentaires sur la conception des erreurs :
https://github.com/golang/go/wiki/Go2ErrorHandlingFeedback

J'aime la suggestion de @alercah pour résoudre cette seule caractéristique ennuyeuse de go-lang dont parle @LegoRemix , au lieu de créer un type de retour séparé.

Je suggérerais simplement de suivre encore plus la RFC de Rust pour éviter de deviner des valeurs nulles et d'introduire l'expression catch pour permettre à la fonction de spécifier explicitement ce qui est renvoyé au cas où le corps principal renvoie une erreur :

Donc ça:

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
}

Devient ce code 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)
}

Et exiger que la fonction qui utilise l'opérateur ? doit également définir catch

@bradfitz @peterbourgon @SamWhited Peut-être devrait-il y avoir un autre problème pour cela ?

@sheerun Votre ? et votre instruction catch ressemblent beaucoup à l'opérateur check et à l'instruction handle dans le nouveau projet de conception de gestion des erreurs (https : //go.googlesource.com/proposal/+/master/design/go2draft.md).

C'est encore mieux, pour les curieux, voici à quoi ressemblerait mon code avec check et 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
}

La seule chose que je changerais serait de me débarrasser des handle implicites et d'exiger qu'ils soient définis si la vérification est utilisée. Cela empêchera les développeurs d'utiliser paresseusement check et de réfléchir davantage à la manière de gérer ou d'envelopper les erreurs. Le retour implicite devrait être une caractéristique distincte et pourrait être utilisé comme proposé précédemment :

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
}

En tant qu'auteur de cette proposition, je pense qu'il convient de noter qu'elle est effectivement invalidée par #15292 et fonctionne comme https://go.googlesource.com/proposal/+/master/design/go2draft-contracts.md , comme cette proposition a été écrit en supposant que les installations de programmation génériques ne sont pas disponibles. En tant que tel, il suggère une nouvelle syntaxe pour permettre le polymorphisme de type pour le cas particulier de result() , et si cela peut être évité en utilisant par exemple des contrats, je ne pense plus que cette proposition ait de sens.

Puisqu'il semble qu'au moins l'un d'entre eux soit susceptible de se retrouver dans Go 2, je me demande si cette proposition particulière devrait être fermée et si les gens sont toujours intéressés par un type de résultat comme alternative à handle , qu'il soit réécrit en supposant, par exemple, que des contrats sont disponibles.

(Notez que je n'ai probablement pas le temps de faire ce travail, mais si quelqu'un d'autre est intéressé à voir cette idée avancer, allez-y)

@sheerun l'endroit où déposer vos commentaires et idées sur la gestion des erreurs de Go 2 est cette page wiki :
https://github.com/golang/go/wiki/Go2ErrorHandlingFeedback

et/ou cette liste complète des _Exigences à prendre en compte pour la gestion des erreurs Go 2 :_
https://gist.github.com/networkimprov/961c9caa2631ad3b95413f7d44a2c98a

Cette page vous a été utile?
0 / 5 - 0 notes