Go: proposition : Go 2 : simplifier la gestion des erreurs avec || suffixe d'erreur

Créé le 25 juil. 2017  ·  519Commentaires  ·  Source: golang/go

Il y a eu de nombreuses propositions sur la façon de simplifier la gestion des erreurs dans Go, toutes basées sur la plainte générale selon laquelle trop de code Go contient les lignes

if err != nil {
    return err
}

Je ne suis pas sûr qu'il y ait ici un problème à résoudre, mais comme il revient sans cesse, je vais proposer cette idée.

L'un des principaux problèmes avec la plupart des suggestions pour simplifier la gestion des erreurs est qu'elles ne simplifient que deux façons de gérer les erreurs, mais il y en a en fait trois :

  1. ignorer l'erreur
  2. renvoyer l'erreur non modifiée
  3. renvoyer l'erreur avec des informations contextuelles supplémentaires

Il est déjà facile (peut-être trop facile) d'ignorer l'erreur (voir #20803). De nombreuses propositions existantes pour la gestion des erreurs facilitent le renvoi de l'erreur non modifiée (par exemple, #16225, #18721, #21146, #21155). Peu facilitent le retour de l'erreur avec des informations supplémentaires.

Cette proposition est vaguement basée sur les langages shell Perl et Bourne, sources fertiles d'idées de langage. Nous introduisons un nouveau type d'instruction, similaire à une instruction d'expression : une expression d'appel suivie de || . La grammaire est :

PrimaryExpr Arguments "||" Expression

De même, nous introduisons un nouveau type d'instruction d'affectation :

ExpressionList assign_op PrimaryExpr Arguments "||" Expression

Bien que la grammaire accepte n'importe quel type après le || dans le cas de non-affectation, le seul type autorisé est le type prédéclaré error . L'expression qui suit || doit avoir un type attribuable à error . Ce n'est peut-être pas un type booléen, pas même un type booléen nommé assignable à error . (Cette dernière restriction est nécessaire pour rendre cette proposition rétrocompatible avec la langue existante.)

Ces nouveaux types d'instructions ne sont autorisés que dans le corps d'une fonction qui a au moins un paramètre de résultat, et le type du dernier paramètre de résultat doit être le type prédéclaré error . La fonction appelée doit de même avoir au moins un paramètre de résultat, et le type du dernier paramètre de résultat doit être le type prédéclaré error .

Lors de l'exécution de ces instructions, l'expression d'appel est évaluée comme d'habitude. S'il s'agit d'une instruction d'affectation, les résultats de l'appel sont affectés aux opérandes de gauche comme d'habitude. Ensuite, le résultat du dernier appel, qui comme décrit ci-dessus doit être de type error , est comparé à nil . Si le résultat du dernier appel n'est pas nil , une instruction return est implicitement exécutée. Si la fonction appelante a plusieurs résultats, la valeur zéro est renvoyée pour tous les résultats sauf le dernier. L'expression suivant le || est renvoyée comme dernier résultat. Comme décrit ci-dessus, le dernier résultat de la fonction appelante doit avoir le type error , et l'expression doit pouvoir être affectée au type error .

Dans le cas de non-affectation, l'expression est évaluée dans une portée dans laquelle une nouvelle variable err est introduite et définie sur la valeur du dernier résultat de l'appel de fonction. Cela permet à l'expression de se référer facilement à l'erreur renvoyée par l'appel. Dans le cas de l'affectation, l'expression est évaluée dans la portée des résultats de l'appel, et peut donc faire directement référence à l'erreur.

C'est la proposition complète.

Par exemple, la fonction os.Chdir est actuellement

func Chdir(dir string) error {
    if e := syscall.Chdir(dir); e != nil {
        return &PathError{"chdir", dir, e}
    }
    return nil
}

Selon cette proposition, il pourrait être écrit comme

func Chdir(dir string) error {
    syscall.Chdir(dir) || &PathError{"chdir", dir, err}
    return nil
}

J'écris cette proposition principalement pour encourager les personnes qui souhaitent simplifier la gestion des erreurs Go à réfléchir à des moyens de faciliter l'encapsulation du contexte autour des erreurs, et pas seulement de renvoyer l'erreur non modifiée.

FrozenDueToAge Go2 LanguageChange NeedsInvestigation Proposal error-handling

Commentaire le plus utile

Une idée simple, avec prise en charge de la décoration d'erreur, mais nécessitant un changement de langue plus drastique (évidemment pas pour go1.10) est l'introduction d'un nouveau mot-clé check .

Il aurait deux formes : check A et check A, B .

A et B doivent tous deux être error . La deuxième forme ne serait utilisée que lors d'une erreur de décoration ; les personnes qui n'ont pas besoin ou ne souhaitent pas décorer leurs erreurs utiliseront la forme la plus simple.

1ère forme (cocher A)

check A évalue A . Si nil , il ne fait rien. Sinon nil , check agit comme un return {<zero>}*, A .

Exemples

  • Si une fonction renvoie juste une erreur, elle peut être utilisée en ligne avec check , donc
err := UpdateDB()    // signature: func UpdateDb() error
if err != nil {
    return err
}

devient

check UpdateDB()
  • Pour une fonction avec plusieurs valeurs de retour, vous devrez attribuer, comme nous le faisons maintenant.
a, b, err := Foo()    // signature: func Foo() (string, string, error)
if err != nil {
    return "", "", err
}

// use a and b

devient

a, b, err := Foo()
check err

// use a and b

2ème forme (cocher A, B)

check A, B évalue A . Si nil , il ne fait rien. Sinon nil , check agit comme un return {<zero>}*, B .

C'est pour les besoins de décoration d'erreur. Nous vérifions toujours A , mais c'est B qui est utilisé dans le return implicite.

Exemple

a, err := Bar()    // signature: func Bar() (string, error)
if err != nil {
    return "", &BarError{"Bar", err}
}

devient

a, err := Foo()
check err, &BarError{"Bar", err}

Remarques

C'est une erreur de compilation pour

  • utilisez l'instruction check sur des choses qui ne s'évaluent pas à error
  • utiliser check dans une fonction avec des valeurs de retour qui ne sont pas sous la forme { type }*, error

La forme à deux expressions check A, B est court-circuitée. B n'est pas évalué si A vaut nil .

Remarques sur la praticité

Il existe un support pour les erreurs de décoration, mais vous ne payez pour la syntaxe check A, B maladroite que lorsque vous avez réellement besoin de décorer les erreurs.

Pour le if err != nil { return nil, nil, err } partout (ce qui est très courant), check err est aussi bref que possible sans sacrifier la clarté (voir la note sur la syntaxe ci-dessous).

Remarques sur la syntaxe

Je dirais que ce type de syntaxe ( check .. , au début de la ligne, similaire à un return ) est un bon moyen d'éliminer les erreurs de vérification sans masquer la perturbation du flux de contrôle qui les retours implicites introduisent.

Un inconvénient des idées comme le <do-stuff> || <handle-err> et le <do-stuff> catch <handle-err> ci-dessus, ou le a, b = foo()? proposé dans un autre fil, est qu'ils cachent la modification du flux de contrôle d'une manière qui rend le flux plus difficile suivre; le premier avec || <handle-err> machine ajouté à la fin d'une ligne par ailleurs simple, le second avec un petit symbole qui peut apparaître partout, y compris au milieu et à la fin d'une ligne de code simple, éventuellement plusieurs fois.

Une instruction check sera toujours de niveau supérieur dans le bloc actuel, ayant la même importance que d'autres instructions qui modifient le flux de contrôle (par exemple, une première return ).

Tous les 519 commentaires

    syscall.Chdir(dir) || &PathError{"chdir", dir, e}

D'où vient e ? Faute de frappe?

Ou vouliez-vous dire :

func Chdir(dir string) (e error) {
    syscall.Chdir(dir) || &PathError{"chdir", dir, e}
    return nil
}

(C'est-à-dire, la vérification implicite err != nil affecte-t-elle d'abord l'erreur au paramètre de résultat, qui peut être nommé pour le modifier à nouveau avant le retour implicite ?)

Soupir, foiré mon propre exemple. Maintenant corrigé : le e devrait être err . La proposition place err dans la portée pour contenir la valeur d'erreur de l'appel de fonction lorsqu'elle n'est pas dans une instruction d'affectation.

Bien que je ne sois pas sûr d'être d'accord avec l'idée ou la syntaxe, je dois vous remercier d'avoir prêté attention à l'ajout de contexte aux erreurs avant de les renvoyer.

Cela pourrait intéresser @davecheney , qui a écrit https://github.com/pkg/errors.

Que se passe-t-il dans ce code :

if foo, err := thing.Nope() || &PathError{"chdir", dir, err}; err == nil || ignoreError {
}

(Mes excuses si ce n'est même pas possible sans la partie || &PathError{"chdir", dir, e} ; j'essaie d'exprimer que cela ressemble à un remplacement déroutant du comportement existant et que les retours implicites sont... sournois ?)

@ object88 Je serais bien de ne pas autoriser ce nouveau cas dans un SimpleStmt tel qu'il est utilisé dans les déclarations if et for et switch . Ce serait probablement mieux, même si cela compliquerait légèrement la grammaire.

Mais si nous ne le faisons pas, alors ce qui se passe est que si thing.Nope() renvoie une erreur non nulle, alors la fonction appelante renvoie &PathError{"chdir", dir, err} (où err est le variable définie par l'appel à thing.Nope() ). Si thing.Nope() renvoie une erreur nil , alors nous savons avec certitude que err == nil est vrai dans la condition de l'instruction if , et donc le corps du si l'instruction est exécutée. La variable ignoreError n'est jamais lue. Il n'y a pas d'ambiguïté ou de dépassement du comportement existant ici ; le traitement de || introduit ici n'est accepté que lorsque l'expression après || n'est pas une valeur booléenne, ce qui signifie qu'elle ne compilerait pas actuellement.

Je suis d'accord que les retours implicites sont sournois.

Ouais, mon exemple est assez pauvre. Mais ne pas autoriser l'opération à l'intérieur d'un if , for , ou switch résoudrait beaucoup de confusion potentielle.

Étant donné que la barre de considération est généralement quelque chose qui est difficile à faire dans la langue telle quelle, j'ai décidé de voir à quel point cette variante était difficile à coder dans la langue. Pas beaucoup plus dur que les autres : https://play.golang.org/p/9B3Sr7kj39

Je n'aime vraiment pas toutes ces propositions pour rendre un type de valeur et une position dans les arguments de retour spéciaux. Celui-ci est à certains égards en fait pire car il fait également de err un nom spécial dans ce contexte spécifique.

Bien que je convienne certainement que les gens (y compris moi !) devraient être plus fatigués de renvoyer des erreurs sans contexte supplémentaire.

Lorsqu'il existe d'autres valeurs de retour, comme

if err != nil {
  return 0, nil, "", Struct{}, wrap(err)
}

cela peut certainement devenir ennuyeux à lire. J'ai un peu aimé la suggestion de @nigeltao pour return ..., err dans https://github.com/golang/go/issues/19642#issuecomment -288559297

Si je comprends bien, pour construire l'arbre syntaxique, l'analyseur aurait besoin de connaître les types de variables pour faire la distinction entre

boolean := BoolFunc() || BoolExpr

et

err := FuncReturningError() || Expr

Ça n'a pas l'air bien.

moins est plus...

Lorsque le retour ExpressionList contient deux éléments ou plus, comment cela fonctionne-t-il ?

BTW, je veux plutôt paniquer.

err := doSomeThing()
panicIf(err)

err = doAnotherThing()
panicIf(err)

@ianlancetaylor Dans l'exemple de votre proposition, err n'est toujours pas déclaré explicitement et est inséré comme « magique » (langue prédéfinie), n'est-ce pas ?

Ou sera-ce quelque chose comme

func Chdir(dir string) error {
    return (err := syscall.Chdir(dir)) || &PathError{"chdir", dir, err}
}

?

D'un autre côté (puisqu'il est déjà marqué comme un "changement de langue"...)
Introduisez un nouvel opérateur (!! ou ??) qui fait le raccourci en cas d'erreur != nil (ou n'importe quel nullable ?)

func DirCh(dir string) (string, error) {
    return dir, (err := syscall.Chdir(dir)) !! &PathError{"chdir", dir, err}
}

Désolé si c'est trop loin :)

Je suis d'accord que la gestion des erreurs dans Go peut être répétitive. La répétition ne me dérange pas, mais trop d'entre elles affectent la lisibilité. Il y a une raison pour laquelle la « complexité cyclomatique » (que vous y croyiez ou non) utilise des flux de contrôle comme mesure de la complexité. L'instruction "if" ajoute du bruit supplémentaire.

Cependant, la syntaxe proposée "||" n'est pas très intuitif à lire, d'autant plus que le symbole est communément appelé opérateur OU. De plus, comment gérez-vous les fonctions qui renvoient plusieurs valeurs et erreurs ?

Je lance juste quelques idées ici. Et si au lieu d'utiliser l'erreur en sortie, nous utilisions l'erreur en entrée ? Exemple : https://play.golang.org/p/rtfoCIMGAb

Merci pour tous vos commentaires.

@opennota Bon point. Cela pourrait encore fonctionner mais je suis d'accord que cet aspect est gênant.

@mattn Je ne pense pas qu'il y ait de retour ExpressionList, donc je ne suis pas sûr de ce que vous demandez. Si la fonction appelante a plusieurs résultats, tous sauf le dernier sont renvoyés comme la valeur zéro du type.

@mattn panicif n'aborde pas l'un des éléments clés de cette proposition, ce qui est un moyen simple de renvoyer une erreur avec un contexte supplémentaire. Et, bien sûr, on peut écrire panicif aujourd'hui assez facilement.

@tandr Oui, err est défini comme par magie, ce qui est assez horrible. Une autre possibilité serait de permettre à l'expression d'erreur d'utiliser error pour faire référence à l'erreur, ce qui est horrible d'une manière différente.

@tandr Nous pourrions utiliser un opérateur différent mais je ne vois pas de grand avantage. Cela ne semble pas rendre le résultat plus lisible.

@henryas Je pense que la proposition explique comment elle gère plusieurs résultats.

@henryas Merci pour l'exemple. Ce que je n'aime pas dans ce genre d'approche, c'est qu'elle fait de la gestion des erreurs l'aspect le plus important du code. Je veux que la gestion des erreurs soit présente et visible, mais je ne veux pas que ce soit la première chose sur la ligne. C'est vrai aujourd'hui, avec l'idiome if err != nil et l'indentation du code de gestion des erreurs, et cela devrait rester vrai si de nouvelles fonctionnalités sont ajoutées pour la gestion des erreurs.

Merci encore.

@ianlancetaylor Je ne sais pas si vous avez consulté mon lien sur le terrain de jeu, mais il y avait un "panicIf" qui vous permettait d'ajouter un contexte supplémentaire.

Je reproduis ici une version un peu simplifiée :

func panicIf(err error, transforms ...func(error) error) {
  if err == nil {
    return
  }
  for _, transform := range transforms {
    err = transform(err)
  }
  panic(err)
}

Par coïncidence, je viens de donner un discours éclair à la GopherCon où j'ai utilisé (mais pas sérieusement proposé) un peu de syntaxe pour aider à visualiser le code de gestion des erreurs. L'idée est de mettre ce code de côté pour qu'il ne gêne pas le flux principal, sans recourir à des tours de magie pour raccourcir le code. Le résultat ressemble

func DirCh(dir string) (string, error) {
    dir := syscall.Chdir(dir)        =: err; if err != nil { return "", err }
}

=: est le nouveau bit de syntaxe, un miroir de := qui s'assigne dans l'autre sens. Évidemment, nous aurions également besoin de quelque chose pour = , ce qui est certes problématique. Mais l'idée générale est de permettre au lecteur de mieux comprendre le chemin heureux, sans perdre d'informations.

D'un autre côté, la méthode actuelle de gestion des erreurs a certains avantages en ce sens qu'elle sert de rappel flagrant que vous pouvez faire trop de choses dans une seule fonction, et qu'une refactorisation peut être en retard.

J'aime beaucoup la syntaxe proposée par @billyh ici

func Chdir(dir string) error {
    e := syscall.Chdir(dir) catch: &PathError{"chdir", dir, e}
    return nil
}

ou exemple plus complexe en utilisant https://github.com/pkg/errors

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
}

Une idée simple, avec prise en charge de la décoration d'erreur, mais nécessitant un changement de langue plus drastique (évidemment pas pour go1.10) est l'introduction d'un nouveau mot-clé check .

Il aurait deux formes : check A et check A, B .

A et B doivent tous deux être error . La deuxième forme ne serait utilisée que lors d'une erreur de décoration ; les personnes qui n'ont pas besoin ou ne souhaitent pas décorer leurs erreurs utiliseront la forme la plus simple.

1ère forme (cocher A)

check A évalue A . Si nil , il ne fait rien. Sinon nil , check agit comme un return {<zero>}*, A .

Exemples

  • Si une fonction renvoie juste une erreur, elle peut être utilisée en ligne avec check , donc
err := UpdateDB()    // signature: func UpdateDb() error
if err != nil {
    return err
}

devient

check UpdateDB()
  • Pour une fonction avec plusieurs valeurs de retour, vous devrez attribuer, comme nous le faisons maintenant.
a, b, err := Foo()    // signature: func Foo() (string, string, error)
if err != nil {
    return "", "", err
}

// use a and b

devient

a, b, err := Foo()
check err

// use a and b

2ème forme (cocher A, B)

check A, B évalue A . Si nil , il ne fait rien. Sinon nil , check agit comme un return {<zero>}*, B .

C'est pour les besoins de décoration d'erreur. Nous vérifions toujours A , mais c'est B qui est utilisé dans le return implicite.

Exemple

a, err := Bar()    // signature: func Bar() (string, error)
if err != nil {
    return "", &BarError{"Bar", err}
}

devient

a, err := Foo()
check err, &BarError{"Bar", err}

Remarques

C'est une erreur de compilation pour

  • utilisez l'instruction check sur des choses qui ne s'évaluent pas à error
  • utiliser check dans une fonction avec des valeurs de retour qui ne sont pas sous la forme { type }*, error

La forme à deux expressions check A, B est court-circuitée. B n'est pas évalué si A vaut nil .

Remarques sur la praticité

Il existe un support pour les erreurs de décoration, mais vous ne payez pour la syntaxe check A, B maladroite que lorsque vous avez réellement besoin de décorer les erreurs.

Pour le if err != nil { return nil, nil, err } partout (ce qui est très courant), check err est aussi bref que possible sans sacrifier la clarté (voir la note sur la syntaxe ci-dessous).

Remarques sur la syntaxe

Je dirais que ce type de syntaxe ( check .. , au début de la ligne, similaire à un return ) est un bon moyen d'éliminer les erreurs de vérification sans masquer la perturbation du flux de contrôle qui les retours implicites introduisent.

Un inconvénient des idées comme le <do-stuff> || <handle-err> et le <do-stuff> catch <handle-err> ci-dessus, ou le a, b = foo()? proposé dans un autre fil, est qu'ils cachent la modification du flux de contrôle d'une manière qui rend le flux plus difficile suivre; le premier avec || <handle-err> machine ajouté à la fin d'une ligne par ailleurs simple, le second avec un petit symbole qui peut apparaître partout, y compris au milieu et à la fin d'une ligne de code simple, éventuellement plusieurs fois.

Une instruction check sera toujours de niveau supérieur dans le bloc actuel, ayant la même importance que d'autres instructions qui modifient le flux de contrôle (par exemple, une première return ).

@ALTree , je n'ai pas compris comment votre exemple :

a, b, err := Foo()
check err

Obtient les trois rendements valorisés de l'original :

return "", "", err

Renvoie-t-il simplement des valeurs nulles pour tous les retours déclarés, à l'exception de l'erreur finale ? Qu'en est-il des cas où vous voudriez renvoyer une valeur valide avec une erreur, par exemple le nombre d'octets écrits lorsqu'un Write() échoue ?

Quelle que soit la solution choisie, elle doit restreindre au minimum la généralité de la gestion des erreurs.

En ce qui concerne la valeur d'avoir check au début de la ligne, ma préférence personnelle est de voir le flux de contrôle principal au début de chaque ligne et que la gestion des erreurs interfère aussi peu avec la lisibilité de ce flux de contrôle principal que possible. De plus, si la gestion des erreurs est mise à part par un mot réservé comme check ou catch , alors à peu près n'importe quel éditeur moderne va mettre en évidence la syntaxe du mot réservé d'une manière ou d'une autre et la rendre visible même si c'est du côté droit.

@billyh ceci est expliqué ci-dessus, sur la ligne qui dit :

S'il n'est pas nul, le chèque agit comme un return {<zero>}*, A

check renverra la valeur zéro de toute valeur de retour, à l'exception de l'erreur (en dernière position).

Qu'en est-il des cas où vous souhaitez retourner une valeur valide avec une erreur

Ensuite, vous utiliserez l'idiome if err != nil { .

Il existe de nombreux cas où vous aurez besoin d'une procédure de récupération d'erreurs plus sophistiquée. Par exemple, après avoir détecté une erreur, vous devrez peut-être annuler quelque chose ou écrire quelque chose dans un fichier journal. Dans tous ces cas, vous aurez toujours l'idiome if err habituel dans votre boîte à outils, et vous pouvez l'utiliser pour démarrer un nouveau bloc, où tout type d'opération liée à la gestion des erreurs, quelle que soit son articulation, peut être effectué.

Quelle que soit la solution choisie, elle doit restreindre au minimum la généralité de la gestion des erreurs.

Voir ma réponse ci-dessus. Vous aurez toujours if et tout ce que la langue vous donne maintenant.

à peu près n'importe quel éditeur moderne va mettre en évidence le mot réservé

Peut-être. Mais introduire une syntaxe opaque, qui nécessite une coloration syntaxique pour être lisible, n'est pas idéal.

ce bogue particulier peut être corrigé en introduisant une fonction de double retour dans le langage.
dans ce cas la fonction a() renvoie 123 :

func a() int {
b()
retour 456
}
fonction b() {
return return int(123)
}

Cette fonction peut être utilisée pour simplifier la gestion des erreurs comme suit :

func handle(var *foo, err error )(var *foo, err error ) {
si erreur != nil {
retour retour nul, err
}
retour var, nul
}

func client_code() (*client_object, erreur) {
var obj, err =handle(quelquechose_qui_can_fail())
// ceci n'est atteint que si quelque chose n'a pas échoué
// sinon la fonction client_code propagerait l'erreur dans la pile
assert(err == nil)
}

Cela permet aux gens d'écrire des fonctions de gestion d'erreurs qui peuvent propager les erreurs dans la pile
ces fonctions de gestion des erreurs peuvent être séparées du code principal

Désolé si je me suis trompé, mais je veux clarifier un point, la fonction ci-dessous produira une erreur, vet avertissement

func Chdir(dir string) (err error) {
    syscall.Chdir(dir) || err
    return nil
}

@rodcorsi Selon cette proposition, votre exemple serait accepté sans avertissement du vétérinaire. Cela équivaudrait à

if err := syscall.Chdir(dir); err != nil {
    return err
}

Que diriez-vous d'étendre l'utilisation de Context pour gérer les erreurs ? Par exemple, étant donné la définition suivante :
type ErrorContext interface { HasError() bool SetError(msg string) Error() string }
Maintenant dans la fonction sujette aux erreurs ...
func MyFunction(number int, ctx ErrorContext) int { if ctx.HasError() { return 0 } return number + 1 }
Dans la fonction intermédiaire...
func MyIntermediateFunction(ctx ErrorContext) int { if ctx.HasError() { return 0 } number := 0 number = MyFunction(number, ctx) number = MyFunction(number, ctx) number = MyFunction(number, ctx) return number }
Et dans la fonction de niveau supérieur
func main() { ctx := context.New() no := MyIntermediateFunction(ctx) if ctx.HasError() { log.Fatalf("Error: %s", ctx.Error()) return } fmt.Printf("%d\n", no) }
Il y a plusieurs avantages à utiliser cette approche. Premièrement, cela ne distrait pas le lecteur du chemin d'exécution principal. Il y a un minimum d'instructions "if" pour indiquer un écart par rapport au chemin d'exécution principal.

Deuxièmement, il ne cache pas l'erreur. Il ressort clairement de la signature de la méthode que si elle accepte ErrorContext, alors la fonction peut avoir des erreurs. À l'intérieur de la fonction, il utilise les instructions de branchement normales (par exemple "if") qui montrent comment l'erreur est gérée à l'aide du code Go normal.

Troisièmement, l'erreur est automatiquement transmise à la partie intéressée, qui dans ce cas est le propriétaire du contexte. S'il y a un traitement d'erreur supplémentaire, il sera clairement indiqué. Par exemple, apportons quelques modifications à la fonction intermédiaire pour envelopper toute erreur existante :
func MyIntermediateFunction(ctx ErrorContext) int { if ctx.HasError() { return 0 } number := 0 number = MyFunction(number, ctx) number = MyFunction(number, ctx) number = MyFunction(number, ctx) if ctx.HasError() { ctx.SetError(fmt.Sprintf("wrap msg: %s", ctx.Error()) return } number *= 20 number = MyFunction(number, ctx) return number }
Fondamentalement, vous écrivez simplement le code de gestion des erreurs selon vos besoins. Vous n'avez pas besoin de les gonfler manuellement.

Enfin, en tant qu'auteur de la fonction, vous avez votre mot à dire si l'erreur doit être gérée. En utilisant l'approche Go actuelle, il est facile de le faire ...
````
// étant donné la définition suivante
func MyFunction (nombre entier) erreur

//puis faites ceci
MyFunction(8) //sans vérifier l'erreur
With the ErrorContext, you as the function owner can make the error checking optional with this:
func MyFunction(ctx ErrorContext) {
si ctx != nil && ctx.HasError() {
retourner
}
//...
}
Or make it compulsory with this:
func MyFunction(ctx ErrorContext) {
if ctx.HasError() { // paniquera si ctx est nul
retourner
}
//...
}
If you make error handling compulsory and yet the user insists on ignoring error, they can still do that. However, they have to be very explicit about it (to prevent accidental ignore). For instance:
func UpperFunction (ctx ErrorContext) {
ignoré := context.New()
MyFunction(ignored) //celui-ci est ignoré

 MyFunction(ctx) //this one is handled

}
````
Cette approche ne change rien au langage existant.

@ALTree Alberto, que diriez-vous de mélanger vos check et ce que @ianlancetaylor a proposé ?

alors

func F() (int, string, error) {
   i, s, err := OhNo()
   if err != nil {
      return i, s, &BadStuffHappened(err, "oopsie-daisy")
   }
   // all is good
   return i+1, s+" ok", nil
}

devient

func F() (int, string, error) {
   i, s, err := OhNo()
   check i, s, err || &BadStuffHappened(err, "oopsie-daisy")
   // all is good
   return i+1, s+" ok", nil
}

En outre, nous pouvons limiter check pour ne traiter que les types d'erreur, donc si vous avez besoin de plusieurs valeurs de retour, elles doivent être nommées et affectées, donc il attribue "en place" en quelque sorte et se comporte comme un simple "retour"

func F() (a int, s string, err error) {
   i, s, err = OhNo()
   check err |=  &BadStuffHappened(err, "oopsy-daisy")  // assigns in place and behaves like simple "return"
   // all is good
   return i+1, s+" ok", nil
}

Si return devenait acceptable dans l'expression un jour, alors check n'est pas nécessaire, ou devient une fonction standard

func check(e error) bool {
   return e != nil
}

func F() (a int, s string, err error) {
   i, s, err = OhNo()
   check(err) || return &BadStuffHappened(err, "oopsy-daisy")
   // all is good
   return i+1, s+" ok", nil
}

la dernière solution ressemble à Perl cependant 😄

Je ne me souviens plus qui l'a proposé à l'origine, mais voici une autre idée de syntaxe (le bikeshed préféré de tout le monde :-). Je ne dis pas que c'est une bonne idée, mais si on jette des idées dans la marmite...

x, y := try foo()

équivaudrait à :

x, y, err := foo()
if err != nil {
    return (an appropriate number of zero values), err
}

et

x, y := try foo() catch &FooErr{E:$, S:"bad"}

équivaudrait à :

x, y, err := foo()
if err != nil {
    return (an appropriate number of zero values), &FooErr{E:err, S:"bad"}
}

La forme try a certainement été proposée avant, un certain nombre de fois, modulo des différences de syntaxe superficielles. La forme try ... catch est moins souvent proposée, mais elle est clairement similaire à la construction check A, B @ALTree et à la @tandr . Une différence est qu'il s'agit d'une expression, pas d'une déclaration, de sorte que vous pouvez dire :

z(try foo() catch &FooErr{E:$, S:"bad"})

Vous pouvez avoir plusieurs try/catchs dans une seule instruction :

p = try q(0) + try q(1)
a = try b(c, d() + try e(), f, try g() catch &GErr{E:$}, h()) catch $BErr{E:$}

même si je ne pense pas que nous voulions encourager cela. Vous devez également faire attention ici à l'ordre d'évaluation. Par exemple, si h() est évalué pour les effets secondaires si e() renvoie une erreur non nulle.

De toute évidence, de nouveaux mots-clés comme try et catch briseraient la compatibilité Go 1.x.

Je suggère que nous devrions serrer la cible de cette proposition. Quel problème sera résolu par cette proposition ? Réduisez les trois lignes suivantes en deux ou une ? Cela pourrait être un changement de langue de retour/si.

if err != nil {
    return err
}

Ou réduire le nombre de fois pour vérifier l'erreur ? Il peut s'agir d'une solution try/catch pour cela.

Je voudrais suggérer que toute syntaxe de raccourci raisonnable pour la gestion des erreurs a trois propriétés :

  1. Il ne doit pas apparaître avant le code qu'il vérifie, afin que le chemin sans erreur soit bien visible.
  2. Il ne doit pas introduire de variables implicites dans la portée, afin que les lecteurs ne soient pas confus lorsqu'il existe une variable explicite avec le même nom.
  3. Cela ne devrait pas rendre une action de récupération (par exemple, return err ) plus facile qu'une autre. Parfois, une action entièrement différente peut être préférable (comme appeler t.Fatal ). Nous ne voulons pas non plus décourager les gens d'ajouter un contexte supplémentaire.

Compte tenu de ces contraintes, il semble qu'une syntaxe presque minimale serait quelque chose comme

STMT SEPARATOR_TOKEN VAR BLOCK

Par example,

syscall.Chdir(dir) :: err { return err }

ce qui équivaut à

if err := syscall.Chdir(dir); err != nil {
    return err
}
````
Even though it's not much shorter, the new syntax moves the error path out of the way. Part of the change would be to modify `gofmt` so it doesn't line-break one-line error-handling blocks, and it indents multi-line error-handling blocks past the opening `}`.

We could make it a bit shorter by declaring the error variable in place with a special marker, like

syscall.Chdir(dir) :: { return @err }
```

Je me demande comment cela se comporte comme pour les valeurs non nulles et les erreurs renvoyées. Par exemple, bufio.Peek renvoie éventuellement une valeur non nulle et ErrBufferFull les deux en même temps.

@mattn, vous pouvez toujours utiliser l'ancienne syntaxe.

@nigeltao Oui, je comprends. Je soupçonne que ce comportement peut créer un bogue dans le code de l'utilisateur puisque bufio.Peek renvoie également des valeurs non nulles et nulles. Le code ne doit pas impliquer de valeurs implicites et d'erreur à la fois. Ainsi, la valeur et l'erreur doivent toutes deux être renvoyées à l'appelant (dans ce cas).

ret, err := doSomething() :: err { return err }
return ret, err

@jba Ce que vous décrivez ressemble un peu à un opérateur de composition de fonction transposé :

syscall.Chdir(dir) ⫱ func (err error) { return &PathError{"chdir", dir, err} }

Mais le fait que nous écrivions du code majoritairement impératif nécessite que nous n'utilisions pas de fonction en deuxième position, car une partie du but est de pouvoir revenir tôt.

Alors maintenant, je pense à trois observations qui sont toutes liées :

  1. La gestion des erreurs est comme la composition de fonctions, mais la façon dont nous faisons les choses dans Go est en quelque sorte l'opposé de la monade d'erreur de Haskell : parce que nous écrivons principalement du code impératif au lieu du code séquentiel, nous voulons transformer l'erreur (pour ajouter du contexte) plutôt que la valeur sans erreur (que nous préférons simplement lier à une variable).

  2. Les fonctions Go qui renvoient (x, y, error) signifient généralement quelque chose comme une union (#19412) de (x, y) | error .

  3. Dans les langages qui décompressent ou font correspondre les unions, les cas sont des portées distinctes, et une grande partie des problèmes que nous avons avec les erreurs dans Go est due à l'ombrage inattendu des variables redéclarées qui pourraient être améliorées en séparant ces portées (#21114).

Alors peut-être que ce que nous voulons vraiment, c'est comme l'opérateur =: , mais avec une sorte de conditionnelle de correspondance d'union :

syscall.Chdir(dir) =? err { return &PathError{"chdir", dir, err} }

``` va
n := io.WriteString(w, s) =? err { retourner err }

and perhaps a boolean version for `, ok` index expressions and type assertions:
```go
y := m[x] =! { return ErrNotFound }

À l'exception de la portée, ce n'est pas très différent de simplement changer gofmt pour être plus favorable aux one-liners :

err := syscall.Chdir(dir); if err != nil { return &PathError{"chdir", dir, err} }

``` va
n, err := io.WriteString(w, s); if err != nil { return err }

```go
y, ok := m[x]; if !ok { return ErrNotFound }

Mais la portée est un gros problème! Les problèmes de portée sont l'endroit où ce type de code franchit la ligne de « quelque peu moche » à « des bogues subtils ».

@ianlancetaylor
Bien que je sois un fan de l'idée globale, je ne suis pas un grand partisan de la syntaxe cryptique de type perl pour cela. Peut-être qu'une syntaxe plus verbeuse serait moins déroutante, comme :

syscall.Chdir(dir) or dump(err): errors.Wrap(err, "chdir failed")

syscall.Chdir(dir) or dump

De plus, je n'ai pas compris si le dernier argument est sauté en cas d'affectation, par exemple :

resp := http.Get("https://example.com") or dump

N'oublions pas que les erreurs sont des valeurs dans go et non un type spécial.
Il n'y a rien que nous puissions faire aux autres structures que nous ne puissions faire aux erreurs et inversement. Cela signifie que si vous comprenez les structs en général, vous comprenez les erreurs et comment elles sont gérées (même si vous pensez que c'est verbeux)

Cette syntaxe obligerait les développeurs nouveaux et anciens à apprendre une nouvelle information avant de pouvoir commencer à comprendre le code qui l'utilise.

Cela seul rend cette proposition pas la peine à mon humble avis.

Personnellement je préfère cette syntaxe

err := syscall.Chdir(dir)
if err != nil {
    return err
}
return nil

sur

if err := syscall.Chdir(dir); err != nil {
    return err
}
return nil

C'est une ligne de plus mais elle sépare l'action prévue de la gestion des erreurs. Ce formulaire est le plus lisible pour moi.

@bcmills :

À l'exception de la portée, ce n'est pas très différent du simple fait de changer de gofmt pour être plus propice aux one-liners

Pas seulement la portée ; il y a aussi le bord gauche. Je pense que cela affecte vraiment la lisibilité. je pense

syscall.Chdir(dir) =: err; if err != nil { return &PathError{"chdir", dir, err} } 

est beaucoup plus clair que

err := syscall.Chdir(dir); if err != nil { return &PathError{"chdir", dir, err} } 

surtout lorsqu'il se produit sur plusieurs lignes consécutives, car votre œil peut balayer le bord gauche pour ignorer la gestion des erreurs.

En mélangeant l'idée @bcmills, nous pouvons introduire un opérateur de transfert de canal conditionnel.

La fonction F2 sera exécutée si la dernière valeur n'est pas nil .

func F1() (foo, bar){}

first := F1() ?> last: F2(first, last)

Un cas particulier de transfert de tube avec instruction return

func Chdir(dir string) error {
    syscall.Chdir(dir) ?> err: return &PathError{"chdir", dir, err}
    return nil
}

Exemple réel apporté par @urandom dans un autre numéro
Pour moi beaucoup plus lisible avec un focus dans le flux primaire

func configureCloudinit(icfg *instancecfg.InstanceConfig, cloudcfg cloudinit.CloudConfig) (cloudconfig.UserdataConfig, error) {
    // When bootstrapping, we only want to apt-get update/upgrade
    // and setup the SSH keys. The rest we leave to cloudinit/sshinit.
    udata := cloudconfig.NewUserdataConfig(icfg, cloudcfg) ?> err: return nil, err
    if icfg.Bootstrap != nil {
        udata.ConfigureBasic() ?> err: return nil, err
        return udata, nil
    }
    udata.Configure() ?> err: return nil, err
    return udata, nil
}

func ComposeUserData(icfg *instancecfg.InstanceConfig, cloudcfg cloudinit.CloudConfig, renderer renderers.ProviderRenderer) ([]byte, error) {
    if cloudcfg == nil {
        cloudcfg = cloudinit.New(icfg.Series) ?> err: return nil, errors.Trace(err)
    }
    _ = configureCloudinit(icfg, cloudcfg) ?> err: return nil, errors.Trace(err)
    operatingSystem := series.GetOSFromSeries(icfg.Series) ?> err: return nil, errors.Trace(err)
    udata := renderer.Render(cloudcfg, operatingSystem) ?> err: return nil, errors.Trace(err)
    logger.Tracef("Generated cloud init:\n%s", string(udata))
    return udata, nil
}

Je reconnais que la gestion des erreurs n'est pas ergonomique. À savoir, lorsque vous lisez le code ci-dessous, vous devez le vocaliser en if error not nil then - ce qui se traduit par if there is an error then .

if err != nil {
    // handle error
}

J'aimerais avoir la possibilité d'exprimer le code ci-dessus de cette manière, ce qui, à mon avis, est plus lisible.

if err {
    // handle error
}

Juste mon humble suggestion :)

Cela ressemble à perl, il a même la variable magique
Pour référence, en perl vous feriez

open (FILE, $file) ou die("impossible d'ouvrir $file : $!");

IMHO, ça n'en vaut pas la peine, un point que j'aime à propos de go est cette gestion des erreurs
est explicite et 'dans votre visage'

Si nous nous en tenons à cela, j'aimerais n'avoir aucune variable magique, nous devrions être
capable de nommer la variable d'erreur

e := syscall.Chdir(dir) ?> e: &PathError{"chdir", dir, e}

Et nous pourrions aussi bien utiliser un symbole différent de || spécifique à cette tâche,
Je suppose que les symboles de texte comme « ou » ne sont pas possibles en raison de l'envers
compatibilité

n, _, err, _ = somecall(...) ?> err: &PathError{"somecall", n, err}

Le mardi 1er août 2017 à 14 h 47, Rodrigo [email protected] a écrit :

Mélanger l'idée @bcmills https://github.com/bcmills nous pouvons vous présenter
opérateur de transfert de tuyau conditionnel.

La fonction F2 sera exécutée si la dernière valeur n'est pas nulle .

func F1() (foo, bar){}
premier := F1() ?> dernier : F2(premier, dernier)

Un cas particulier de transfert de tube avec instruction return

func Chdir (chaîne de répertoire) erreur {
syscall.Chdir(dir) ?> err: return &PathError{"chdir", dir, err}
retour nul
}

Exemple réel
https://github.com/juju/juju/blob/01b24551ecdf20921cf620b844ef6c2948fcc9f8/cloudconfig/providerinit/providerinit.go
apporté par @urandom https://github.com/urandom dans un autre numéro
Pour moi beaucoup plus lisible avec un focus dans le flux primaire

func configureCloudinit(icfg *instancecfg.InstanceConfig, cloudcfg cloudinit.CloudConfig) (cloudconfig.UserdataConfig, erreur) {
// Lors de l'amorçage, nous voulons seulement apt-get update/upgrade
// et configurez les clés SSH. Le reste, nous laissons à cloudinit/sshinit.
udata := cloudconfig.NewUserdataConfig(icfg, cloudcfg) ?> err: return nil, err
if icfg.Bootstrap != nil {
udata.ConfigureBasic() ?> err : renvoie nil, err
retourner udata, nil
}
udata.Configure() ?> err : renvoie nil, err
retourner udata, nil
}
func ComposeUserData(icfg *instancecfg.InstanceConfig, cloudcfg cloudinit.CloudConfig, renderer renderers.ProviderRenderer) ([]byte, error) {
si cloudcfg == nil {
cloudcfg = cloudinit.New(icfg.Series) ?> err: return nil, error.Trace(err)
}
configureCloudinit(icfg, cloudcfg) ?> err : renvoie nil, error.Trace(err)
OperatingSystem := series.GetOSFromSeries(icfg.Series) ?> err: return nil, error.Trace(err)
udata := renderer.Render(cloudcfg, operatingSystem) ?> err: return nil, error.Trace(err)
logger.Tracef("Init cloud généré :\n%s", string(udata))
retourner udata, nil
}

-
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/21161#issuecomment-319359614 , ou couper le son
le fil
https://github.com/notifications/unsubscribe-auth/AbwRO_J0h2dQHqfysf2roA866vFN4_1Jks5sTx5hgaJpZM4Oi1c-
.

Suis-je le seul à penser que tous ces changements proposés seraient plus compliqués que la forme actuelle.

Je pense que la simplicité et la brièveté ne sont pas égales ou interchangeables. Oui, tous ces changements seraient plus courts d'une ou plusieurs lignes mais introduiraient des opérateurs ou des mots-clés qu'un utilisateur de la langue devrait apprendre.

@rodcorsi Je sais que cela semble mineur, mais je pense qu'il est important que la deuxième partie soit un bloc : les instructions if et for existantes utilisent toutes les deux des blocs, et select et switch utilisent tous deux une syntaxe délimitée par des accolades, il semble donc difficile d'omettre les accolades pour cette opération particulière de flux de contrôle.

Il est également beaucoup plus facile de s'assurer que l'arbre d'analyse est sans ambiguïté si vous n'avez pas à vous soucier des expressions arbitraires suivant les nouveaux symboles.

La syntaxe et la sémantique que j'avais en tête pour mon sketch sont :


NonZeroGuardStmt = ( Expression | IdentifierList ":=" Expression |
                     ExpressionList assign_op Expression ) "=?" [ identifier ] Block .
ZeroGuardStmt = ( Expression | IdentifierList ":=" Expression |
                  ExpressionList assign_op Expression ) "=!" Block .

Un NonZeroGuardStmt exécute Block si la dernière valeur d'un Expression n'est pas égale à la valeur zéro de son type. Si un identifier est présent, il est lié à cette valeur dans Block . Un ZeroGuardStmt exécute Block si la dernière valeur d'un Expression est égale à la valeur zéro de son type.

Pour la forme := , les autres valeurs (principales) du Expression sont liées au IdentifierList comme dans un ShortVarDecl . Les identifiants sont déclarés dans le scope conteneur, ce qui implique qu'ils sont également visibles dans le Block .

Pour la forme assign_op , chaque opérande de gauche doit être adressable, une expression d'index de carte ou (pour les affectations = uniquement) l'identifiant vide. Les opérandes peuvent être entre parenthèses. Les autres valeurs (principales) du membre de droite Expression sont évaluées comme dans un Assignment . L'affectation a lieu avant l'exécution du Block et indépendamment du fait que le Block soit ensuite exécuté.


Je pense que la grammaire proposée ici est compatible avec Go 1 : ? n'est pas un identifiant valide et il n'y a pas d'opérateurs Go existants utilisant ce caractère, et bien que ! soit un opérateur valide, il n'y a pas production existante dans laquelle il peut être suivi d'un { .

@bcmills LGTM, avec des modifications concomitantes de gofmt.

J'aurais pensé que vous feriez de =? et =! chacun un jeton à part entière, ce qui rendrait la grammaire trivialement compatible.

J'aurais pensé que tu ferais =? et =! chacun un signe à part entière, ce qui rendrait la grammaire trivialement compatible.

On peut faire ça dans la grammaire, mais pas dans le lexer : la séquence "=!" peut apparaître dans un code Go 1 valide (https://play.golang.org/p/pMTtUWgBN9).

L'accolade est ce qui rend l'analyse sans ambiguïté dans ma proposition : =! ne peut actuellement apparaître que dans une déclaration ou une affectation à une variable booléenne, et les déclarations et les affectations ne peuvent actuellement pas apparaître immédiatement avant une accolade (https ://play.golang.org/p/ncJyg-GMuL) sauf si séparé par un point-virgule implicite (https://play.golang.org/p/lhcqBhr7Te).

@romainmenke Non. Tu n'es pas le seul. Je ne vois pas la valeur d'une gestion d'erreur d'une seule ligne. Vous pouvez économiser une ligne mais ajouter beaucoup plus de complexité. Le problème est que dans bon nombre de ces propositions, la partie gestion des erreurs devient cachée. L'idée n'est pas de les rendre moins visibles car la gestion des erreurs est importante, mais de rendre le code plus facile à lire. La brièveté n'est pas synonyme de lisibilité facile. Si vous devez apporter des modifications au système de gestion des erreurs existant, je trouve que le try-catch-finally conventionnel est beaucoup plus attrayant que de nombreuses idées ici.

J'aime la proposition check car vous pouvez également l'étendre pour gérer

f, err := os.Open(myfile)
check err
defer check f.Close()

D'autres propositions ne semblent pas pouvoir se mélanger avec defer . check est également très lisible, et simple pour Google si vous ne le connaissez pas. Je ne pense pas que cela doive être limité au type error . Tout ce qui est un paramètre de retour en dernière position pourrait l'utiliser. Ainsi, un itérateur peut avoir un check pour un Next() bool .

J'ai écrit une fois un scanner qui ressemble à

func (s *Scanner) Next() bool {
    if s.Error != nil || s.pos >= s.RecordCount {
        return false
    }
    s.pos++

    var rt uint8
    if !s.read(&rt) {
        return false
    }
...

Ce dernier bit pourrait être check s.read(&rt) place.

@carlmjohnson

D'autres propositions ne semblent pas pouvoir se mélanger avec defer .

Si vous supposez que nous allons développer defer pour permettre le retour de la fonction externe à l'aide de la nouvelle syntaxe, vous pouvez également appliquer cette hypothèse à d'autres propositions.

defer f.Close() =? err { return err }

Étant donné que la proposition check @ALTree introduit une déclaration distincte, je ne vois pas comment vous pourriez mélanger cela avec un defer qui fait autre chose que simplement renvoyer l'erreur.

defer func() {
  err := f.Close()
  check err, fmt.Errorf(…, err) // But this func() doesn't return an error!
}()

Contraste:

defer f.Close() =? err { return fmt.Errorf(…, err) }

La justification de bon nombre de ces propositions est une meilleure "ergonomie", mais je ne vois pas vraiment en quoi l'une d'entre elles est meilleure autre que de le faire pour qu'il y ait un peu moins à taper. Comment cela augmente-t-il la maintenabilité du code ? La composabilité ? La lisibilité ? La facilité de compréhension du flux de contrôle ?

@jimmyfrasche

Comment cela augmente-t-il la maintenabilité du code ? La composabilité ? La lisibilité ? La facilité de compréhension du flux de contrôle ?

Comme je l'ai noté plus tôt, le principal avantage de l'une de ces propositions devrait probablement provenir d'une portée plus claire des affectations et des variables err : voir #19727, #20148, #5634, #21114, et probablement d'autres pour divers manières dont les gens rencontrent des problèmes de portée en ce qui concerne la gestion des erreurs.

@bcmills merci d'avoir fourni une motivation et désolé de l'avoir manqué dans votre message précédent.

Compte tenu de cette prémisse, cependant, ne serait-il pas préférable de fournir une facilité plus générale pour une « définition plus claire des affectations » qui pourrait être utilisée par toutes les variables ? J'ai involontairement occulté ma part de variables sans erreur, certainement.

Je me souviens quand le comportement actuel de := été introduit - une grande partie de ce fil sur go nut† était à la demande d'un moyen d'annoter explicitement les noms à réutiliser au lieu de l'implicite "réutiliser uniquement si cette variable existe dans exactement la portée actuelle" qui est l'endroit où se manifestent tous les problèmes subtils difficiles à voir, d'après mon expérience.

Je ne trouve pas ce fil, est-ce que quelqu'un a un lien ?

Je pense qu'il y a beaucoup de choses qui pourraient être améliorées à propos de Go, mais le comportement de := m'a toujours semblé être la seule erreur grave. Peut-être que revoir le comportement de := est le moyen de résoudre le problème à la racine ou au moins de réduire le besoin d'autres changements plus extrêmes ?

@jimmyfrasche

Compte tenu de cette prémisse, cependant, ne serait-il pas préférable de fournir une facilité plus générale pour une « définition plus claire des affectations » qui pourrait être utilisée par toutes les variables ?

Oui. C'est l'une des choses que j'aime à propos de l'opérateur =? ou :: que @jba et moi avons proposé : il s'étend également bien à (un sous-ensemble certes limité de) non-erreurs.

Personnellement, je soupçonne que je serais plus heureux à long terme avec une fonctionnalité de type de données tagged-union/varint/algebraic plus explicite (voir aussi #19412), mais c'est un changement beaucoup plus important dans le langage : il est difficile de voir comment nous pourrions moderniser cela sur les API existantes dans un environnement mixte Go 1 / Go 2.

La facilité de compréhension du flux de contrôle ?

Dans mes propositions et @bcmills , votre œil peut balayer le côté gauche et prendre facilement en compte le flux de contrôle sans erreur.

@bcmills Je pense que je suis responsable d'au moins la moitié des mots dans #19412 donc vous n'avez pas besoin de me vendre des types de somme ;)

Lorsqu'il s'agit de renvoyer des éléments avec une erreur, il existe quatre cas

  1. juste une erreur (pas besoin de faire quoi que ce soit, retourne juste une erreur)
  2. des trucs ET une erreur (vous géreriez cela exactement comme vous le feriez maintenant)
  3. une chose OU une erreur (vous pouvez utiliser des types de somme ! :tada: )
  4. deux choses ou plus OU une erreur

Si vous atteignez 4, c'est là que les choses se compliquent. Sans introduire de types de tuples (types de produits non étiquetés pour aller avec les types de produits étiquetés de la structure), vous devrez réduire le problème au cas 3 en regroupant tout dans une structure si vous souhaitez utiliser des types de somme pour modéliser "ceci ou une erreur".

L'introduction de types de tuple causerait toutes sortes de problèmes et de problèmes de compatibilité et de chevauchements étranges (est-ce que func() (int, string, error) un tuple défini implicitement ou plusieurs valeurs de retour sont-elles un concept distinct ? S'il s'agit d'un tuple défini implicitement, cela signifie-t-il func() (n int, msg string, err error) est une structure définie implicitement !? Si c'est une structure, comment puis-je accéder aux champs si je ne suis pas dans le même package !)

Je pense toujours que les types de somme offrent de nombreux avantages, mais ils ne font rien pour résoudre les problèmes de portée, bien sûr. Si quoi que ce soit, ils pourraient aggraver les choses, car vous pouviez masquer la totalité du "résultat ou de l'erreur" au lieu de simplement masquer le cas d'erreur lorsque vous aviez quelque chose dans le cas de résultat.

@jba Je ne vois pas en quoi c'est une propriété souhaitable. Mis à part un manque général de facilité avec le concept de rendre le flux de contrôle bidimensionnel, pour ainsi dire, je ne peux pas vraiment penser à pourquoi il ne l'est pas non plus. Pouvez-vous m'expliquer l'avantage?

Sans introduire de types de tuples […] vous devrez tout regrouper dans une structure si vous souhaitez utiliser des types sum pour modéliser « ceci ou une erreur ».

Je suis d'accord avec ça: je pense que nous aurions ainsi des sites d'appel beaucoup plus lisibles (plus de liaisons de position transposées accidentellement!), Et #12854 atténuerait une grande partie de la surcharge actuellement associée aux retours de struct.

Le gros problème est la migration : comment passer du modèle « valeurs et erreur » de Go 1 à un modèle potentiel « valeurs ou erreur » dans Go 2, en particulier compte tenu des API comme io.Writer qui renvoient vraiment des « valeurs » et erreur" ?

Je pense toujours que les types de somme offrent de nombreux avantages, mais ils ne font rien pour résoudre les problèmes de portée, bien sûr.

Cela dépend de la façon dont vous les déballez, ce qui, je suppose, nous ramène là où nous en sommes aujourd'hui. Si vous préférez les unions, vous pouvez peut-être envisager une version de =? tant qu'API de « correspondance de modèle asymétrique » :

i := match strconv.Atoi(str) | err error { return err }

match serait l'opération de correspondance de modèle de style ML traditionnelle, mais le dans le cas d'une correspondance non exhaustive renverrait la valeur (sous forme de interface{} si l'union a plus d'une alternative sans correspondance) plutôt que de paniquer avec un échec de match non exhaustif.

Je viens d'enregistrer un package sur https://github.com/mpvl/errd qui résout les problèmes discutés ici par programmation (aucun changement de langue). L'aspect le plus important de ce package est qu'il raccourcit non seulement le traitement des erreurs, mais le rend également plus facile à faire correctement. Je donne des exemples dans la documentation sur la façon dont la gestion traditionnelle des erreurs idiomatiques est plus délicate qu'il n'y paraît, en particulier dans l'interaction avec defer.

Je considère cependant qu'il s'agit d'un package « brûleur » ; l'objectif est d'acquérir une bonne expérience et un aperçu de la meilleure façon d'étendre la langue. Il interagit assez bien avec les génériques, d'ailleurs, si cela devenait une chose.

Je travaille toujours sur d'autres exemples, mais ce package est prêt à être expérimenté.

@bcmills un million :+1: pour #12854

Comme vous l'avez noté, il y a "retour X et erreur" et "retour X ou erreur" donc vous ne pourriez pas vraiment contourner cela sans une macro qui traduit l'ancienne méthode en la nouvelle à la demande (et bien sûr il y aurait des bugs ou au moins des paniques d'exécution alors qu'il était inévitablement utilisé pour une fonction "X et erreur").

Je n'aime vraiment pas l'idée d'introduire des macros spéciales dans le langage, surtout si c'est juste pour la gestion des erreurs, ce qui est mon problème majeur avec beaucoup de ces propositions.

Go n'est pas fan de sucre ou de magie et c'est un plus.

Il y a trop d'inertie et trop peu d'informations encodées dans la pratique actuelle pour gérer un saut de masse vers un paradigme de gestion des erreurs plus fonctionnel.

Si Go 2 obtient des types de somme - ce qui me choquerait franchement (dans le bon sens !) plus de fragmentation et de confusion dans la façon de gérer les erreurs, donc je ne vois pas cela comme un net positif. (Je commencerais cependant immédiatement à l'utiliser pour des trucs comme chan union { Msg1 T; Msg2 S; Err error } au lieu de trois canaux).

Si c'était avant Go1 et que l'équipe Go pouvait dire "nous allons juste passer les six prochains mois à tout déplacer et quand ça casse des trucs, continuez" ce serait une chose, mais pour le moment nous sommes essentiellement coincés même si nous obtenons des types de somme.

Comme vous l'avez noté, il y a "retour X et erreur" et "retour X ou erreur" donc vous ne pouvez pas vraiment contourner cela sans une macro qui traduit l'ancienne méthode dans la nouvelle façon à la demande

Comme j'ai essayé de le dire ci-dessus, je ne pense pas qu'il soit nécessaire pour la nouvelle façon, quelle qu'elle soit, de couvrir "retour X et erreur". Si la grande majorité des cas sont "retour X ou erreur", et que la nouvelle méthode n'améliore que cela, alors c'est très bien, et vous pouvez toujours utiliser l'ancienne méthode compatible Go 1 pour le "retour X et erreur" plus rare.

@nigeltao Vrai, mais nous aurions encore besoin d'un moyen de les distinguer pendant la transition, à moins que vous ne gardions toute la bibliothèque standard dans le style existant.

@jimmyfrasche Je ne pense pas pouvoir construire un argument pour cela. Vous pouvez regarder mon discours ou voir l'exemple dans le repo README . Mais si la preuve visuelle n'est pas convaincante pour vous, alors je ne peux rien dire.

@jba a regardé la conférence et lu le README. Je comprends maintenant d'où vous venez avec le truc entre parenthèses/note de bas de page/note de fin/note secondaire (et je suis un fan des notes secondaires (et des parenthèses)).

Si le but est de mettre, faute d'un meilleur terme, le chemin malheureux de côté, alors un plugin $EDITOR fonctionnerait sans changement de langue et il fonctionnerait avec tout le code existant, quelles que soient les préférences de l'auteur du code.

Un changement de langue rend la syntaxe un peu plus compacte. @bcmills mentionne que cela améliore la portée, mais je ne vois pas vraiment comment cela pourrait être possible à moins qu'il n'ait des règles de portée différentes de celles de := mais il semble que cela causerait plus de confusion.

@bcmills Je ne comprends pas votre commentaire. Vous pouvez évidemment les distinguer. L'ancienne méthode ressemble à ceci :

err := foo()
if err != nil {
  return n, err  // n can be non-zero
}

La nouvelle façon ressemble

check foo()

ou

foo() || &FooError{err}

ou quelle que soit la couleur de l'abri de vélo. Je suppose que la plupart des bibliothèques standard peuvent effectuer une transition, mais pas toutes.

Pour compléter les exigences de

Considérons, par exemple, l'écriture dans un fichier Google Cloud Storage, où nous souhaitons interrompre l'écriture du fichier en cas d'erreur :

func writeToGS(ctx context.Context, bucket, dst string, r io.Reader) (err error) {
    client, err := storage.NewClient(ctx)
    if err != nil {
        return err
    }
    defer client.Close()

    w := client.Bucket(bucket).Object(dst).NewWriter(ctx)
    defer func() {
        if r := recover(); r != nil {
            w.CloseWithError(fmt.Errorf("panic: %v", r))
            panic(r)
        }
        if err != nil {
            _ = w.CloseWithError(err)
        } else {
            err = w.Close()
        }
    }
    _, err = io.Copy(w, r)
    return err
}

Les subtilités de ce code incluent :

  • L'erreur de Copy est sournoisement transmise via l'argument de retour nommé à la fonction defer.
  • Pour être totalement sûr, nous attrapons les paniques de r et veillons à ce que nous abandonnions l'écriture avant de reprendre la panique.
  • Ignorer l'erreur du premier Close est intentionnel, mais ressemble un peu à un artefact de programmeur paresseux.

En utilisant le package errd, ce code ressemble à :

func writeToGS(ctx context.Context, bucket, dst, src string, r io.Reader) error {
    return errd.Run(func(e *errd.E) {
        client, err := storage.NewClient(ctx)
        e.Must(err)
        e.Defer(client.Close, errd.Discard)

        w := client.Bucket(bucket).Object(dst).NewWriter(ctx)
        e.Defer(w.CloseWithError)

        _, err = io.Copy(w, r)
        e.Must(err)
    })
}

errd.Discard est un gestionnaire d'erreurs. Les gestionnaires d'erreurs peuvent également être utilisés pour envelopper, consigner, toutes les erreurs.

e.Must est l'équivalent de foo() || wrapError

e.Defer est extra et traite les erreurs de transmission aux différés.

En utilisant des génériques, ce morceau de code pourrait ressembler à quelque chose comme :

func writeToGS(ctx context.Context, bucket, dst, src string, r io.Reader) error {
    return errd.Run(func(e *errd.E) {
        client := e.Must(storage.NewClient(ctx))
        e.Defer(client.Close, errd.Discard)

        w := client.Bucket(bucket).Object(dst).NewWriter(ctx)
        e.Defer(w.CloseWithError)

        _ = e.Must(io.Copy(w, r))
    })
}

Si on standardise les méthodes à utiliser pour le Defer, cela pourrait même ressembler à :

func writeToGS(ctx context.Context, bucket, dst, src string, r io.Reader) error {
    return errd.Run(func(e *errd.E) {
        client := e.DeferClose(e.Must(storage.NewClient(ctx)), errd.Discard)
       e.Must(io.Copy(e.DeferClose(client.Bucket(bucket).Object(dst).NewWriter(ctx)), r)
    })
}

Où DeferClose sélectionne Close ou CloseWithError. Ne pas dire que c'est mieux, mais juste montrer les possibilités.

Quoi qu'il en soit, j'ai fait une présentation lors d'une réunion à Amsterdam la semaine dernière sur ce sujet et il semblait que la possibilité de faciliter la gestion des erreurs est considérée comme plus utile que de la raccourcir.

Une solution qui améliore les erreurs devrait se concentrer au moins autant sur le fait de rendre les choses plus faciles que de raccourcir les choses.

@ALTree errd gère la "récupération d'erreurs sophistiquée"

@jimmyfrasche : errd fait à peu près ce que fait votre exemple de terrain de jeu, mais tisse aussi au passage des erreurs et des paniques pour reporter.

@jimmyfrasche : Je suis d'accord que la plupart des propositions n'ajoutent pas grand-chose à ce qui peut déjà être réalisé dans le code.

@romainmenke : d'accord pour dire qu'on met trop l'accent sur la brièveté. Pour qu'il soit plus facile de faire les choses correctement, il faut se concentrer davantage.

@jba : l'approche errd permet d'analyser assez facilement le flux d'erreur par rapport au flux sans erreur en regardant simplement le côté gauche (tout ce qui commence par e. est une erreur ou une gestion différée). Cela permet également de rechercher très facilement quelles valeurs de retour sont gérées en cas d'erreur ou de report et lesquelles ne le sont pas.

@bcmills : bien que errd ne résolve pas les problèmes de portée en soi, il élimine le besoin de transmettre les erreurs en aval aux variables d'erreur déclarées précédemment et à tout, atténuant ainsi considérablement le problème de gestion des erreurs, AFAICT.

errd semble reposer entièrement sur les paniques et la récupération. cela semble venir avec une pénalité de performance significative. Je ne suis pas sûr que ce soit une solution globale à cause de cela.

@urandom : Sous le capot, il est implémenté comme un report plus coûteux, mais unique.
Si le code d'origine :

  • n'utilise pas de report : la pénalité liée à l'utilisation de errd est importante, environ 100 ns*.
  • utilise le report idiomatique : le temps d'exécution ou errd est du même ordre, bien qu'un peu plus lent
  • utilise une gestion d'erreur appropriée pour le report : le temps d'exécution est à peu près égal ; errd peut être plus rapide si le nombre de reports est > 1

Autres frais généraux :

  • Passer des fermetures (w.Close) à Defer ajoute actuellement une surcharge d'environ 25 ns*, par rapport à l'utilisation de l'API DeferClose ou DeferFunc (voir la version v0.1.0). Après avoir discuté avec @rsc, je l'ai supprimé pour garder l'API simple et me soucier de l'optimisation plus tard.
  • L'encapsulation des chaînes d'erreur en ligne en tant que gestionnaires ( e.Must(err, msg("oh noes!") ) coûte environ 30 ns avec Go 1.8. Avec un pourboire (1,9), cependant, bien que toujours une allocation, j'ai chronométré le coût à 2 ns. Bien sûr, pour les messages d'erreur pré-déclarés, le coût est toujours négligeable.

(*) tous les numéros fonctionnant sur mon MacBook Pro 2016.

Dans l'ensemble, le coût semble acceptable si votre code d'origine utilise le report. Sinon, Austin s'efforce de réduire considérablement le coût du report, de sorte que le coût peut même baisser avec le temps.

Quoi qu'il en soit, le but de ce package est d'acquérir de l'expérience sur la façon dont l'utilisation d'une gestion alternative des erreurs se sentirait et serait utile maintenant afin que nous puissions créer le meilleur ajout de langue dans Go 2. Le cas d'espèce est la discussion actuelle, elle se concentre trop sur la réduction d'un quelques lignes pour des cas simples, alors qu'il y a beaucoup plus à gagner et sans doute que d'autres points sont plus importants.

@jimmyfrasche :

alors un plugin $EDITOR fonctionnerait sans changement de langue

Oui, c'est exactement ce que je dis dans le discours. Ici, je soutiens que si nous faisons un changement de langue, cela devrait être en accord avec le concept de "sidenote".

@nigeltao

Vous pouvez évidemment les distinguer. L'ancienne méthode ressemble à ceci :

Je parle du point de déclaration, pas du point d'utilisation.

Certaines des propositions discutées ici ne font pas de distinction entre les deux sur le site de l'appel, mais d'autres le font. Si nous choisissons l'une des options qui supposent "valeur ou erreur" - comme || , try … catch ou match - alors il devrait s'agir d'une erreur de compilation à utiliser cette syntaxe avec une fonction "valeur et erreur", et il devrait appartenir à l'implémenteur de la fonction de définir de laquelle il s'agit.

Au moment de la déclaration, il n'y a actuellement aucun moyen de faire la distinction entre « valeur et erreur » et « valeur ou erreur » :

func Atoi(string) (int, error)

et

func WriteString(Writer, String) (int, error)

ont les mêmes types de retour, mais une sémantique d'erreur différente.

@mpvl Je regarde les docs et src pour errd. Je pense que je commence à comprendre comment cela fonctionne, mais il semble qu'il y ait beaucoup d'API qui gênent la compréhension et qui semblent pouvoir être implémentées dans un package séparé. Je suis sûr que tout le rend plus utile dans la pratique mais à titre d'illustration, cela ajoute beaucoup de bruit.

Si nous ignorons les aides courantes telles que les fonctions de haut niveau pour opérer sur le résultat de WithDefault(), et supposons, par souci de simplicité que nous avons toujours utilisé le contexte, et ignorons toutes les décisions prises pour les performances, l'API barebone minimale absolue se réduirait-elle au ci-dessous les opérations?

type Handler = func(ctx context.Context, panicing bool, err error) error
Run(context.Context, func(*E), defaults ...Handler) //egregious style but most minimal
type struct E {...}
func (*E) Must(err error, handlers ...Handler)
func (*E) Defer(func() error, handlers ...Handler)

En regardant le code, je vois de bonnes raisons pour lesquelles il n'est pas défini comme ci-dessus, mais j'essaie d'accéder à la sémantique de base afin de mieux comprendre le concept. Par exemple, je ne sais pas si IsSentinel est dans le noyau ou non.

@jimmyfrasche

@bcmills mentionne que cela améliore la portée mais je ne vois pas vraiment comment cela pourrait

La principale amélioration est de garder la variable err hors de portée. Cela éviterait des bogues tels que ceux liés à https://github.com/golang/go/issues/19727. Pour illustrer avec un extrait de l'un d'entre eux :

    res, err := ctxhttp.Get(ctx, c.HTTPClient, dirURL)
    if err != nil {
        return Directory{}, err
    }
    defer res.Body.Close()
    c.addNonce(res.Header)
    if res.StatusCode != http.StatusOK {
        return Directory{}, responseError(res)
    }

    var v struct {
        …
    }
    if json.NewDecoder(res.Body).Decode(&v); err != nil {
        return Directory{}, err
    }

Le bogue se produit dans la dernière instruction if : l'erreur de Decode est supprimée, mais ce n'est pas évident car un err d'une vérification précédente était toujours dans la portée. En revanche, en utilisant l'opérateur :: ou =? , cela s'écrirait :

    res := ctxhttp.Get(ctx, c.HTTPClient, dirURL) =? err { return Directory{}, err }
    defer res.Body.Close()
    c.addNonce(res.Header)
    (res.StatusCode == http.StatusOK) =! { return Directory{}, responseError(res) }

    var v struct {
        …
    }
    json.NewDecoder(res.Body).Decode(&v) =? err { return Directory{}, err }

Voici deux améliorations de la portée qui aident :

  1. Le premier err (de l'appel précédent de Get ) est uniquement dans la portée du bloc return , il ne peut donc pas être utilisé accidentellement dans les vérifications suivantes.
  2. Étant donné que le err de Decode est déclaré dans la même instruction dans laquelle il est vérifié pour la nullité, il ne peut y avoir de décalage entre la déclaration et le contrôle.

(1) seul aurait été suffisant pour révéler l'erreur au moment de la compilation, mais (2) le rend facile à éviter lors de l'utilisation de l'instruction guard de manière évidente.

@bcmills merci pour la clarification

Ainsi, dans res := ctxhttp.Get(ctx, c.HTTPClient, dirURL) =? err { return Directory{}, err } la macro =? développe en

var res *http.Reponse
{
  var err error
  res, err = ctxhttp.Get(ctx, c.HTTPClient, dirURL)
  if err != nil {
    return Directory{}, err 
  }
}

Si c'est correct, c'est ce que je voulais dire quand j'ai dit que cela devrait avoir une sémantique différente de := .

Il semble que cela causerait ses propres confusions telles que :

func f() error {
  var err error
  g() =? err {
    if err != io.EOF {
      return err
    }
  }
  //one could expect that err could be io.EOF here but it will never be so
}

A moins que j'aie mal compris quelque chose.

Oui, c'est la bonne extension. Vous avez raison de dire que cela diffère de := , et c'est intentionnel.

Il semble que cela causerait ses propres confusions

C'est vrai. Il n'est pas évident pour moi si ce serait déroutant dans la pratique. Si c'est le cas, nous pourrions fournir des variantes ":" de l'instruction guard pour la déclaration (et n'affecter que les variantes "=").

(Et maintenant, cela me fait penser que les opérateurs devraient être orthographiés ? et ! au lieu de =? et =! .)

res := ctxhttp.Get(ctx, c.HTTPClient, dirURL) ?: err { return Directory{}, err }

mais

func f() error {
  var err error
  g() ?= err { (err == io.EOF) ! { return err } }
  // err may be io.EOF here.
}

@mpvl Ma principale préoccupation à propos de errd concerne l'interface Handler : elle semble encourager les pipelines de rappels de style fonctionnel, mais mon expérience avec le code de style de rappel / continuation (à la fois dans les langages impératifs comme Go et C++ et dans les langages fonctionnels langages comme ML et Haskell) est qu'il est souvent beaucoup plus difficile à suivre que le style séquentiel / impératif équivalent, qui s'aligne également avec le reste des idiomes Go.

Envisagez-vous des chaînes de style Handler dans le cadre de l'API, ou votre Handler un substitut pour une autre syntaxe que vous envisagez (comme quelque chose fonctionnant sur Block s?)

@bcmills Je ne suis toujours pas à bord avec les fonctionnalités magiques qui introduisent des dizaines de concepts dans le langage en une seule ligne et ne fonctionnent qu'avec une seule chose, mais je comprends enfin pourquoi elles sont plus qu'un moyen légèrement plus court d'écrire x, err := f(); if err != nil { return err } . Merci de m'avoir aidé à comprendre et désolé d'avoir été si long.

@bcmills J'ai réécrit l'exemple motivant de @mpvl , qui comporte une gestion des erreurs ennuyeuse, en utilisant la dernière proposition =? qui ne déclare pas toujours une nouvelle variable d'erreur :

func writeToGS(ctx context.Context, bucket, dst string, r io.Reader) (err error) {
        client := storage.NewClient(ctx) =? err { return err }
        defer client.Close()

        w := client.Bucket(bucket).Object(dst).NewWriter(ctx)

        defer func() {
                if r := recover(); r != nil { // r is interface{} not error so we can't use it here
                        _ = w.CloseWithError(fmt.Errorf("panic: %v", r))
                        panic(r)
                }

                if err != nil { // could use =! here but I don't see how that simplifies anything
                        _ = w.CloseWithError(err)
                } else {
                        err = w.Close()
                }
        }()

        io.Copy(w, r) =? err { return err } // what about n? does this need to be prefixed by a '_ ='?
        return nil
}

La majorité de la gestion des erreurs est inchangée. Je ne pouvais utiliser =? qu'à deux endroits. En premier lieu, cela n'a pas vraiment fourni d'avantage que je pouvais voir. Dans le second, le code est plus long et masque le fait que io.Copy renvoie deux choses, il aurait donc probablement été préférable de ne pas l'utiliser ici.

@jimmyfrasche Ce code est l'exception, pas la règle. Nous ne devrions pas concevoir des fonctionnalités pour faciliter l'écriture.

Aussi, je me demande si le recover devrait même être là. Si w.Write ou r.Read (ou io.Copy !) panique, il est probablement préférable de mettre fin.

Sans le recover , il n'y a pas vraiment besoin du defer , et le bas de la fonction pourrait devenir

_ = io.Copy(w, r) =? err { _ = w.CloseWithError(err); return err }
return w.Close()

@jimmyfrasche

// r is interface{} not error so we can't use it here

Notez que ma formulation spécifique (dans https://github.com/golang/go/issues/21161#issuecomment-319434101) concerne les valeurs zéro, pas les erreurs en particulier.

// what about n? does this need to be prefixed by a '_ ='?

Ce n'est pas le cas, même si j'aurais pu être plus explicite à ce sujet.

Je n'aime pas particulièrement l'utilisation de recover par @mpvl dans cet exemple : cela encourage l'utilisation de la panique par rapport au flux de contrôle idiomatique, alors que je pense que nous devrions éliminer les appels recover superflus ( comme ceux de fmt ) de la bibliothèque standard de Go 2.

Avec cette approche, j'écrirais ce code comme suit :

func writeToGS(ctx context.Context, bucket, dst string, r io.Reader) (err error) {
        client := storage.NewClient(ctx) =? err { return err }
        defer client.Close()

        w := client.Bucket(bucket).Object(dst).NewWriter(ctx)
        io.Copy(w, r) =? err {
                w.CloseWithError(err)
                return err
        }
        return w.Close()
}

D'un autre côté, vous avez raison de dire qu'avec la récupération unidiomatique, il y a peu d'opportunités d'appliquer des fonctionnalités destinées à prendre en charge la gestion des erreurs idiomatiques. Cependant, séparer la récupération de l'opération Close conduit à un code IMO un peu plus propre.

func writeToGS(ctx context.Context, bucket, dst string, r io.Reader) (err error) {
        client := storage.NewClient(ctx) =? err { return err }
        defer client.Close()

        w := client.Bucket(bucket).Object(dst).NewWriter(ctx)
        defer func() {
                if err != nil {
                        _ = w.CloseWithError(err)
                } else {
                        err = w.Close()
                }
        }()
        defer func() {
                recover() =? r {
                        err = fmt.Errorf("panic: %v", r)
                        panic(r)
                }
        }()

        io.Copy(w, r) =? err { return err }
        return nil
}

@jba le gestionnaire de report re-panics: il est là pour tenter de notifier le processus sur l'autre ordinateur afin qu'il ne commette pas accidentellement une mauvaise transaction (en supposant que cela soit toujours possible dans un état d'erreur potentiel). Si ce n'est pas assez courant, cela devrait probablement l'être. Je suis d'accord que la lecture/écriture/copie ne devrait pas paniquer, mais s'il y avait un autre code qui pourrait raisonnablement paniquer, pour une raison quelconque, nous serions de retour là où nous avons commencé.

@bcmills cette dernière révision a l'air mieux (même si vous avez retiré le =? , vraiment)

@jba :

_ = io.Copy(w, r) =? err { _ = w.CloseWithError(err); return err }
return w.Close()

cela ne couvre toujours pas le cas d'une panique chez le lecteur. Il s'agit certes d'un cas rare, mais assez important : appeler Close ici en cas de panique, c'est très mal.

Ce code est l'exception, pas la règle. Nous ne devrions pas concevoir des fonctionnalités pour faciliter l'écriture.

@jba : Je ne suis pas du tout d'accord dans ce cas. Il est important de gérer correctement les erreurs. Permettre aux cas simples d'être plus faciles encouragera encore moins les gens à penser à une gestion appropriée des erreurs. J'aimerais voir une approche qui, comme errd , rend la gestion des erreurs conservatrice trivialement facile, tout en nécessitant un certain effort pour assouplir les règles, pas quelque chose qui bouge même légèrement dans l'autre sens.

@jimmyfrasche : concernant ta simplification : tu as à peu près raison.

  • IsSentinel n'est pas indispensable, juste pratique et commun. Je l'ai laissé tomber, pour l'instant du moins.
  • L'erreur dans l'état est différente de l'erreur, donc votre API supprime cela. Ce n'est pas essentiel pour la compréhension, cependant.
  • Les gestionnaires peuvent être des fonctions, mais sont des interfaces principalement pour des raisons de performances. Je sais juste que bon nombre de personnes n'utiliseront pas le package s'il n'est pas optimisé. (voir certains des premiers commentaires sur errd dans ce numéro)
  • Le contexte est malheureux. AppEngine en a besoin, mais pas grand-chose d'autre, je pense. Je serais bien de retirer le soutien jusqu'à ce que les gens rechignent.

@mpvl J'essayais juste de le réduire à quelques petites choses afin qu'il soit plus facile de comprendre comment cela fonctionnait, comment l'utiliser et d'imaginer comment cela s'intégrerait avec le code que j'ai écrit.

@jimmyfrasche : compris, même si c'est bien si une API ne vous oblige pas à le faire. :)

@bcmills : Les gestionnaires ont plusieurs objectifs, par exemple, par ordre d'importance :

  • encapsuler une erreur
  • définir pour ignorer les erreurs (pour rendre cela explicite. Voir l'exemple)
  • erreurs de journal
  • métriques d'erreur

Encore une fois, par ordre d'importance, ceux-ci doivent être délimités par :

  • bloquer
  • ligne
  • paquet

Les erreurs par défaut sont juste là pour garantir plus facilement qu'une erreur est gérée quelque part,
mais je ne pourrais vivre qu'avec le niveau bloc. J'avais à l'origine une API avec Options au lieu de Handlers. Cela a entraîné une API plus grande et plus maladroite en plus d'être plus lente, cependant.

Je ne vois pas le problème de rappel être si grave ici. Les utilisateurs définissent un Runner en lui passant un Handler qui est appelé en cas d'erreur. Le coureur spécifique est explicitement spécifié dans le bloc où les erreurs sont gérées. Dans de nombreux cas, un gestionnaire sera simplement un littéral de chaîne enveloppé transmis en ligne. Je vais jouer un peu pour voir ce qui est utile et ce qui ne l'est pas.

BTW, si nous ne devons pas encourager les erreurs de journalisation dans les gestionnaires, la prise en charge du contexte peut probablement être abandonnée.

@jba :

Aussi, je me demande si la récupération devrait même être là. Si w.Write ou r.Read (ou io.Copy !) panique, il est probablement préférable d'arrêter.

writeToGS se termine toujours s'il y a une panique, comme il se doit (!!!), il s'assure simplement qu'il appelle CloseWithError avec une erreur non nulle. Si la panique n'est pas gérée, le defer est toujours appelé, mais avec err == nil, ce qui entraîne la matérialisation d'un fichier potentiellement corrompu sur Cloud Storage. La bonne chose à faire ici est d'appeler CloseWithError avec une erreur temporaire, puis de continuer la panique.

J'ai trouvé un tas d'exemples comme celui-ci dans le code Go. Traiter avec io.Pipes aboutit également souvent à un code un peu trop subtil. La gestion des erreurs n'est souvent pas aussi simple qu'il n'y paraît comme vous l'avez vu vous-même maintenant.

@bcmills

Je n'aime pas particulièrement l'utilisation de la récupération par @mpvl dans cet exemple : elle encourage l'utilisation de la panique par rapport au flux de contrôle idiomatique,

N'essayant pas du tout d'encourager l'utilisation de la panique. Notez que la panique est relancée juste après CloseWithError et ne modifie donc pas autrement le flux de contrôle. Une panique reste une panique.
Ne pas utiliser recover ici est une erreur, car une panique provoquera l'appel du defer avec une erreur nulle, signalant que ce qui a été écrit jusqu'à présent peut être validé.

Le seul argument assez valable pour ne pas utiliser recover ici est qu'il est très peu probable qu'une panique se produise, même pour un Reader arbitraire (le Reader est de type inconnu dans cet exemple pour une raison :) ).
Cependant, pour le code de production, c'est une position inacceptable. Surtout lors de la programmation à une échelle suffisamment grande, cela doit arriver parfois (la panique peut être causée par d'autres choses que des bogues dans le code).

BTW, notez que le package errd élimine le besoin pour l'utilisateur d'y penser. Tout autre mécanisme qui signale une erreur en cas de panique au report est correct, cependant. Ne pas appeler en cas de panique fonctionnerait aussi, mais cela a ses propres problèmes.

Au moment de la déclaration, il n'y a actuellement aucun moyen de faire la distinction entre « valeur et erreur » et « valeur ou erreur » :

@bcmills Oh, je vois. Pour ouvrir une autre boîte de bikesheds, je suppose que vous pourriez dire

func Atoi(string) ?int

à la place de

func Atoi(string) (int, error)

mais WriteString resterait inchangé :

func WriteString(Writer, String) (int, error)

J'aime mieux la proposition =? / =! / :=? / :=! de @bcmills / @jba que des propositions similaires. Il a de belles propriétés :

  • composable (vous pouvez utiliser =? dans un bloc =? )
  • général (ne se soucie que de la valeur zéro, pas spécifique au type d'erreur)
  • portée améliorée
  • pourrait fonctionner avec différer (dans une variante ci-dessus)

Il a également des propriétés que je ne trouve pas aussi agréables.

Nids de composition. L'utilisation répétée va continuer à indenter de plus en plus à droite. Ce n'est pas nécessairement une mauvaise chose en soi, mais j'imagine que dans des situations avec une gestion des erreurs très compliquée qui nécessite de traiter des erreurs provoquant des erreurs provoquant des erreurs que le code pour y faire face deviendrait rapidement beaucoup moins clair que le statu quo actuel. Dans une telle situation, on pourrait utiliser =? pour l'erreur la plus externe et if err != nil pour les erreurs internes, mais cela a-t-il vraiment amélioré la gestion des erreurs en général ou simplement dans le cas courant ? Peut-être qu'il suffit d'améliorer le cas commun, mais je ne le trouve pas convaincant, personnellement.

Il introduit la fausseté dans la langue pour gagner sa généralité. La fausseté étant définie comme "n'est (pas) la valeur zéro" est parfaitement raisonnable, mais if err != nil { vaut mieux que if err { car c'est explicite, à mon avis. Je m'attendrais à voir des contorsions dans la nature qui essaient d'utiliser =? /etc. sur un flux de contrôle plus naturel pour essayer d'accéder à sa fausseté. Ce serait certainement unidiomatique et mal vu, mais cela arriverait. Alors que l'abus potentiel d'une fonctionnalité n'est pas en soi un argument contre une fonctionnalité, c'est quelque chose à considérer.

La portée améliorée (pour les variantes qui déclarent leur paramètre) est agréable dans certains cas, mais si la portée doit être corrigée, corrigez la portée en général.

La sémantique du "seul résultat le plus à droite" a du sens mais me semble un peu étrange. C'est plus un sentiment qu'un argument.

Cette proposition ajoute de la brièveté à la langue mais aucune puissance supplémentaire. Il pourrait être entièrement implémenté en tant que préprocesseur qui effectue une expansion de macro. Ce serait bien sûr indésirable : cela compliquerait les constructions et fragmenterait le développement et tout préprocesseur de ce type serait extrêmement compliqué car il doit être sensible au type et hygiénique. Je n'essaie pas d'ignorer en disant "juste faire un préprocesseur". Je soulève cette question uniquement pour souligner que cette proposition est entièrement du sucre. Il ne vous permet pas de faire quoi que ce soit que vous ne puissiez pas faire dans Go now ; il vous permet simplement de l'écrire de manière plus compacte. Je ne suis pas dogmatiquement opposé au sucre. Il y a du pouvoir dans une abstraction linguistique soigneusement choisie, mais le fait qu'il s'agisse de sucre signifie qu'il doit être considéré 👎 jusqu'à preuve du contraire, pour ainsi dire.

Les lhs des opérateurs sont une instruction mais un sous-ensemble très limité d'instructions. Les éléments à inclure dans ce sous-ensemble sont assez évidents, mais, à tout le moins, cela nécessiterait de refactoriser la grammaire dans la spécification du langage pour s'adapter au changement.

Est-ce que quelque chose comme

func F() (S, T, error)

func MustF() (S, T) {
  return F() =? err { panic(err) }
}

être autorisé?

Si

defer f.Close() :=? err {
    return err
}

est autorisé qui doit être (d'une manière ou d'une autre) équivalent à

func theOuterFunc() (err error) {
  //...
  defer func() {
    if err2 := f.Close(); err2 != nil {
      err = err2
    }
  }()
  //...
}

ce qui semble profondément problématique et susceptible de provoquer des situations très déroutantes, même en ignorant que d'une manière très peu conforme, cela masque les implications en termes de performances de l'attribution implicite d'une fermeture. L'alternative est d'avoir return retour de la fermeture implicite, puis d'avoir un message d'erreur indiquant que vous ne pouvez pas retourner une valeur de type error partir d'un func() qui est un peu obtus.

Vraiment, à part un correctif de portée légèrement amélioré, cela ne résout aucun des problèmes auxquels je suis confronté en cas d'erreur dans Go. Tout au plus, taper if err != nil { return err } est une nuisance, modulo les légers problèmes de lisibilité que j'ai exprimés dans #21182. Les deux plus gros problèmes sont

  1. réfléchir à la façon de gérer l'erreur - et il n'y a rien qu'un langage puisse faire à ce sujet
  2. l'introspection d'une erreur pour déterminer ce qu'il faut faire dans certaines situations — une convention supplémentaire avec le support du package errors irait un long chemin ici, bien qu'elles ne puissent pas résoudre tous les problèmes.

Je me rends compte que ce ne sont pas les seuls problèmes et que beaucoup trouvent d'autres aspects plus immédiatement préoccupants, mais ce sont ceux avec lesquels je passe le plus de temps et que je trouve plus vexants et gênants qu'autre chose.

Une meilleure analyse statique pour détecter quand j'ai foiré quelque chose serait toujours appréciée, bien sûr (et en général, pas seulement ce scénario). Des changements de langue et des conventions facilitant l'analyse de la source afin qu'elles soient plus utiles seraient également intéressants.

Je viens d'écrire beaucoup (BEAUCOUP ! désolé !) à ce sujet mais je ne rejette pas la proposition. Je pense qu'il a du mérite, mais je ne suis pas convaincu qu'il franchisse la barre ou tire son poids.

@jimmyfrasche

Je me souviens quand le comportement actuel de := a été introduit—une grande partie de ce fil sur go nut† était à la demande d'un moyen d'annoter explicitement les noms à réutiliser au lieu de l'implicite "réutiliser uniquement si cette variable existe exactement dans la portée actuelle " c'est là que se manifestent tous les problèmes subtils et difficiles à voir, d'après mon expérience.

Je ne trouve pas ce fil, est-ce que quelqu'un a un lien ?

Je pense que vous devez vous souvenir d'un fil différent, à moins que vous n'ayez été impliqué dans Go lors de sa sortie. La spécification du 09/11/2009, juste avant sa sortie, a :

Contrairement aux déclarations de variables normales, une déclaration de variable courte peut redéclarer des variables à condition qu'elles aient été déclarées à l'origine dans le même bloc avec le même type, et qu'au moins une des variables non vides soit nouvelle.

Je me souviens avoir vu cela en lisant la spécification pour la première fois et en pensant que c'était une excellente règle, car j'avais déjà utilisé un langage avec := mais sans cette règle de réutilisation, et penser à de nouveaux noms pour la même chose était fastidieux.

@mpvl
Je pense que la subtilité de votre exemple original est plus le résultat de la
l'API que vous utilisez là-bas que de la gestion des erreurs de Go elle-même.

C'est un exemple intéressant cependant, en particulier en raison du fait que
vous ne voulez pas fermer le fichier normalement si vous paniquez, alors le
L'idiome normal "defer w.Close()" ne fonctionne pas.

Si vous n'aviez pas besoin d'éviter d'appeler Close lorsqu'il y a un
panique, alors tu pourrais faire :

func writeToGS(ctx context.Context, bucket, dst string, r io.Reader) (err error) {
    client, err := storage.NewClient(ctx)
    if err != nil {
        return err
    }
    defer client.Close()

    w := client.Bucket(bucket).Object(dst).NewWriter(ctx)
    defer w.Close()
    _, err = io.Copy(w, r)
    if err != nil {
        w.CloseWithError(err)
    }
    return err
}

en supposant que la sémantique a été modifiée de telle sorte que l'appel à Close
après avoir appelé CloseWithError est un no-op.

Je pense que ça n'a plus l'air si mal.

Même avec l'exigence que le fichier ne soit pas écrit sans erreur en cas de panique, cela ne devrait pas être trop difficile à gérer ; par exemple en ajoutant une fonction Finalize qui doit être appelée explicitement avant Close.

    w := client.Bucket(bucket).Object(dst).NewWriter(ctx)
    defer w.Close()
    _, err = io.Copy(w, r)
    return w.Finalize(err)

Cela ne peut cependant pas attacher le message d'erreur de panique, mais une journalisation décente pourrait rendre cela plus clair.
(la méthode Close pourrait même contenir un appel de récupération, bien que je ne sois pas sûr que ce soit
en fait une très mauvaise idée...)

Cependant, je pense que l'aspect de récupération de panique de cet exemple est en quelque sorte un faux-fuyant dans ce contexte, car plus de 99 % des cas de gestion d'erreurs ne font pas de récupération de panique.

@rogpeppe :

Cela ne peut cependant pas attacher le message d'erreur de panique, mais une journalisation décente pourrait rendre cela plus clair.

Je ne pense pas que ce soit un problème.

Le changement d'API que vous proposez atténue mais ne résout pas encore complètement le problème. La sémantique requise nécessite également qu'un autre code se comporte correctement. Considérez l'exemple ci-dessous :

r, w := io.Pipe()
go func() {
    var err error                // used to intercept downstream errors
    defer func() {
        w.CloseWithError(err)
    }()

    r, err := newReader()
    if err != nil {
        return
    }
    defer func() {
        if errC := r.Close(); errC != nil && err == nil {
            err = errC
        }
    }
    _, err = io.Copy(w, r)
}()
return r

À lui seul, ce code montre que la gestion des erreurs peut être délicate ou au moins désordonnée (et je serais curieux de savoir comment cela pourrait être amélioré avec les autres propositions) : il transmet furtivement les erreurs en aval à travers une variable et a un peu trop une instruction if maladroite pour s'assurer que la bonne erreur est transmise. Les deux détournent trop de la « logique métier ». La gestion des erreurs domine le code. Et cet exemple ne gère même pas encore les paniques.

Pour être complet, dans errd cela _gérerait_ correctement les paniques et ressemblerait à :

r, w := io.Pipe()
go errd.Run(func(e *errd.E) {
    e.Defer(w.CloseWithError)

    r, err := newReader()
    e.Must(err)
    e.Defer(r.Close)

    _, err = io.Copy(w, r)
    e.Must(err)
})
return r

Si le lecteur ci-dessus (n'utilisant pas errd ) est passé en tant que lecteur à writeToGS et que le io.Reader renvoyé par newReader panique, cela entraînerait toujours une sémantique défectueuse avec le correctif d'API proposé (pourrait se précipiter pour fermer avec succès le fichier GS après la fermeture du tuyau en cas de panique avec une erreur nulle.)

Cela prouve également le point. Il n'est pas trivial de raisonner sur la gestion appropriée des erreurs dans Go. Quand j'ai regardé à quoi ressemblerait le code en le réécrivant avec errd , j'ai trouvé un tas de code bogué. Je n'ai vraiment appris à quel point il était difficile et subtil d'écrire une gestion appropriée des erreurs idiomatiques de Go, cependant, lors de l'écriture des tests unitaires pour le package errd . :)

Une alternative à votre changement d'API proposé serait de ne gérer aucun report en cas de panique. Cela a ses propres problèmes et ne résoudrait pas complètement le problème et est probablement infaisable, mais aurait de belles qualités.

Quoi qu'il en soit, le mieux serait un changement de langage qui atténue les subtilités de la gestion des erreurs, plutôt qu'un changement qui se concentre sur la brièveté.

@mpvl
Je trouve souvent avec le code de gestion des erreurs dans Go que la création d'une autre fonction peut nettoyer les choses. J'écrirais votre code ci-dessus quelque chose comme ceci:

func something() {
    r, w := io.Pipe()
    go func() {
        err := copyFromNewReader(w)
        w.CloseWithError(err)
    }()
    ...
}

func copyFromNewReader(w io.Writer) error {
    r, err := newReader()
    if err != nil {
        return err
    }
    defer r.Close()
    _, err = io.Copy(w, r)
    return err
}()

Je suppose que r.Close ne renvoie pas une erreur utile - si vous avez lu tout le long d'un lecteur et que vous venez de rencontrer uniquement io.EOF, alors cela n'a presque pas d'importance s'il renvoie une erreur à la fermeture.

Je n'aime pas l'API errd - elle est trop sensible au démarrage des goroutines. Par exemple : https://play.golang.org/p/iT441gO5us Que doSomething démarre ou non une goroutine pour exécuter le
La fonction in ne devrait pas affecter l'exactitude du programme, mais lorsque vous utilisez errd, c'est le cas. Vous vous attendez à ce que les paniques traversent en toute sécurité les limites de l'abstraction, et elles ne le font pas dans Go.

@mpvl

différer w.CloseWithError(err)

BTW, cette ligne appelle toujours CloseWithError avec une valeur d'erreur nulle. Je pense que tu voulais
écrivez:

defer func() { 
   w.CloseWithError(err)
}()

@mpvl

Notez que l'erreur renvoyée par la méthode Close sur un io.Reader n'est presque jamais utile (voir la liste dans https://github.com/golang/go/issues/20803#issuecomment-312318808 ).

Cela suggère que nous devrions écrire votre exemple aujourd'hui comme suit :

r, w := io.Pipe()
go func() (err error) {
    defer func() { w.CloseWithError(err) }()

    r, err := newReader()
    if err != nil {
        return err
    }
    defer r.Close()

    _, err = io.Copy(w, r)
    return err
}()
return r

... ce qui me semble parfaitement bien, en plus d'être un peu bavard.

Il est vrai qu'il transmet une erreur nulle à w.CloseWithError en cas de panique, mais tout le programme se termine quand même à ce stade. S'il est important de ne jamais fermer avec une erreur nulle, il s'agit d'un simple renommer plus une ligne supplémentaire :

-go func() (err error) {
-   defer func() { w.CloseWithError(err) }()
+go func() (rerr error) {
+   rerr = errors.New("goroutine exited by panic")
+   defer func() { w.CloseWithError(rerr) }()

@rogpeppe : en effet, merci. :)

Oui, je suis au courant du problème de la goroutine. C'est méchant, mais probablement quelque chose qui n'est pas difficile à attraper avec un contrôle vétérinaire. Quoi qu'il en soit, je ne vois pas errd comme une solution finale mais plutôt comme un moyen d'acquérir de l'expérience sur la meilleure façon de gérer les erreurs. Idéalement, il y aurait un changement de langue qui résoudrait les mêmes problèmes, mais avec les restrictions appropriées imposées.

Vous vous attendez à ce que les paniques traversent en toute sécurité les limites de l'abstraction, et elles ne le font pas dans Go.

Ce n'est pas ce à quoi je m'attends. Dans ce cas, je m'attends à ce que les API ne signalent pas de succès alors qu'il n'y en avait pas. Votre dernier morceau de code le gère correctement, car il n'utilise pas de report pour l'écrivain. Mais c'est très subtil. De nombreux utilisateurs utiliseraient le report dans ce cas car il est considéré comme idiomatique.

Peut-être qu'un ensemble de contrôles vétérinaires pourrait détecter les utilisations problématiques des reports. Pourtant, à la fois dans le code "idiomatique" d'origine et dans votre dernier morceau remanié, il y a beaucoup de bricolage pour contourner les subtilités de la gestion des erreurs pour quelque chose qui est par ailleurs un morceau de code assez simple. Le code de contournement n'est pas destiné à déterminer comment gérer certains cas d'erreur, c'est purement un gaspillage de cycles cérébraux qui pourraient être utilisés à des fins productives.

Plus précisément, ce que j'essaie d'apprendre de errd est de savoir si cela rend la gestion des erreurs plus simple lorsqu'il est utilisé directement. D'après ce que je peux voir, de nombreuses complications et subtilités s'évanouissent. Il serait bon de voir si nous pouvons codifier des aspects de sa sémantique en de nouvelles fonctionnalités linguistiques.

@jimmyfrasche

Il introduit la fausseté dans la langue pour gagner sa généralité.

C'est un très bon point. Les problèmes habituels avec la fausseté viennent de l'oubli d'invoquer une fonction booléenne ou de l'oubli de déréférencer un pointeur à nil.

Nous pourrions résoudre ce dernier problème en définissant l'opérateur pour qu'il ne fonctionne qu'avec des types nillables (et probablement en supprimant =! en conséquence, car ce serait la plupart du temps inutile).

Nous pourrions résoudre le premier en le restreignant davantage pour qu'il ne fonctionne pas avec les types de fonction, ou pour ne fonctionner qu'avec les types de pointeur ou d'interface : alors il serait clair que la variable n'est pas un booléen, et les tentatives de l'utiliser pour des comparaisons booléennes seraient plus évidemment faux.

Est-ce que quelque chose comme [ MustF ] serait autorisé ?

Oui.

Si [ defer f.Close() :=? err { ] est autorisé, cela doit être (d'une manière ou d'une autre) équivalent à
[ defer func() { … }() ].

Pas forcément, non. Il pourrait avoir sa propre sémantique (plutôt comme call/cc qu'une fonction anonyme). Je n'ai pas proposé de changement de spécification pour l'utilisation de =? dans defer (cela nécessiterait au moins un changement de grammaire), donc je ne sais pas exactement à quel point une telle définition serait compliquée .

Les deux plus gros problèmes sont […] 2. l'introspection d'une erreur pour déterminer ce qu'il faut faire dans certaines situations

Je conviens que c'est un problème plus important dans la pratique, mais cela semble plus ou moins orthogonal à ce problème (qui concerne davantage la réduction du passe-partout et le potentiel d'erreurs associé).

( @rogpeppe , @davecheney , @dsnet , @crawshaw , moi et quelques autres que j'oublie sûrement eu une bonne discussion à GopherCon sur les API pour inspecter les erreurs, et j'espère que nous verrons de bonnes propositions sur ce front aussi , mais je pense vraiment que c'est une question pour un autre problème.)

@bcmills : ce code a deux problèmes 1) comme mentionné par @rogpeppe : l'erreur passée à CloseWithError est toujours nulle, et 2) il ne gère toujours pas les paniques, ce qui signifie que l'API signalera explicitement le succès en cas de panique (le r renvoyé peut émettre un io.EOF même si tous les octets n'ont pas été écrits), même si 1 est fixé.

Sinon, je suis d'accord pour dire que l'erreur renvoyée par Close peut souvent être ignorée. Pas toujours, cependant (voir le premier exemple).

Je trouve quelque peu surprenant que 4 ou 5 suggestions erronées aient été faites sur mes exemples plutôt simples (y compris une de moi-même) et j'ai toujours l'impression de devoir affirmer que la gestion des erreurs dans Go n'est pas triviale. :)

@bcmills :

Il est vrai qu'il transmet une erreur nulle à w.CloseWithError en cas de panique, mais l'ensemble du programme se termine quand même à ce stade.

Est-ce que c'est? Les reports de cette goroutine sont toujours appelés. Pour autant que je sache, ils seront exécutés jusqu'à la fin. Dans ce cas, le Close signalera un io.EOF.

Voir, par exemple, https://play.golang.org/p/5CFbsAe8zF. Après que la goroutine ait paniqué, elle passe toujours joyeusement "foo" à l'autre goroutine qui l'oblige ensuite à l'écrire sur Stdout.

De même, un autre code peut recevoir un io.EOF incorrect d'une goroutine en panique (comme celui de votre exemple), conclure avec succès et valider un fichier dans GS avant que la goroutine en panique ne reprenne sa panique.

Votre argument suivant peut être : n'écrivez pas de code bogué, mais :

  • puis facilitez la prévention de ces bogues, et
  • les paniques peuvent être causées par des facteurs externes, tels que les MOO.

S'il est important de ne jamais fermer avec une erreur nulle, il s'agit d'un simple renommer plus une ligne supplémentaire :

Il devrait toujours se fermer avec nil pour signaler io.EOF quand il est terminé, donc cela ne fonctionnera pas.

S'il est important de ne jamais fermer avec une erreur nulle, il s'agit d'un simple renommer plus une ligne supplémentaire :

Il devrait toujours se fermer avec nil pour signaler io.EOF quand il est terminé, donc cela ne fonctionnera pas.

Pourquoi pas? Le return err à la fin définira rerr à nil .

@bcmills : ah je vois ce que tu veux dire maintenant. Oui, cela devrait fonctionner. Je ne suis pas inquiet du nombre de lignes, cependant, mais plutôt de la subtilité du code.

Je trouve que cela fait partie de la même catégorie de problèmes que l'ombrage variable, juste moins susceptible de se heurter (ce qui l'aggrave peut-être.) La plupart des bogues d'ombrage variable que vous rencontrerez avec de bons tests unitaires. Les paniques arbitraires sont plus difficiles à détecter.

Lorsque vous travaillez à grande échelle, il est à peu près garanti que vous verrez des bogues comme celui-ci se manifester. Je suis peut-être paranoïaque, mais j'ai vu des scénarios beaucoup moins probables entraîner une perte et une corruption de données. Normalement, c'est bien, mais pas pour le traitement des transactions (comme l'écriture de fichiers gs.)

J'espère que cela ne vous dérange pas que je détourne votre proposition avec une syntaxe alternative - que pensent les gens de quelque chose comme ça :

return err if f, err := os.Open("..."); err != nil

@SirCmpwn Cela enterre le leader. La chose la plus facile à lire dans une fonction devrait être le flux de contrôle normal, pas la gestion des erreurs.

C'est juste, mais votre proposition me met également mal à l'aise - elle introduit une syntaxe opaque (||) qui se comporte différemment de la façon dont les utilisateurs ont été formés pour s'y attendre || se comporter. Je ne sais pas quelle est la bonne solution, je vais y réfléchir un peu plus.

@SirCmpwn Oui, comme je l'ai dit dans le message d'origine "

Compris.

C'est un peu plus radical, mais peut-être qu'une approche macro-conduite fonctionnerait mieux.

f = try!(os.Open("..."))

try! mangerait la dernière valeur du tuple et la renverrait si elle n'est pas nulle, et sinon renverrait le reste du tuple.

Je voudrais suggérer que notre énoncé de problème est,

La gestion des erreurs dans Go est verbeuse et répétitive. Le format idiomatique de la gestion des erreurs de Go rend plus difficile la visualisation du flux de contrôle sans erreur et la verbosité est peu attrayante, en particulier pour les nouveaux arrivants. À ce jour, les solutions proposées pour ce problème nécessitent généralement des fonctions artisanales de gestion des erreurs ponctuelles, réduisent la localisation de la gestion des erreurs et augmentent la complexité. Étant donné que l'un des objectifs de Go est de forcer le rédacteur à envisager la gestion des erreurs et la récupération, toute amélioration de la gestion des erreurs doit également s'appuyer sur cet objectif.

Pour résoudre cet énoncé de problème, je propose ces objectifs d'amélioration de la gestion des erreurs dans Go 2.x :

  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.

Évaluation de cette proposition :

f.Close() =? err { return fmt.Errorf(…, err) }

d'après ces buts, je conclurais qu'il réussit bien le but #1. Je ne sais pas comment cela aide avec #2, mais cela ne rend pas l'ajout de contexte moins probable non plus (ma propre proposition partage cette faiblesse sur #2). Il ne réussit pas vraiment aux 3 et 4, cependant :
1) Comme d'autres l'ont dit, la vérification et l'affectation des valeurs d'erreur sont opaques et inhabituelles ; et
2) La syntaxe =? est également inhabituelle. C'est particulièrement déroutant s'il est combiné avec la syntaxe =! similaire mais différente. Il faudra un certain temps pour que les gens s'habituent à leurs significations ; et
3) Le renvoi d'une valeur valide avec l'erreur est suffisamment courant pour que toute nouvelle solution gère également ce cas.

Faire de l'erreur de manipulation un bloc peut être une bonne idée, cependant, si, comme d'autres l'ont suggéré, cela est combiné avec des modifications apportées à gofmt . Par rapport à ma proposition, cela améliore la généralité, ce qui devrait aider avec l'objectif n°4 et la familiarité qui aide l'objectif n°3 au prix d'un sacrifice de brièveté pour le cas courant consistant simplement à renvoyer l'erreur avec un contexte supplémentaire.

Si vous m'aviez demandé dans l'abstrait, j'aurais peut-être convenu qu'une solution plus générale serait préférable à une solution spécifique de gestion des erreurs tant qu'elle répondait aux objectifs d'amélioration de la gestion des erreurs ci-dessus. Maintenant, cependant, après avoir lu cette discussion et y avoir réfléchi davantage, je suis enclin à croire qu'une solution spécifique de gestion des erreurs se traduira par une plus grande clarté et simplicité. Alors que les erreurs dans Go ne sont que des valeurs, la gestion des erreurs constitue une partie si importante de toute programmation que le fait d'avoir une syntaxe spécifique pour rendre le code de gestion des erreurs clair et concis semble approprié. Je crains que nous ne rendions un problème déjà difficile (proposer une solution propre pour la gestion des erreurs) encore plus difficile et plus compliqué si nous le confondons avec d'autres objectifs tels que la portée et la composabilité.

Quoi qu'il en soit, comme le souligne @rsc dans son article, Toward Go 2 , ni l'énoncé du problème, ni les objectifs ni aucune proposition de syntaxe ne sont susceptibles d'avancer sans rapports d'expérience démontrant que le problème est important. Peut-être qu'au lieu de débattre de diverses propositions de syntaxe, devrions-nous commencer à chercher des données à l'appui ?

Quoi qu'il en soit, comme le souligne @rsc dans son article, Toward Go 2, ni l'énoncé du problème, ni les objectifs ni aucune proposition de syntaxe ne sont susceptibles d'avancer sans rapports d'expérience démontrant que le problème est important. Peut-être qu'au lieu de débattre de diverses propositions de syntaxe, devrions-nous commencer à chercher des données à l'appui ?

Je pense que cela va de soi si l'on suppose que l'ergonomie est importante. Ouvrez n'importe quelle base de code Go et recherchez les endroits où il existe des opportunités de SÉCHER les choses et/ou d'améliorer l'ergonomie que le langage peut résoudre - pour le moment, la gestion des erreurs est une valeur aberrante. Je pense que l'approche Toward Go 2 peut préconiser à tort de ne pas tenir compte des problèmes qui ont des solutions de contournement - dans ce cas, les gens se contentent de sourire et de le supporter.

if $val, err := $operation($args); err != nil {
  return err
}

Quand il y a plus de passe-partout que de code, le problème est évident à mon humble avis.

@billyh

J'ai l'impression que le format : f.Close() =? err { return fmt.Errorf(…, err) } est trop verbeux et déroutant. Personnellement, je ne pense pas que la partie erreur devrait être dans un bloc. Inévitablement, cela conduirait à l'étaler sur 3 lignes au lieu de 1. De plus, dans le cas où vous devez faire plus que simplement modifier une erreur avant de la renvoyer, on peut simplement utiliser le if err != nil { ... } actuel

L'opérateur =? est également un peu déroutant. Ce qui s'y passe n'est pas immédiatement évident.

Avec quelque chose comme ça :
file := os.Open("/some/file") or raise(err) errors.Wrap(err, "extra context")
ou le raccourci :
file := os.Open("/some/file") or raise
et le différé :
defer f.Close() or raise(err2) errors.ReplaceIfNil(err, err2)
est un peu plus bavard et le choix du mot pourrait réduire la confusion initiale (c. -à- personnes pourraient immédiatement associé raise avec un mot clé similaire d'autres langues comme python, ou tout simplement déduire cette augmentation soulève l'erreur / dernière non valeur par défaut dans la pile jusqu'à l'appelant).

C'est aussi une bonne solution impérative, qui n'essaie pas de résoudre toutes les erreurs obscures possibles sous le soleil. De loin, la plus grande partie de la gestion des erreurs dans la nature est de la nature mentionnée ci-dessus. Pour la suite, la syntaxe actuelle est aussi là pour aider.

Éditer:
Si nous voulons réduire un peu la "magie", les exemples précédents pourraient également ressembler à :
file, err := os.Open("/some/file") or raise errors.Wrap(err, "extra context")
file, err := os.Open("/some/file") or raise err
defer err2 := f.Close() or errors.ReplaceIfNil(err, err2)
Personnellement, je pense que les exemples précédents sont meilleurs, car ils déplacent la gestion complète des erreurs vers la droite, au lieu de la diviser comme c'est le cas ici. C'est peut-être plus clair quand même.

Je voudrais suggérer que notre énoncé de problème est, ...

Je ne suis pas d'accord avec l'énoncé du problème. J'aimerais proposer une alternative :


La gestion des erreurs n'existe pas du point de vue du langage. La seule chose que Go fournit est un type d'erreur prédéclaré et même cela est juste pour plus de commodité car il ne permet rien de vraiment nouveau. Les erreurs ne sont que des valeurs . La gestion des erreurs n'est qu'un code utilisateur normal. Il n'y a rien de spécial à ce sujet dans le POV de la langue et il ne devrait pas y avoir quelque chose de spécial à ce sujet. Le seul problème avec la gestion des erreurs est que certaines personnes pensent que cette précieuse et belle simplicité doit être éliminée à tout prix.

Dans le sens de ce que dit Cznic, ce serait bien d'avoir une solution utile pour plus que la simple gestion des erreurs.

Une façon de rendre la gestion des erreurs plus générale est de l'envisager en termes de types union/sum-types et de dépliage. Swift et Rust ont tous deux des solutions avec ? ! syntaxe, bien que je pense que celle de Rust a été un peu instable.

Si nous ne voulons pas faire des types de somme un concept de haut niveau, nous pourrions en faire simplement une partie du retour multiple, de la même manière que les tuples ne font pas vraiment partie de Go, mais vous pouvez toujours faire un retour multiple.

Un coup de pinceau à la syntaxe inspirée de Swift :

func Failable() (*Thingie | error) {
    ...
}

guard thingie, err := Failable() else { 
    return wrap(err, "Could not make thingie)
}
// err is not in scope here

Vous pouvez également l'utiliser pour d'autres choses, comme :

guard val := myMap[key] else { val = "default" }

La solution =? proposée par @bcmills et @jba n'est pas seulement pour l'erreur, le concept est pour non nul. cet exemple fonctionnera normalement.

func Foo()(Bar, Recover){}
bar := Foo() =? recover { log.Println("[Info] Recovered:", recover)}

L'idée principale de cette proposition réside dans les notes annexes, qui séparent l'objectif principal du code et mettent de côté les cas secondaires, afin d'en faciliter la lecture.
Pour moi la lecture d'un code Go, dans certains cas, n'est pas continue, plusieurs fois tu as l'arrêt de l'idée avec if err!= nil {return err} , donc l'idée des notes secondaires me semble intéressante, comme dans un livre que l'on lit l'idée principale continuellement, puis lisez les notes annexes. ( @jba parle )
Dans de très rares situations, l'erreur est l'objectif principal d'une fonction, peut-être lors d'une récupération. Normalement, lorsque nous avons une erreur, nous ajoutons du contexte, un journal et un retour, dans ces cas, des notes annexes peuvent rendre votre code plus lisible.
Je ne sais pas si c'est la meilleure syntaxe, en particulier je n'aime pas le bloc dans la deuxième partie, une note latérale doit être petite, une ligne devrait suffire

bar := Foo() =? recover: log.Println("[Info] Recovered:", recover)

@billyh

  1. Comme d'autres l'ont dit, la vérification et l'affectation des valeurs d'erreur sont opaques et inhabituelles ; et

S'il vous plaît soyez plus concret : "opaque et inhabituel" sont terriblement subjectifs. Pouvez-vous donner quelques exemples de code où vous pensez que la proposition pourrait prêter à confusion ?

  1. Le =? la syntaxe est également inhabituelle. […]

OMI c'est une fonctionnalité. Si quelqu'un voit un opérateur inhabituel, je soupçonne qu'il est plus enclin à rechercher ce qu'il fait au lieu de simplement supposer quelque chose qui peut ou non être exact.

  1. Le renvoi d'une valeur valide avec l'erreur est suffisamment courant pour que toute nouvelle solution gère également ce cas.

Cela fait?

Lisez attentivement la proposition : =? effectue les affectations avant d'évaluer le Block , il peut donc également être utilisé dans ce cas :

n := r.Read(buf) =? err {
  if err == io.EOF {
    […]
  }
  return err
}

Et comme l' a noté

Peut-être qu'au lieu de débattre de diverses propositions de syntaxe, devrions-nous commencer à chercher des données à l'appui ?

Voir les nombreux problèmes (et leurs exemples) que Ian a liés dans le message d'origine.
Voir aussi https://github.com/golang/go/wiki/ExperienceReports#error -handling.

Si vous avez obtenu des informations spécifiques à partir de ces rapports, veuillez les partager.

@urandom

Personnellement, je ne pense pas que la partie erreur devrait être dans un bloc. Forcément, cela conduirait à l'étaler en 3 lignes au lieu de 1.

Le but du bloc est double :

  1. fournir une rupture visuelle et grammaticale claire entre l'expression produisant l'erreur et son gestionnaire, et
  2. pour permettre une plus large gamme de gestion des erreurs (conformément à l'objectif déclaré de @ianlancetaylor dans le message d'origine).

3 lignes contre 1 n'est même pas un changement de langue : si le nombre de lignes est votre plus grande préoccupation, nous pourrions résoudre ce problème en modifiant simplement gofmt .

file, err := os.Open("/some/file") or raise errors.Wrap(err, "extra context")
file, err := os.Open("/some/file") or raise err

Nous avons déjà return et panic ; ajouter raise en plus de ceux-ci semble ajouter trop de façons de quitter une fonction pour trop peu de gain.

defer err2 := f.Close() or errors.ReplaceIfNil(err, err2)

errors.ReplaceIfNil(err, err2) nécessiterait une sémantique de passage par référence très inhabituelle.
Vous pouvez passer err par pointeur à la place, je suppose :

defer err2 := f.Close() or errors.ReplaceIfNil(&err, err2)

mais cela me semble toujours très étrange. Le jeton or construit-il une expression, une déclaration ou autre chose ? (Une proposition plus concrète aiderait.)

@carlmjohnson

Quelles seraient la syntaxe et la sémantique concrètes de votre instruction guard … else ? Pour moi, cela ressemble beaucoup à =? ou :: avec les jetons et les positions variables échangés. (Encore une fois, une proposition plus concrète serait utile : quelle est la syntaxe et la sémantique réelles que vous avez en tête ?)

@bcmills
L'hypothétique ReplaceIfNil serait un simple :

func ReplaceIfNil(original, replacement error) error {
   if original == nil {
       return replacement
   }
   return original
}

Rien d'inhabituel à cela. Peut-être le nom...

or serait un opérateur binaire, où l'opérande de gauche serait soit une IdentifierList, soit une PrimaryExpr. Dans le cas du premier, il est réduit à l'identifiant le plus à droite. Il permet ensuite d'exécuter l'opérande de droite si celui de gauche n'est pas une valeur par défaut.

C'est pourquoi j'avais besoin d'un autre jeton par la suite, pour faire la magie de renvoyer les valeurs par défaut, pour tout sauf le dernier paramètre de la fonction Result, qui prendrait la valeur de l'expression par la suite.
IIRC, il n'y a pas si longtemps, il y avait une autre proposition qui demanderait au langage d'ajouter un "..." ou quelque chose, qui remplacerait la fastidieuse initialisation de la valeur par défaut. Dans cette cause, le tout pourrait ressembler à ceci:

f, err := os.Open("/some/file") or return ..., errors.Wrap(err, "more context")

Quant au bloc, je comprends qu'il permet une manipulation plus large. Personnellement, je ne sais pas si la portée de cette proposition devrait être d'essayer de répondre à tous les scénarios possibles, par opposition à couvrir un hypothétique 80 %. Et je pense personnellement que le nombre de lignes qu'un résultat prendrait compte (bien que je n'aie jamais dit que c'était ma plus grande préoccupation, c'est en fait la lisibilité, ou le manque de lisibilité, lors de l'utilisation de jetons obscurs comme =?). Si cette nouvelle proposition s'étend sur plusieurs lignes dans le cas général, je ne vois personnellement pas ses avantages par rapport à quelque chose comme :

if f, err := os.Open("/some/file"); err != nil {
     return errors.Wrap(err, "more context")
}
  • si les variables définies ci-dessus devaient être rendues disponibles en dehors de la portée de if .
    Et cela rendrait toujours une fonction avec seulement quelques instructions de ce type plus difficile à lire, en raison du bruit visuel de ces blocs de gestion des erreurs. Et c'est l'une des plaintes que les gens ont lorsqu'ils discutent de la gestion des erreurs dans go.

@urandom

or serait un opérateur binaire, où l'opérande de gauche serait soit une IdentifierList, soit une PrimaryExpr. […] Il permet alors d'exécuter l'opérande de droite si celui de gauche n'est pas une valeur par défaut.

Les opérateurs binaires de Go sont des expressions, pas des déclarations, donc faire de or un opérateur binaire soulèverait beaucoup de questions. (Quelle est la sémantique de or dans le cadre d'une expression plus large, et comment cela correspond-il aux exemples que vous avez publiés avec := ?)

En supposant qu'il s'agisse en fait d'un énoncé, quel est l'opérande de droite ? S'il s'agit d'une expression, quel est son type et raise peut-il être utilisé comme expression dans d'autres contextes ? S'il s'agit d'une déclaration, quelle est sa sémantique s'il y a autre chose qu'un raise ? Ou proposez-vous que or raise soit essentiellement une déclaration unique (par exemple, or raise comme alternative syntaxique à :: ou =? ) ?

Puis-je écrire

defer f.Close() or raise(err2) errors.ReplaceIfNil(err, err2) or raise(err3) Transform(err3)

?

Puis-je écrire

f(r.Read(buf) or raise err)

?

defer f.Close() or raise(err2) errors.ReplaceIfNil(err, err2) or raise(err3) Transform(err3)

Non, ce serait invalide à cause du deuxième raise . Si ce n'était pas le cas, alors toute la chaîne de transformation devrait passer et le résultat final devrait être renvoyé à l'appelant. Bien qu'une telle sémantique dans son ensemble ne soit probablement pas nécessaire, puisque vous pouvez simplement écrire :

defer f.Close() or raise(err2) Transform(errors.ReplaceIfNil(err, err2)


f(r.Read(buf) or raise err)

Si nous supposons mon commentaire d'origine - où ou prendrait la dernière valeur du côté gauche, de sorte que s'il s'agissait d'une valeur par défaut, l'expression finale serait évaluée par rapport au reste de la liste de résultats ; alors oui, cela devrait être valide. Dans ce cas, si r.Read renvoie une erreur, cette erreur est renvoyée à l'appelant. Sinon, n serait passé à f

Éditer:

À moins que je ne sois confus par les termes, je considère or comme un opérateur binaire, dont les opérandes doivent être du même type (mais un peu magique, si l'opérande de gauche est une liste de choses, et dans ce cas, il prend le dernier élément de ladite liste de choses). raise serait un opérateur unaire qui prend son opérande et retourne de la fonction, en utilisant la valeur de cet opérande comme valeur du dernier argument de retour, les précédents ayant des valeurs par défaut. Vous pouvez ensuite utiliser techniquement raise dans une instruction autonome, dans le but de revenir d'une fonction, alias return ..., err

Ce sera le cas idéal, mais je suis également d' or raise avec =? , tant qu'il accepte également une simple instruction au lieu d'un bloc, afin de couvrir la majorité des cas d'utilisation de manière moins verbeuse. Ou nous pouvons également utiliser une grammaire de type defer, où elle accepte une expression. Cela couvrirait la majorité des cas comme :

f := os.Open("/some/file") or raise(err) errors.Wrap(err, "with context")

et cas complexes :

f := os.Open or raise(err) func() {
     if err == io.EOF {
         […]
     }
  return err
}()

En réfléchissant un peu plus à ma proposition, je laisse tomber le peu sur les types union/somme. La syntaxe que je propose est

guard [ ASSIGNMENT || EXPRESSION ] else { [ BLOCK ] }

Dans le cas d'une expression, l'expression est évaluée et si le résultat n'est pas égal à true pour les expressions booléennes ou à la valeur vide pour les autres expressions, BLOCK est exécuté. Dans une affectation, la dernière valeur affectée est évaluée pour != true / != nil . Après une instruction de garde, toutes les affectations effectuées seront dans la portée (cela ne crée pas une nouvelle portée de bloc [sauf peut-être pour la dernière variable ?]).

Dans Swift, le BLOCK pour les instructions guard doit contenir l'un des return , break , continue , ou throw . Je n'ai pas décidé si j'aime ça ou pas. Cela semble ajouter de la valeur car un lecteur sait grâce au mot guard ce qui va suivre.

Quelqu'un suit-il suffisamment Swift pour dire si guard est bien considéré par cette communauté ?

Exemples:

guard f, err := os.Open("/some/file") else { return errors.Wrap(err, "could not open:") }

guard data, err := ioutil.ReadAll(f) else { return errors.Wrap(err, "could not read:") }

var obj interface{}

guard err = json.Unmarshal(data, &obj) else { return errors.Wrap(err, "could not unmarshal:") }

guard m, _ := obj.(map[string]interface{}) else { return errors.New("unexpected data format") }

guard val, _ := m["key"] else { return errors.New("missing key") }

À mon humble avis, tout le monde discute ici d'un trop grand nombre de problèmes à la fois, mais le schéma le plus courant en réalité est "l'erreur de retour telle quelle". Alors pourquoi ne pas aborder le plus gros problème avec qch comme :

code, err ?= fn()

ce qui signifie que la fonction doit retourner sur err != nil?

pour := opérateur nous pouvons introduire ?:=

code, err ?:= fn()

la situation avec ?:= semble être pire en raison de l'ombrage, puisque le compilateur devra passer la variable "err" à la même valeur de retour err nommée.

Je suis en fait assez excité que certaines personnes se concentrent sur le fait de faciliter l'écriture de code correct au lieu de simplement raccourcir le code incorrect.

Quelques notes:

Un "rapport d'expérience" intéressant d'un des concepteurs de Midori chez Microsoft sur les modèles d'erreur.

Je pense que certaines idées de ce document et Swift peuvent s'appliquer magnifiquement à Go2.

En introduisant un nouveau mot clé réservé throws , les fonctions peuvent être définies comme :

func Get() []byte throws {
  if (...) {
    raise errors.New("oops")
  }

  return []byte{...}
}

Essayer d'appeler cette fonction à partir d'une autre fonction non lancée entraînera une erreur de compilation, en raison d'une erreur jetable non gérée.
Au lieu de cela, nous devrions être en mesure de propager l'erreur, ce que tout le monde convient d'un cas courant, ou de la gérer.

func ScrapeDate() time.Time throws {
  body := Get() // compilation error, unhandled throwable
  body := try Get() // we've been explicit about potential throwable

  // ...
}

Pour les cas où nous savons qu'une méthode n'échouera pas, ou dans les tests, nous pouvons introduire try! similaire à swift.

func GetWillNotFail() time.Time {
  body := Get() // compilation error, throwable not handled
  body := try Get() // compilation error, throwable can not be propagated, because `GetWillNotFail` is not annotated with throws
  body := try! Get() // works, but will panic on throws != nil

  // ...
}

Pas sûr de ceux-ci cependant (similaire à Swift):

func main() {
  // 1:
  do {
    fmt.Printf("%v", try ScrapeDate())
  } catch err { // err contains caught throwable
    // ...
  }

  // 2:
  do {
    fmt.Printf("%v", try ScrapeDate())
  } catch err.(type) { // similar to a switch statement
    case error:
      // ...
    case io.EOF
      // ...
  }
}

ps1. valeurs de retour multiples func ReadRune() (ch Rune, size int) throws { ... }
ps2. nous pouvons revenir avec return try Get() ou return try! Get()
ps3. nous pouvons maintenant enchaîner des appels comme buffer.NewBuffer(try Get()) ou buffer.NewBuffer(try! Get())
ps4. Pas sûr des annotations (moyen facile d'écrire errors.Wrap(err, "context") )
ps5. ce sont en fait des exceptions
ps6. le plus gros gain est les erreurs de temps de compilation pour les exceptions ignorées

Les suggestions que vous écrivez sont décrites exactement dans le lien Midori avec tous les mauvais
côtés de celui-ci... Et une conséquence évidente des "jets" sera "les gens
déteste ça". Pourquoi devrait-on écrire « jets » à chaque fois pour la plupart des
les fonctions?

BTW, votre intention de forcer les erreurs à être vérifiées et non ignorées peut être
appliqué également aux types sans erreur et à mon humble avis, il est préférable d'avoir dans un plus
forme généralisée (par exemple gcc __attribute__((warn_unused_result))).

Quant à la forme de l'opérateur, je suggérerais soit une forme courte soit
forme de mot-clé comme ceci :

?= fn() OU check fn() -- propage l'erreur à l'appelant
!= fn() OU nofail fn() -- panique en cas d'erreur

Le samedi 26 août 2017 à 12h15, nvartolomei [email protected]
a écrit:

Quelques notes:

Un rapport d'expérience intéressant
http://joeduffyblog.com/2016/02/07/the-error-model/ de l'un des
concepteurs de Midori chez Microsoft sur les modèles d'erreur.

Je pense que quelques idées de ce document et Swift
https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/ErrorHandling.html
peut s'appliquer magnifiquement à Go2.

En introduisant un nouveau mot-clé throws réservé, les fonctions peuvent être définies comme :

func Get() []byte jette {
si (...) {
lever des erreurs.New("oops")
}

return []octet{...}
}

Essayer d'appeler cette fonction à partir d'une autre fonction de non-lancement
entraîner une erreur de compilation, en raison d'une erreur jetable non gérée.
Au lieu de cela, nous devrions être capables de propager l'erreur, ce que tout le monde s'accorde à dire est un
cas courant, ou le gérer.

func ScrapeDate() time.Time jette {
body := Get() // erreur de compilation, jetable non géré
body := try Get() // nous avons été explicites sur le potentiel jetable

// ...
}

Pour les cas où nous savons qu'une méthode n'échouera pas, ou dans les tests, nous pouvons
introduire essayer! semblable à rapide.

func GetWillNotFail() time.Time {
body := Get() // erreur de compilation, jetable non géré
body := try Get() // erreur de compilation, throwable ne peut pas être propagé, car GetWillNotFail n'est pas annoté avec des throws
corps := essayez ! Get() // fonctionne, mais paniquera aux lancers != nil

// ...
}

Pas sûr de ceux-ci cependant (similaire à Swift):

fonction principale() {
// 1:
faire {
fmt.Printf("%v", essayez ScrapeDate())
} catch err { // err contient attrapé jetable
// ...
}

// 2:
faire {
fmt.Printf("%v", essayez ScrapeDate())
} catch err.(type) { // similaire à une instruction switch
erreur de cas :
// ...
cas io.EOF
// ...
}
}

ps1. plusieurs valeurs de retour func ReadRune() (ch Rune, size int) renvoie {
... }
ps2. nous pouvons revenir avec return try Get() ou return try! Avoir()
ps3. nous pouvons maintenant enchaîner des appels comme buffer.NewBuffer(try Get()) ou buffer.NewBuffer(try!
Avoir())
ps4. Pas sûr des annotations

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

Je pense que l'opérateur proposé par @jba et @bcmills est une très bonne idée, bien qu'il soit mieux orthographié comme " ??" au lieu de "=?" OMI.

En regardant cet exemple :

func doStuff() (int,error) {
    x, err := f() 
    if err != nil {
        return 0, wrapError("f failed", err)
    }

    y, err := g(x)
    if err != nil {
        return 0, wrapError("g failed", err)
    }

    return y, nil
}

func doStuff2() (int,error) {
    x := f()  ?? (err error) { return 0, wrapError("f failed", err) }
    y := g(x) ?? (err error) { return 0, wrapError("g failed", err) }
    return y, nil
}

Je pense que doStuff2 est considérablement plus facile et plus rapide à lire car il :

  1. gaspille moins d'espace vertical
  2. est facile de lire rapidement le chemin heureux sur le côté gauche
  3. est facile de lire rapidement les conditions d'erreur sur le côté droit
  4. n'a pas de variable err polluant l'espace de noms local de la fonction

Pour moi, cette proposition à elle seule semble incomplète et a trop de magie. Comment l'opérateur ?? serait-il défini ? « Capture la dernière valeur de retour si non nulle » ? "Capture la dernière valeur d'erreur si correspond au type de méthode ?"

L'ajout de nouveaux opérateurs pour gérer les valeurs de retour en fonction de leur position et de leur type ressemble à un hack.

Le 29 août 2017, 13:03 +0300, Mikael Gustavsson [email protected] , a écrit :

Je pense que l'opérateur proposé par @jba et @bcmills est une très bonne idée, bien qu'il soit mieux orthographié comme " ??" au lieu de "=?" OMI.
En regardant cet exemple :
func doStuff() (int, erreur) {
x, erreur := f()
si erreur != nil {
return 0, wrapError("f a échoué", err)
}

   y, err := g(x)
   if err != nil {
           return 0, wrapError("g failed", err)
   }

   return y, nil

}

func doStuff2() (int, erreur) {
x := f() ?? (erreur d'erreur) { return 0, wrapError ("f a échoué", err) }
y := g(x) ?? (erreur d'erreur) { return 0, wrapError("g a échoué", err) }
retour y, nul
}
Je pense que doStuff2 est considérablement plus facile et plus rapide à lire car il :

  1. gaspille moins d'espace vertical
  2. est facile de lire rapidement le chemin heureux sur le côté gauche
  3. est facile de lire rapidement les conditions d'erreur sur le côté droit
  4. n'a pas de variable err polluant l'espace de noms local de la fonction

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

@nvartolomei

Comment l'opérateur ?? serait-il défini ?

Voir https://github.com/golang/go/issues/21161#issuecomment -319434101 et https://github.com/golang/go/issues/21161#issuecomment -320758279.

Étant donné que @bcmills a recommandé de ressusciter un thread @slvmnd , refait avec des modificateurs d'instructions :

func doStuff() (int, err) {
        x, err := f()
        return 0, wrapError("f failed", err)     if err != nil

    y, err := g(x)
        return 0, wrapError("g failed", err)     if err != nil

        return y, nil
}

Pas aussi concis que d'avoir l'instruction et la vérification d'erreur sur une seule ligne, mais cela se lit assez bien. (Je suggérerais d'interdire la forme d'affectation := dans l'expression if, sinon les problèmes de portée confondraient probablement les gens même s'ils sont clairs dans la grammaire) Autoriser "à moins que" comme version niée de "si" est un peu de sucre syntaxique, mais cela fonctionne bien à lire et mériterait d'être considéré.

Je ne recommanderais pas de cribler de Perl ici, cependant. (Basic Plus 2 est bien) De cette façon se trouvent les modificateurs d'instructions en boucle qui, bien que parfois utiles, apportent un autre ensemble de problèmes assez complexes.

une version plus courte :
retourner si err != nil
devrait également être pris en charge alors.

avec une telle syntaxe, la question se pose - les instructions de non-retour doivent-elles également être
pris en charge avec de telles déclarations "if", comme ceci :
func(args) si condition

peut-être qu'au lieu d'inventer l'après-action, si cela vaut la peine d'introduire le single
ligne si ?

si err!=nil retour
si err!=nil renvoie 0, wrapError("échoué", err)
si err!=nil do_smth()

cela semble beaucoup plus naturel que des formes spéciales de syntaxe, non ? Bien que je suppose
cela introduit beaucoup de douleur dans l'analyse :/

Mais... ce ne sont que de petits ajustements et pas de support lang spécial pour les erreurs
manipulation/propagation.

Le lundi 18 septembre 2017 à 16h14, dsugalski [email protected] a écrit :

Puisque @bcmills https://github.com/bcmills a recommandé de ressusciter un
fil au repos, si nous envisageons d'utiliser d'autres langues,
il semble que les modificateurs de déclaration offriraient une solution raisonnable à tous
cette. Pour prendre l'exemple de @slvmnd https://github.com/slvmnd , refait avec
modificateurs d'instructions :

func doStuff() (int, err) {
x, erreur := f()
renvoie 0, wrapError("f a échoué", err) si err != nil

  y, err := g(x)
    return 0, wrapError("g failed", err)     if err != nil

    return y, nil

}

Pas aussi concis que d'avoir l'instruction et la vérification des erreurs dans un seul
ligne, mais il se lit assez bien. (Je suggérerais d'interdire la forme := de
affectation dans l'expression if, sinon les problèmes de portée seraient probablement
confondre les gens même s'ils sont clairs dans la grammaire) Permettre "à moins que" comme
version négative de "if" est un peu de sucre syntaxique, mais cela fonctionne bien pour
lu et mériterait d'être considéré.

Je ne recommanderais pas de cribler de Perl ici, cependant. (Basique Plus 2 est
bien) De cette façon se trouvent les modificateurs d'instructions en boucle qui, bien que parfois
utile, apporter un autre ensemble de questions assez complexes.

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

En réfléchissant un peu plus à la suggestion de @jba et d'autres ont demandée, à savoir que le code sans erreur soit visiblement distinct du code d'erreur. Cela pourrait toujours être une idée intéressante si elle présente également des avantages significatifs pour les chemins de code sans erreur, mais plus j'y pense, moins elle semble attrayante par rapport aux alternatives proposées.

Je ne sais pas à quel point il est raisonnable d'attendre de la distinction visuelle du texte pur. À un moment donné, il semble plus approprié de placer cela dans l'IDE ou la couche de coloration du code de votre éditeur de texte.

Mais pour la distinction visible basée sur le texte, la norme de formatage que nous avions en place lorsque j'ai commencé à l'utiliser pour la première fois il y a longtemps était que les modificateurs d'instruction IF/UNLESS devaient être justifiés à droite, ce qui les faisait ressortir assez bien. (Bien que accordé une norme qui était plus facile à appliquer et peut-être plus distincte visuellement sur un terminal VT-220 que dans les éditeurs avec des tailles de fenêtre plus flexibles)

Pour moi, au moins, je trouve que le cas du modificateur de déclaration est facilement distinct et se lit mieux que le schéma if-block actuel. Cela peut ne pas être le cas pour d'autres personnes, bien sûr - je lis le code source de la même manière que je lis le texte anglais afin qu'il corresponde à un modèle confortable existant, et tout le monde ne le fait pas.

return 0, wrapError("f failed", err) if err != nil peut s'écrire if err != nil { return 0, wrapError("f failed", err) }

if err != nil return 0, wrapError("f failed", err) peut s'écrire de la même manière.

Peut-être que tout ce dont on a besoin ici est que gofmt laisse les if écrits sur une seule ligne sur une seule ligne au lieu de les étendre sur trois lignes ?

Il y a une autre possibilité qui me frappe. Une grande partie des frictions que je rencontre lorsque j'essaie d'écrire rapidement du code Go jetable est due au fait que je dois vérifier les erreurs à chaque appel, donc je ne peux pas imbriquer les appels correctement.

Par exemple, je ne peux pas appeler http.Client.Do sur un nouvel objet de requête sans affecter d'abord le résultat http.NewRequest à une variable temporaire, puis appeler Do sur cela.

Je me demande si nous pourrions autoriser :

f(y())

fonctionner même si y renvoie (T, error) tuple

Ensuite, je pourrais faire :

n, err := http.DefaultClient.Do(http.NewRequest("DELETE", "/foo", nil))

et le résultat de l'erreur serait non nul si NewRequest ou Do échouaient.

Cela a cependant un problème important - l'expression ci-dessus est déjà valide si f accepte deux arguments, ou des arguments variadiques. En outre, les règles exactes pour ce faire sont susceptibles d'être assez complexes.

Donc, en général, je ne pense pas que je l'aime (je n'aime pas non plus les autres propositions de ce fil), mais j'ai pensé que je rejetterais l'idée pour examen de toute façon.

@rogpeppe ou vous pouvez simplement utiliser json.NewEncoder

@gbbr Ha oui, mauvais exemple.

Un meilleur exemple pourrait être http.Request. J'ai modifié le commentaire pour l'utiliser.

Wow. De nombreuses idées rendent la lisibilité du code encore pire.
je suis d'accord avec l'approche

if val, err := DoMethod(); err != nil {
   // val is accessible only here
   // some code
}

Une seule chose est vraiment ennuyeuse, c'est la portée des variables renvoyées.
Dans ce cas, vous devez utiliser val mais c'est dans la portée de if .
Vous devez donc utiliser else mais linter sera contre (et moi aussi), et le seul moyen est

val, err := DoMethod()
if err != nil {
   // some code
}
// some code with val

Ce serait bien d'avoir accès aux variables du bloc if :

if val, err := DoMethod(); err != nil {
   // some code
}
// some code with val

@dmbreaker C'est essentiellement à cela que sert la clause de garde de Swift. Il affecte une variable dans la portée actuelle s'il satisfait à une condition. Voir mon commentaire précédent .

Je suis tout à fait pour simplifier la gestion des erreurs dans Go (même si cela ne me dérange pas personnellement), mais je pense que cela ajoute un peu de magie à un langage par ailleurs simple et extrêmement facile à lire.

@gbbr
Quel est le « ceci » auquel vous faites référence ici ? Il y a pas mal de suggestions différentes sur la façon de procéder.

Peut-être une solution en deux parties ?

Définissez try comme "retirez la valeur la plus à droite dans le tuple de retour ; si ce n'est pas la valeur zéro pour son type, renvoyez-la comme la valeur la plus à droite de cette fonction avec les autres mises à zéro". Cela rend le cas commun

 a := try ErrorableFunction(b)

et permet d'enchaîner

 a := try ErrorableFunction(try SomeOther(b, c))

(Facultativement, rendez-le non nul plutôt que non nul, pour plus d'efficacité.) Si les fonctions erronées renvoient un non nul/non nul, la fonction "abandonne avec une valeur". La valeur la plus à droite de la fonction try 'ed doit être assignable à la valeur la plus à droite de la fonction appelante ou il s'agit d'une erreur de vérification de type au moment de la compilation. (Donc, ce n'est pas codé en dur pour ne gérer que error , bien que la communauté devrait peut-être décourager son utilisation pour tout autre code "intelligent".)

Ensuite, autorisez les retours d'essai à être interceptés avec un mot-clé de type defer, soit :

catch func(e error) {
    // whatever this function returns will be returned instead
}

ou, plus prolixe peut-être mais plus conforme à la façon dont Go fonctionne déjà :

defer func() {
    if err := catch(); err != nil {
        set_catch(ErrorWrapper{a, "while posting request to server"})
    }
}()

Dans le cas catch , le paramètre de la fonction doit correspondre exactement à la valeur renvoyée. Si plusieurs fonctions sont fournies, la valeur les parcourra toutes dans l'ordre inverse. Vous pouvez bien sûr mettre une valeur dans qui résout une fonction du type correct. Dans le cas de l'exemple basé sur defer , si une fonction defer appelle set_catch la prochaine fonction différée obtiendra sa valeur de catch() . (Si vous êtes assez stupide pour le remettre à zéro dans le processus, vous obtiendrez une valeur de retour déroutante. Ne faites pas cela.) La valeur passée à set_catch doit être assignable au type renvoyé. Dans les deux cas, je m'attends à ce que cela fonctionne comme defer en ce sens qu'il s'agit d'une déclaration, pas d'une déclaration, et ne s'appliquera au code qu'après l'exécution de la déclaration.

J'ai tendance à préférer la solution basée sur le report du point de vue de la simplicité (essentiellement aucun nouveau concept n'y est introduit, c'est un deuxième type de recover() plutôt qu'une nouvelle chose), mais je reconnais qu'il peut y avoir des problèmes de performances. Avoir un mot-clé catch séparé pourrait permettre plus d'efficacité en étant plus facile à ignorer entièrement lorsqu'un retour normal se produit, et si l'on souhaite une efficacité maximale, peut-être les lier à des portées afin qu'un seul soit autorisé à être actif par portée ou fonction , ce qui serait, je pense, presque nul. (Peut-être que le nom du fichier de code source et le numéro de ligne devraient également être renvoyés à partir de la fonction catch ?

L'un ou l'autre permettrait également de gérer efficacement la gestion des erreurs répétitives à un seul endroit dans une fonction et d'offrir facilement la gestion des erreurs en tant que fonction de bibliothèque, ce qui est à mon humble avis l'un des pires aspects du cas actuel, selon les commentaires de rsc ci-dessus ; la pénibilité du traitement des erreurs a tendance à encourager le "retour d'erreur" plutôt que le traitement correct. Je sais que j'ai beaucoup de mal avec ça moi-même.

@thejerf Une partie du point de vue d'Ian avec cette proposition est d'explorer des moyens de traiter les erreurs passe-partout sans décourager les fonctions d'ajouter du contexte ou de manipuler les erreurs qu'elles renvoient.

Séparer la gestion des erreurs en try et catch semble aller à l'encontre de cet objectif, même si je suppose que cela dépend du type de détails que les programmes voudront généralement ajouter.

À tout le moins, je voudrais voir comment cela fonctionne avec des exemples plus réalistes.

Tout l'intérêt de ma proposition est de permettre d'ajouter un contexte ou de manipuler les erreurs, d'une manière que je considère comme plus correcte du point de vue du programme que la plupart des propositions ici qui impliquent de répéter ce contexte encore et encore, ce qui inhibe lui-même le désir de mettre le contexte supplémentaire dans .

Pour réécrire l'exemple original,

func Chdir(dir string) error {
    if e := syscall.Chdir(dir); e != nil {
        return &PathError{"chdir", dir, e}
    }
    return nil
}

sort comme

func Chdir(dir string) error {
    catch func(e error) {
        return &PathError{"chdir", dir, e}
    }

    try syscall.Chdir(dir)
    return nil
}

sauf que cet exemple est trop trivial pour, eh bien, n'importe laquelle de ces propositions, en fait, et je dirais que dans ce cas, nous laisserons simplement la fonction d'origine seule.

Personnellement, je ne considère pas la fonction Chdir d'origine comme un problème en premier lieu. Je règle cela spécifiquement pour traiter le cas où une fonction est perturbée par une longue gestion d'erreurs répétées, pas pour une fonction à une erreur. Je dirais aussi que si vous avez une fonction où vous faites littéralement quelque chose de différent pour chaque cas d'utilisation possible, la bonne réponse est probablement de continuer à écrire ce que nous avons déjà. Cependant, je soupçonne que c'est de loin le cas rare pour la plupart des gens, au motif que si c'était le cas courant, il n'y aurait pas de plainte en premier lieu. Le bruit de la vérification de l'erreur n'est significatif que précisément parce que les gens veulent faire "presque la même chose" encore et encore dans une fonction.

Je soupçonne également que la plupart de ce que les gens veulent seraient rencontrés

func SomethingBigger(dir string) (interface{}, error) {
     catch func (e error, filename string, lineno int) {
         return PackageSpecificError{e, filename, lineno, dir}
     }

     x := try Something()

     if x == true {
         try SomethingElse()
     } else {
         a, b = try AThirdThing()
     }

     return whatever, nil
}

Si nous éliminons le problème d'essayer de faire en sorte qu'une seule instruction if ait l'air bien au motif qu'elle est trop petite pour s'en soucier, et éliminons le problème d'une fonction qui fait vraiment quelque chose d'unique pour chaque erreur, retourne au motif que A : c'est en fait un cas assez rare et B: dans ce cas, la surcharge passe-partout n'est en fait pas si importante par rapport à la complexité du code de gestion unique, le problème peut peut-être être réduit à quelque chose qui a une solution.

moi aussi je veux vraiment voir

func packageSpecificHandler(f string) func (err error, filename string, lineno int) {
    return func (err error, filename string, lineno int) {
        return &PackageSpecificError{"In function " + f, err, filename, lineno}
    }
}

 func SomethingBigger(dir string) (interface{}, error) {
     catch packageSpecificHandler("SomethingBigger")

     ...
 }

ou un équivalent soit possible, pour quand cela fonctionne.

Et, de toutes les propositions sur la page... cela ne ressemble-t-il pas encore à Go ? Cela ressemble plus à Go que Go actuel.

Pour être honnête, la plupart de mon expérience professionnelle en ingénierie a été avec PHP (je sais) mais l'attraction principale pour Go a toujours été la lisibilité. Bien que j'apprécie certains aspects de PHP, la partie que je méprise le plus est le non-sens "final" "abstrait" "statique" et l'application de concepts trop compliqués à un morceau de code qui fait une chose.

Voir cette proposition m'a immédiatement donné le sentiment de regarder un morceau et de devoir faire une double prise et vraiment "réfléchir" à ce que ce morceau de code dit/fait. Je ne pense pas que ce code soit lisible et n'ajoute pas vraiment à la langue. Mon premier réflexe est de regarder à gauche et je pense que cela renvoie toujours nil . Cependant, avec ce changement, je devrais maintenant regarder à gauche et à droite pour déterminer le comportement du code, ce qui signifie plus de temps de lecture et plus de modèle mental.

Cependant, cela ne signifie pas qu'il n'y a pas de place pour des améliorations de la gestion des erreurs dans Go.

Je suis désolé de ne pas avoir (encore) lu ce fil en entier (c'est super long) mais je vois des gens jeter une syntaxe alternative, alors j'aimerais partager mon idée :

a, err := helloWorld(); err? {
  return fmt.Errorf("helloWorld failed with %s", err)
}

J'espère que je n'ai pas raté quelque chose ci-dessus qui annule cela. Je promets que je passerai à travers tous les commentaires un jour :)

L'opérateur ne devrait être autorisé que sur le type error , je crois, pour éviter le désordre sémantique de la conversion de type.

Intéressant, @buchanae , mais cela nous dépasse-t-il beaucoup :

if a, err := helloWorld(); err != nil {
  return fmt.Errorf("helloWorld failed with %s", err)
}

Je vois que cela permettrait à a de s'échapper, alors que dans l'état actuel, il est limité aux blocs then et else.

@ object88 Vous avez raison, le changement est subtil, esthétique et subjectif. Personnellement, tout ce que je veux de Go 2 sur ce sujet est un changement subtil de lisibilité.

Personnellement, je le trouve plus lisible car la ligne ne commence pas par if et ne nécessite pas le !=nil . Les variables sont sur le bord gauche où elles se trouvent (la plupart ?) des autres lignes.

Excellent point sur la portée de a , je n'y avais pas pensé.

Compte tenu des autres possibilités de cette grammaire, il semble que cela soit possible.

err := helloWorld(); err? {
  return fmt.Errorf("error: %s", err)
}

et probablement

helloWorld()? {
  return fmt.Errorf("hello world failed")
}

qui sont peut-être là où il s'effondre.

Peut-être que le retour d'une erreur devrait faire partie de chaque appel de fonction dans Go, vous pouvez donc imaginer :
```
a := bonjourMonde(); se tromper? {
return fmt.Errorf("helloWorld a échoué : %s", err)
}

Et si vous disposiez d'une véritable gestion des exceptions ? Je veux dire Try, catch, enfin plutôt comme beaucoup de langues modernes ?

Non, cela rend le code implicite et peu clair (bien qu'un peu plus court)

Le jeu. 23 nov. 2017 à 07:27, Kamyar Miremadi [email protected]
a écrit:

Et si vous disposiez d'une véritable gestion des exceptions ? Je veux dire Essayez, attrapez, enfin
au lieu de cela comme beaucoup de langues modernes ?

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

De retour à @mpvl « s WriteToGCS exemple en fils , je voudrais suggérer (encore une fois) que la livraison / motif rollback ne suffit pas commun pour justifier un changement majeur dans la gestion des erreurs de Go. Il n'est pas difficile de capturer le modèle dans une fonction ( lien de terrain de jeu ):

func runWithCommit(f, commit func() error, rollback func(error)) (err error) {
    defer func() {
        if r := recover(); r != nil {
            rollback(fmt.Errorf("panic: %v", r))
            panic(r)
        }
    }()
    if err := f(); err != nil {
        rollback(err)
        return err
    }
    return commit()
}

Ensuite, nous pouvons écrire l'exemple comme

func writeToGCS(ctx context.Context, bucket, dst string, r io.Reader) error {
    client, err := storage.NewClient(ctx)
    if err != nil {
        return err
    }
    defer client.Close()

    w := client.Bucket(bucket).Object(dst).NewWriter(ctx)
    return runWithCommit(
        func() error { _, err := io.Copy(w, r); return err },
        func() error { return w.Close() },
        func(err error) { _ = w.CloseWithError(err) })
}

Je suggérerais une solution plus simple :

func someFunc() error {
    ^err := someAction()
    ....
}

Pour plusieurs retours de fonctions multiples :

func someFunc() error {
    result, ^err := someAction()
    ....
}

Et pour plusieurs arguments de retour :

func someFunc() (result Result, err error) {
    var result Result
    params, ^err := someAction()
    ....
}

^ signe signifie retour si le paramètre n'est pas nul.
Fondamentalement "déplacer l'erreur vers le haut de la pile si cela se produit"

Des inconvénients de cette méthode ?

@gladkikhartem
Comment modifier l'erreur avant qu'elle ne soit renvoyée ?

@urandom
Les erreurs d'emballage sont une action importante qui, à mon avis, devrait être effectuée explicitement.
Le code Go concerne la lisibilité, pas la magie.
Je voudrais garder l'emballage d'erreur plus clair

Mais en même temps, je voudrais me débarrasser du code qui ne contient pas beaucoup d'informations et prend juste de l'espace.

if err != nil {
    return err
}

C'est comme un cliché de Go - vous ne voulez pas le lire, vous voulez juste le sauter.

Ce que j'ai vu jusqu'à présent dans cette discussion est une combinaison de:

  1. réduire la verbosité de la syntaxe
  2. améliorer l'erreur en ajoutant du contexte

Ceci est conforme à la description du problème d'origine par @ianlancetaylor qui mentionne les deux aspects, mais à mon avis, les deux devraient être discutés/définis/expérimentés séparément et éventuellement dans des itérations différentes pour limiter la portée des changements et uniquement pour des raisons d'efficacité (un un changement plus important de la langue est plus difficile à faire qu'un changement progressif).

1. Réduction de la verbosité de la syntaxe

J'aime l'idée de @gladkikhartem , même dans sa forme originale que je rapporte ici puisqu'elle a été éditée/étendue :

 result, ^ := someAction()

Dans le cadre d'une fonction :

func getOddResult() (int, error) {
    result, ^ := someResult()
    if result % 2 == 0 {
          return result + 1, nil
    }
    return result, nil
}

Cette syntaxe courte - ou sous la forme proposée par @gladkikhartem avec err^ - résoudrait la partie verbosité syntaxique du problème (1).

2. Contexte de l'erreur

Pour la 2ème partie, en ajoutant plus de contexte, on pourrait même complètement l'oublier pour le moment et proposer plus tard d'ajouter automatiquement un stacktrace à chaque erreur si un type spécial contextError est utilisé. Un tel nouveau type d'erreur natif pourrait comporter des traces de pile complètes ou courtes (imaginez un GO_CONTEXT_ERROR=full ) et être compatible avec l'interface error tout en offrant la possibilité d'extraire au moins la fonction et le nom de fichier de la pile d'appels supérieure entrée.

Lors de l'utilisation d'un contextError , Go devrait attacher la trace de pile d'appels exactement au point où l'erreur est créée.

Encore une fois avec un exemple de fonction :

func getOddResult() (int, contextError) {
    result, ^ := someResult() // here a 'contextError' is created; if the error received from 'someResult()' is also a `contextError`, the two are nested
    if result % 2 == 0 {
          return result + 1, nil
    }
    return result, nil
}

Seul le type est passé de error à contextError , qui pourrait être défini comme :

type contextError interface {
    error
    Stack() []StackEntry
    Cause() contextError
}

(remarquez en quoi ce Stack() est différent de https://golang.org/pkg/runtime/debug/#Stack, car nous espérons avoir une version sans octets de la pile d'appels goroutine ici)

La méthode Cause() renverrait nil ou le contextError précédent

Je suis très conscient des implications potentielles sur la mémoire du transport de piles comme celle-ci, j'ai donc fait allusion à la possibilité d'avoir une pile courte par défaut qui ne contiendrait qu'une ou quelques entrées de plus. Un développeur activerait généralement les traces de piste complètes dans les versions de développement/débogage et laisserait la valeur par défaut (traces de pile courtes) dans le cas contraire.

Art antérieur :

Juste matière à réflexion.

@gladkikhartem @gdm85

Je pense que vous avez manqué le but de cette proposition. Le message original de Per Ian :

Il est déjà facile (peut-être trop facile) d'ignorer l'erreur (voir #20803). De nombreuses propositions existantes pour la gestion des erreurs facilitent le renvoi de l'erreur non modifiée (par exemple, #16225, #18721, #21146, #21155). Peu facilitent le retour de l'erreur avec des informations supplémentaires.

Renvoyer des erreurs non modifiées est souvent erroné, et généralement pour le moins inutile. Nous voulons encourager une gestion prudente des erreurs : traiter uniquement le cas d'utilisation « retour non modifié » biaiserait les incitations dans la mauvaise direction.

@bcmills si le contexte (sous la forme d'une trace de pile) est ajouté, l'erreur est renvoyée avec des informations supplémentaires. Est-ce que joindre un message lisible par l'homme, par exemple « erreur lors de l'insertion de l'enregistrement » serait considéré comme une « gestion prudente des erreurs » ? Comment décider à quel point de la pile d'appels ces messages doivent être ajoutés (dans chaque fonction, haut/bas, etc.) ? Ce sont toutes des questions courantes lors des améliorations de la gestion des erreurs de codage.

Le "retour non modifié" pourrait être contrecarré comme expliqué ci-dessus avec "retour non modifié avec stacktrace" par défaut, et (dans un style réactif) ajouter un message lisible par l'homme si nécessaire. Je n'ai pas précisé comment un tel message lisible par l'homme pourrait être ajouté, mais on peut voir comment l'emballage fonctionne dans pkg/errors pour certaines idées.

« Renvoyer des erreurs non modifiées est souvent faux » : je propose donc un chemin de mise à niveau pour le cas d'utilisation paresseux, qui est le même cas d'utilisation actuellement signalé comme préjudiciable.

@bcmills
Je suis à 100% d'accord avec #20803 que les erreurs doivent toujours être traitées ou explicitement ignorées (et je n'ai aucune idée pourquoi cela n'a pas été fait avant...)
oui, je n'ai pas abordé le point de la proposition et je n'ai pas à le faire. Je me soucie de la solution réelle proposée, pas des intentions qui la sous-tendent, car les intentions ne correspondent pas aux résultats. Et quand je vois || tel || trucs proposés - ça me rend vraiment triste.

Si l'intégration d'informations, telles que des codes d'erreur et des messages d'erreur dans l'erreur, était facile et transparente - vous n'aurez pas besoin d'encourager une gestion prudente des erreurs - les gens le feront eux-mêmes.
Par exemple, faites simplement de l'erreur un alias. Nous pourrions retourner n'importe quel type de contenu et l'utiliser en dehors de la fonction sans casting. Cela rendrait la vie tellement plus facile.

J'aime que Go me rappelle de gérer les erreurs, mais je déteste quand le design m'encourage à faire quelque chose de discutable.

@gdm85
Ajouter automatiquement une trace de pile à une erreur est une idée terrible, il suffit de regarder les traces de pile Java.
Lorsque vous enveloppez vous-même les erreurs, il est beaucoup plus facile de naviguer et de comprendre ce qui ne va pas. C'est tout l'intérêt de l'envelopper.

@gladkikhartem Je ne suis pas d'accord pour dire qu'une forme d'"emballage automatique" serait bien pire pour naviguer et aider à comprendre ce qui ne va pas. Je ne comprends pas non plus exactement ce à quoi vous faites référence dans les traces de pile Java (je suppose des exceptions ? esthétiquement moche ? quel problème spécifique ?), mais pour discuter dans un sens constructif : quelle pourrait être une bonne définition d'"erreur soigneusement gérée" ?

Je demande à la fois d'améliorer ma compréhension des meilleures pratiques de Go (plus ou moins canoniques qu'elles soient) et parce que je pense qu'une telle définition pourrait être la clé pour faire une proposition vers une amélioration par rapport à la situation actuelle.

@gladkikhartem Je sais que cette proposition est déjà partout, mais faisons tout notre possible pour qu'elle reste concentrée sur les objectifs que j'ai définis au départ. Comme je l'ai dit lors de la publication de ce numéro, il existe déjà plusieurs propositions différentes qui visent à simplifier if err != nil { return err } , et c'est l'endroit idéal pour discuter de la syntaxe qui n'améliore que ce cas spécifique. Merci.

@ianlancetaylor
Désolé si j'ai déplacé la discussion hors de son chemin.

Si vous souhaitez ajouter des informations contextuelles à une erreur, je vous suggère d'utiliser cette syntaxe :
(et obliger les gens à n'utiliser qu'un seul type d'erreur pour une fonction pour une extraction de contexte facile)

type MyError struct {
    Type int
    Message string
    Context string
    Err error
}

func VeryLongFunc() error {
    var err MyError
    err.Context = "general function context"


   result, ^err.Err := someAction() {
       err.Type = PermissionError
       err.Message = fmt.SPrintf("some action has no right to access file %v: ", file)
   }

    // in case we need to make a cleanup after error

   result, ^err.Err := someAction() {
       err.Type = PermissionError
       err.Message = fmt.SPrintf("some action has no right to access file %v: ", file)
       file.Close()
   }

   // another variant with different symbol and return statement

   result, ?err.Err := someAction() {
       err.Type = PermissionError
       err.Message = fmt.SPrintf("some action has no right to access file %v: ", file)
       return err
   }

   // using original approach

   result, err.Err := someAction()
   if err != nil {
       err.Type = PermissionError
       err.Message = fmt.SPrintf("some action has no right to access file %v: ", file)
       return err
   }
}

func main() {
    err := VeryLongFunc()
    if err != nil {
        e := err.(MyError)
        log.Print(e.Error(), " in ", e.Dir)
    }
}

Le symbole ^ est utilisé pour indiquer le paramètre d'erreur, ainsi que pour différencier la définition de la fonction de la gestion des erreurs pour "someAction() { }"
{ } peut être omis si l'erreur est renvoyée sans modification

Ajout de ressources supplémentaires pour répondre à ma propre invitation afin de mieux définir la "gestion prudente des erreurs":

Aussi fastidieuse que soit l'approche actuelle, je pense que c'est moins déroutant que les alternatives, bien qu'une ligne si les déclarations puissent fonctionner? Peut-être?

blah, err := doSomething()
if err != nil: return err

...ou même...

blah, err := doSomething()
if err != nil: return &BlahError{"Something",err}

Quelqu'un a peut-être déjà soulevé cette question, mais il y a beaucoup, beaucoup de messages et j'en ai lu beaucoup mais pas tous. Cela dit, je pense personnellement qu'il vaudrait mieux être explicite qu'implicite.

J'ai été un fan de la programmation ferroviaire, l'idée vient de la déclaration with d'Elixir.
else bloc e == nil court-circuité.

Voici ma proposition avec pseudo code à venir :

func Chdir(dir string) (e error) {
    with e == nil {
            e = syscall.Chdir(dir)
            e, val := foo()
            val = val + 1
            // something else
       } else {
           printf("e is not nil")
           return
       }
       return nil
}

@ardhitama N'est-ce pas alors comme Try catch, sauf que "With" est comme la déclaration "Try" et que "Else" est comme "Catch"?
Pourquoi ne pas implémenter la gestion des exceptions comme Java ou C# ?
en ce moment, si un programmeur ne veut pas gérer d'exception dans cette fonction, il la renvoie à la suite de cette fonction. Pourtant, il n'y a aucun moyen de forcer un programmeur à gérer une exception s'il ne le veut pas et bien des fois vous n'en avez pas vraiment besoin, mais ce que nous obtenons ici, ce sont beaucoup d'instructions if err!=nil qui rendent le code moche et illisible (beaucoup de bruit). N'est-ce pas la raison pour laquelle l'instruction Try Catch Final a été inventée en premier lieu dans un autre langage de programmation ?

Donc, je pense qu'il vaut mieux que Go Authors "essaye" de ne pas être têtu !! et introduisez simplement l'instruction "Essayez enfin la capture" dans les prochaines versions. Merci.

@KamyarM
Vous ne pouvez pas introduire la gestion des exceptions dans go, car il n'y a pas d'exceptions dans Go.
Introduire try{} catch{} en Go, c'est comme introduire try{} catch{} en C - c'est totalement faux .

@ianlancetaylor
Qu'en est-il de ne pas changer du tout la gestion des erreurs Go, mais plutôt de changer l'outil gofmt comme celui-ci pour la gestion des erreurs sur une seule ligne ?

err := syscall.Chdir(dir)
    if err != nil {return &PathError{"chdir", dir, err}}
err = syscall.Chdir(dir2)
    if err != nil {return err}

Il est rétrocompatible et vous pouvez l'appliquer à vos projets actuels

Les exceptions sont des instructions goto décorées, elles transforment votre pile d'appels en un graphique d'appels et il y a une bonne raison pour laquelle les projets non universitaires les plus sérieux barrent ou limitent leur utilisation. Un objet avec état appelle une méthode qui transfère le contrôle arbitrairement vers le haut de la pile, puis reprend l'exécution des instructions... semble être une mauvaise idée car c'est le cas.

@KamyarM En substance, c'est le cas, mais en pratique, ce n'est pas le cas. À mon avis parce que nous sommes explicites ici et que nous ne brisons aucun idiome de Go.

Pourquoi?

  1. Les expressions à l'intérieur with instruction
  2. Les instructions à l'intérieur de with se comporteront comme à l'intérieur des blocs try et catch . En effet, il sera plus lent car à chaque instruction suivante, il devra évaluer les conditions de with dans le pire des cas.
  3. De par sa conception l'intention est de supprimer excessive if s et ne pas créer de gestionnaire d'exceptions que le gestionnaire sera toujours local ( with l » expression et else bloc).
  4. Pas besoin de dérouler la pile à cause de throw

ps. Corrigez-moi si j'ai tort, s'il-vous plait.

@ardhitama
KamyarM a raison dans le sens où with statement a l'air aussi laid que try catch et il introduit également un niveau d'indentation pour le flux de code normal.
Sans même mentionner l'idée de la proposition originale de modifier chaque erreur individuellement. Cela ne fonctionnera tout simplement pas avec élégance avec try catch , avec ou toute autre méthode qui regroupe des instructions.

@gladkikhartem
Oui, c'est pourquoi je propose d'adopter à la place une "programmation orientée chemin de fer" et d'essayer de ne pas supprimer le caractère explicite. C'est juste un autre angle pour attaquer le problème, les autres solutions veulent le résoudre en ne laissant pas le compilateur écrire automatiquement if err != nil pour vous.

with non seulement pour la gestion des erreurs, il peut être utile pour tout autre flux de contrôle.

@gladkikhartem
Permettez-moi de préciser que je pense que le bloc Try Catch Finally est magnifique. If err!=nil ... est en fait le code moche.

Go n'est qu'un langage de programmation. Il y a tellement d'autres langues. J'ai découvert que beaucoup dans la communauté Go la considèrent comme leur religion et ne sont pas ouverts à changer ou à admettre les erreurs. C'est faux.

@gladkikhartem

Je suis d'accord si les auteurs de Go l'appellent Go++ ou Go# ou GoJava et y présentent le Try Catch Finally ;)

@KamyarM

Éviter les changements inutiles est nécessaire - essentiel - pour toute entreprise d'ingénierie. Quand les gens disent changement dans ce contexte, ils veulent vraiment dire _changer pour le mieux_, qu'ils transmettent efficacement avec des _arguments_ guidant une prémisse à cette conclusion prévue.

L'appel _juste ton esprit, mec !_ n'est pas convaincant. Ironiquement, il essaie de dire que quelque chose que la plupart des programmeurs considèrent comme ancien et maladroit est _nouveau et amélioré_.

Il existe également de nombreuses propositions et discussions où la communauté Go discute des erreurs précédentes. Mais je suis d'accord avec vous quand vous dites que Go n'est qu'un langage de programmation. Il le dit sur le site Web de Go et d'autres endroits, et j'ai parlé à quelques personnes qui l'ont également confirmé.

J'ai découvert que beaucoup dans la communauté Go la considèrent comme leur religion et ne sont pas ouverts à changer ou à admettre les erreurs.

Go est basé sur la recherche académique; les opinions personnelles n'ont pas d'importance.

Même les principaux développeurs du compilateur C# de Microsoft ont reconnu publiquement que les _exceptions_ sont un mauvais moyen de gérer les erreurs, tout en considérant le modèle Go/Rust comme une meilleure alternative : http://joeduffyblog.com/2016/02/07/the-error-model/

Certes, il est possible d'améliorer le modèle d'erreur de Go, mais pas en adoptant des solutions de type exceptions, car elles ne font qu'ajouter une complexité énorme en échange de quelques avantages discutables.

@Dr-Terrible Merci pour l'article.

Mais je n'ai trouvé nulle part qui mentionne GoLang comme langue académique.

Btw, pour clarifier mon point, dans cet exemple

func Execute() error {
    err := Operation1()
    if err!=nil{
        return err
    }

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

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

    err = Operation4()
    return err
}

Est similaire à implémenter la gestion des exceptions en C# comme ceci :

         public void Execute()
        {

            try
            {
                Operation1();
            }
            catch (Exception)
            {
                throw;
            }
            try
            {
                Operation2();
            }
            catch (Exception)
            {
                throw;
            }
            try
            {
                Operation3();
            }
            catch (Exception)
            {
                throw;
            }
            try
            {
                Operation4();
            }
            catch (Exception)
            {
                throw;
            }
        }

N'est-ce pas une façon terrible de gérer les exceptions en C# ? Ma réponse est oui, je ne sais pas pour la vôtre! A Go je n'ai pas d'autre choix. C'est ce choix terrible ou l'autoroute. C'est comme ça dans GO et je n'ai pas le choix.

Soit dit en passant, comme cela est également mentionné dans l'article que vous avez partagé, n'importe quel langage peut implémenter la gestion des erreurs comme Go sans avoir besoin de syntaxe supplémentaire, donc Go n'a en fait implémenté aucune méthode révolutionnaire de gestion des erreurs. Il n'a tout simplement aucun moyen de gérer les erreurs et vous êtes donc limité à l'utilisation de l'instruction If pour la gestion des erreurs.

D'ailleurs, je sais que GO a un Panic, Recover , Defer non recommandé qui ressemble un peu à Try Catch Finally mais à mon avis personnel, la syntaxe Try Catch Finally est une manière beaucoup plus propre et mieux organisée de gérer les exceptions.

@Dr-Terrible

Veuillez également vérifier ceci :
https://github.com/manucorporat/try

@KamyarM , il n'a pas dit que le go est un langage académique, il a dit qu'il est basé sur la recherche académique. L'article n'était pas non plus sur Go, mais il examine le paradigme de gestion des erreurs utilisé par Go.

Si vous trouvez que manucorporat/try fonctionne pour vous, veuillez l'utiliser dans votre code. Mais les coûts (performances, complexité du langage, etc.) d'ajouter try/catch au langage lui-même ne valent pas le compromis.

@KamyarM
Votre exemple n'est pas exact. Alternative à

    err := Operation1()
    if err!=nil {
        return err
    }
    err = Operation2()
    if err!=nil{
        return err
    }
    err = Operation3()
    if err!=nil{
        return err
    }
    return Operation4()

sera

            Operation1();
            Operation2();
            Operation3();
            Operation4();

la gestion des exceptions semble une bien meilleure option dans cet exemple. En théorie ça devrait être bien, mais en pratique
vous devez répondre avec un message d'erreur précis pour chaque erreur survenue dans votre point de terminaison.
L'ensemble de l'application dans Go est généralement traité à 50 % d'erreurs.

         err := Operation1()
    if err!=nil {
        log.Print("input error", err)
                fmt.Fprintf(w,"invalid input")
        w.WriteHeader(http.StatusBadRequest)
        return
    }
    err = Operation2()
    if err!=nil{
        log.Print("controller error", err)
                fmt.Fprintf(w,"operation has no meaning in this context")
        w.WriteHeader(http.StatusBadRequest)
        return
    }
    err = Operation3()
    if err!=nil{
        log.Print("database error", err)
                fmt.Fprintf(w,"unable to access database, try again later")
        w.WriteHeader(http.StatusServiceUnavailable)
        return
    }

Et si les gens disposent d'un outil aussi puissant que try catch, je suis sûr à 100% qu'ils en abuseront au profit d'une gestion prudente des erreurs.

Il est intéressant de noter que le monde universitaire est mentionné, mais Go est une collection de leçons tirées d'une expérience pratique. Si l'objectif est d'écrire une mauvaise API qui renvoie des messages d'erreur incorrects, la gestion des exceptions est la voie à suivre.

Cependant, je ne veux pas d'erreur invalid HTTP header lorsque ma demande contient un JSON request body malformé, la gestion des exceptions est le bouton magique fire-and-forget qui permet d'atteindre cet objectif dans les API C++ et C# qui utilisent eux.

Pour une large couverture d'API, il est impossible de fournir suffisamment de contexte d'erreur pour obtenir une gestion d'erreur significative. C'est parce que toute bonne application gère 50 % d'erreurs dans Go, et devrait être à 90 % dans une langue qui nécessite un transfert de contrôle non local pour gérer les erreurs.

@gladkikhartem

La méthode alternative que vous avez mentionnée est la bonne façon d'écrire le code en C#. Il ne s'agit que de 4 lignes de code et montre le chemin d'exécution heureux. Il n'a pas ces bruits if err!=nil . Si une exception se produit, la fonction qui se soucie de ces exceptions peut la gérer en utilisant Try Catch Finally (cela peut être la même fonction elle-même ou l'appelant ou l'appelant de l'appelant ou l'appelant de l'appelant de l'appelant de l'appelant ... ou simplement un gestionnaire d'événements qui traite toutes les erreurs non gérées dans une application. Le programmeur a différents choix.)

err := Operation1()
    if err!=nil {
        log.Print("input error", err)
                fmt.Fprintf(w,"invalid input")
        w.WriteHeader(http.StatusBadRequest)
        return
    }
    err = Operation2()
    if err!=nil{
        log.Print("controller error", err)
                fmt.Fprintf(w,"operation has no meaning in this context")
        w.WriteHeader(http.StatusBadRequest)
        return
    }
    err = Operation3()
    if err!=nil{
        log.Print("database error", err)
                fmt.Fprintf(w,"unable to access database, try again later")
        w.WriteHeader(http.StatusServiceUnavailable)
        return
    }

Cela a l'air simple mais c'est délicat. Je suppose que vous pouvez créer un type d'erreur personnalisé qui porte l'erreur système, l'erreur utilisateur (sans divulguer l'état interne à l'utilisateur qui pourrait ne pas avoir les meilleures intentions) et le code HTTP.

func Chdir(dir string) error {
    if e := syscall.Chdir(dir); e != nil {
        return &PathError{"chdir", dir, e}
    }
    return nil
}

Mais essaie

func Chdir(dir string) error {
    return  syscall.Chdir(dir) ? &PathError{"chdir", dir, err}:nil;
}
func Chdir(dir string) error {
    return  syscall.Chdir(dir) ? &PathError{"chdir", dir, err};
}



md5-9bcd2745464e8d9597cba6d80c3dcf40



```go
func Chdir(dir string) error {
    n , _ := syscall.Chdir(dir):
               // something to do
               fmt.Println(n)
}

Tous contiennent une sorte de magie non évidente, qui ne simplifie pas les choses pour le lecteur. Dans les deux exemples précédents, err devient une sorte de pseudo-mot-clé ou de variable spontanée. Dans les deux derniers exemples, il n'est pas du tout clair ce que cet opérateur : est censé faire -- une erreur va-t-elle être automatiquement renvoyée ? Le RHS de l'opérateur est-il une instruction unique ou un bloc ?

FWIW, j'écrirais une fonction wrapper pour que vous puissiez faire return newPathErr("chdir", dir, syscall.Chdir(dir)) et cela renverrait automatiquement une erreur nulle si le troisième paramètre est nul. :-)

OMI, la meilleure proposition que j'ai vue pour atteindre les objectifs de "simplifier la gestion des erreurs dans Go" et "renvoyer l'erreur avec des informations contextuelles supplémentaires" vient de @mrkaspa dans #21732 :

a, b, err? := f1()

s'étend à ceci:

if err != nil {
   return nil, errors.Wrap(err, "failed")
}

et je peux le forcer à paniquer avec ça :

a, b, err! := f1()

s'étend à ceci:

if err != nil {
   panic(errors.Wrap(err, "failed"))
}

Cela maintiendra la compatibilité descendante et résoudra tous les points douloureux de la gestion des erreurs dans go

Cela ne gère pas les cas tels que les fonctions bufio qui renvoient des valeurs non nulles ainsi que des erreurs, mais je pense qu'il est acceptable de gérer explicitement les erreurs dans les cas où vous vous souciez des autres valeurs de retour. Et bien sûr, les valeurs de retour sans erreur devraient être la valeur nulle appropriée pour ce type.

Le ? le modificateur réduira les erreurs passe-partout dans les fonctions et le ! modifier fera de même pour les endroits où assert serait utilisé dans d'autres langages, comme dans certaines fonctions principales.

Cette solution a l'avantage d'être très simple et de ne pas essayer d'en faire trop, mais je pense qu'elle répond aux exigences énoncées dans cet énoncé de proposition.

Dans le cas où vous avez...

func foo() (int, int, error) {
    a, b, err? := f1()
    return a, b, nil
}
func bar() (int, error) {
    a, b, err? := foo()
    return a+b, nil
}

Si quelque chose ne va pas dans foo , alors sur le site d'appel de bar , l'erreur est doublée avec le même texte, sans ajouter de sens. À tout le moins, je m'opposerais à la partie errors.Wrap de la suggestion.

Mais en élargissant encore plus, quel est le résultat attendu de cela?

func baz() (a, b int, err error) {
  a = 1
  b = 2
  a, b, err? = f1()
  return

a et b réaffectés à des valeurs nulles ? Si c'est le cas, c'est de la magie, ce que je pense que nous devrions éviter. Réalisent-ils les valeurs précédemment assignées ? (Je ne me soucie pas moi-même des valeurs de retour nommées, mais elles doivent toujours être prises en compte dans le cadre de cette proposition.)

@dup2X Oui,

@ object88, il est naturel de s'attendre à ce qu'en cas d'erreur, tout le reste soit annulé. C'est simple à comprendre et n'a rien de magique, à peu près déjà une convention pour le code Go, nécessite peu de mémoire et n'a pas de cas particuliers. Si nous permettons aux valeurs d'être conservées, cela complique les choses. Si vous oubliez de vérifier les erreurs, les valeurs renvoyées peuvent être utilisées par accident, auquel cas tout peut arriver. Comme appeler des méthodes sur une structure partiellement allouée au lieu de paniquer sur nil. Les programmeurs peuvent même commencer à s'attendre à ce que certaines valeurs soient renvoyées en cas d'erreur. À mon avis, ce serait un gâchis et on n'y gagnerait rien de bon.

En ce qui concerne l'emballage, je ne pense pas que le message par défaut fournisse quoi que ce soit d'utile. Ce serait bien de simplement enchaîner les erreurs. Comme lorsque les exceptions contiennent des exceptions internes. Très utile pour déboguer les erreurs au plus profond d'une bibliothèque.

Respectueusement, je ne suis pas d'accord, @creker. Nous avons des exemples de ce scénario dans la Go stdlib de valeurs de retour non nulles même dans le cas d'une erreur non nulle, et sont en fait fonctionnels, comme plusieurs fonctions dans la structure bufio.Reader . En tant que programmeurs Go, nous sommes activement encouragés à vérifier / gérer toutes les erreurs ; il semble plus flagrant d'ignorer les erreurs que d'obtenir des valeurs de retour non nulles et une erreur. Dans le cas que vous citez, si vous retournez un nils et ne vérifiez pas l'erreur, vous pouvez toujours opérer sur une valeur invalide.

Mais en mettant cela de côté, examinons cela un peu plus loin. Quelle serait la sémantique de l'opérateur ? ? Peut-il être appliqué uniquement aux types qui implémentent l'interface error ? Peut-il être appliqué à d'autres types ou renvoyer des arguments ? S'il peut être appliqué à des types qui n'implémentent pas d'erreur, est-il déclenché par une valeur / un pointeur non nul ? L'opérateur ? peut-il être appliqué à plusieurs valeurs de retour, ou s'agit-il d'une erreur du compilateur ?

@erwbgy
Si vous voulez simplement renvoyer une erreur sans rien d'utile, il serait beaucoup plus simple de simplement dire au compilateur de traiter toutes les erreurs non manuelles comme "if err != nil return...", par exemple :

func doStuff() error {
    doAnotherStuff() // returns error
}

func doStuff() error {
    res := doAnotherStuff() // returns string, error
}

Et il n'y a pas besoin de fou supplémentaire ? symbole dans ce cas.

@ objet88
J'ai essayé d'appliquer la plupart des propositions d'emballage d'erreurs présentées ici dans du code réel et j'ai rencontré un problème majeur - le code devient trop dense et illisible.
Ce qu'il fait, c'est juste sacrifier la largeur du code au profit de la hauteur du code.
L'emballage des erreurs avec d'habitude if err != nil permet en fait de diffuser le code pour une meilleure lisibilité, donc je ne pense pas que nous devrions même changer quoi que ce soit pour l'emballage des erreurs du tout.

@ objet88

Dans le cas de votre site, si vous retournez un nils et ne vérifiez pas l'erreur, vous pouvez toujours opérer sur une valeur invalide.

Mais cela produira une erreur évidente et facile à repérer, comme une panique sur zéro. Si vous avez besoin de renvoyer des valeurs significatives en cas d'erreur, vous devez le faire explicitement et documenter exactement quelle valeur est utilisable dans quel cas. Le simple fait de renvoyer des éléments aléatoires qui se trouvaient dans les variables en cas d'erreur est dangereux et conduira à des erreurs subtiles. Encore une fois, on n'y gagne rien.

@gladkikhartem le problème avec if err != nil est que la logique réelle y est complètement perdue et vous devez la rechercher activement si vous voulez comprendre ce que fait le code sur son chemin réussi et ne vous souciez pas de toute cette gestion des erreurs . Cela devient comme lire beaucoup de code C où vous avez plusieurs lignes de code réel et tout le reste n'est qu'une vérification d'erreur. Les gens ont même recours à des macros qui enveloppent tout cela et vont jusqu'à la fin de la fonction.

Je ne vois pas comment la logique peut devenir trop dense dans un code correctement écrit. C'est logique. Chaque ligne de votre code contient du code réel qui vous intéresse, c'est ce que vous voulez. Ce que vous ne voulez pas, c'est passer par des lignes et des lignes de passe-partout. Utilisez des commentaires et divisez votre code en blocs si cela vous aide. Mais cela ressemble plus à un problème avec le code réel et non avec la langue.

Cela fonctionne dans la cour de récréation, si vous ne la reformatez pas :

a, b, err := Frob("one string"); if err != nil { return a, b, fmt.Errorf("couldn't frob: %v", err) }
// continue doing stuff with a and b

Il me semble donc que la proposition originale, et beaucoup d'autres mentionnées ci-dessus, essaient de trouver une syntaxe abrégée claire pour ces 100 caractères et empêchent gofmt d'insister pour ajouter des sauts de ligne et reformater le bloc sur 3 lignes.

Imaginons donc que nous modifions gofmt pour arrêter d'insister sur les blocs multilignes, commençons par la ligne ci-dessus et essayons de trouver des moyens de la rendre plus courte et plus claire.

Je ne pense pas que la partie avant le point-virgule (l'affectation) doive être modifiée, ce qui laisse 69 caractères que nous pourrions réduire. Parmi ceux-ci, 49 sont l'instruction de retour, les valeurs à retourner et l'emballage d'erreur, et je ne vois pas grand-chose à changer la syntaxe de cela (par exemple, en rendant les instructions de retour facultatives, ce qui confond les utilisateurs).

Cela laisse donc trouver un raccourci pour ; if err != nil { _ } où le trait de soulignement représente un morceau de code. Je pense que tout raccourci devrait inclure explicitement err pour plus de clarté même si cela rend la comparaison nulle quelque peu invisible, nous nous retrouvons donc avec un raccourci pour ; if _ != nil { _ } .

Imaginez un instant que nous utilisions un seul personnage. Je vais choisir § comme espace réservé pour quel que soit ce personnage. La ligne de code serait alors :

a, b, err := Frob("one string") § err return a, b, fmt.Errorf("couldn't frob: %v", err)

Je ne vois pas comment vous pourriez faire mieux que cela sans modifier la syntaxe d'affectation existante ou la syntaxe de retour, ou sans que la magie invisible se produise. (Il y a encore de la magie, en ce que le fait que nous comparons l'erreur à zéro n'est pas évident.)

Cela fait 88 caractères, économisant un total de 12 caractères sur une ligne de 100 caractères.

Ma question est donc : est-ce que ça vaut vraiment la peine de le faire ?

Edit: Je suppose que mon point est, quand les gens regardent les blocs if err != nil Go et disent "Je souhaite que nous puissions nous débarrasser de cette merde", 80-90% de ce dont ils parlent est _des choses que vous avez intrinsèquement faire pour gérer les erreurs_. La surcharge réelle causée par la syntaxe de Go est minime.

@lpar , vous suivez principalement la même logique que j'ai appliquée ci -

a, b := Frob("one string")  § err { return ... }

est plus lisible par un facteur qui dépasse la simple réduction de caractères.

@lpar, vous pouvez enregistrer encore plus de caractères si vous supprimez à peu près fmt.Errorf inutiles, modifiez le retour à une syntaxe spéciale et introduisez la pile d'appels dans les erreurs afin qu'elles aient un contexte réel et ne soient pas de simples chaînes glorifiées. Cela vous laisserait avec quelque chose comme ça

a, b, err? := Frob("one string")

Le problème avec les erreurs Go pour moi a toujours été un manque de contexte. Le retour et l'emballage des chaînes n'est pas du tout utile pour déterminer où l'erreur s'est réellement produite. C'est pourquoi github.com/pkg/errors par exemple, est devenu un incontournable pour moi. Avec de telles erreurs, je bénéficie de la simplicité de gestion des erreurs Go et des avantages des exceptions qui capturent parfaitement le contexte et vous permettent de trouver le lieu exact de l'échec.

Et, même si nous prenons votre exemple tel qu'il est, le fait que la gestion des erreurs soit à droite est une amélioration significative de la lisibilité. Vous n'avez plus besoin de sauter plusieurs lignes de passe-partout pour accéder à la signification réelle du code. Vous pouvez dire ce que vous voulez sur l'importance de la gestion des erreurs, mais lorsque je lis le code pour le comprendre, je me fiche des erreurs. Tout ce dont j'ai besoin, c'est d'un chemin réussi. Et quand j'ai besoin d'erreurs, je les recherche spécifiquement. Les erreurs, de par leur nature, sont des cas exceptionnels et doivent prendre le moins de place possible.

Je pense que la question de savoir si fmt.Errorf est « inutile » par rapport à errors.Wrap est orthogonale à ce problème, car ils sont tous les deux à peu près aussi verbeux. (Dans les applications réelles que je n'utilise pas non plus, j'utilise autre chose qui enregistre également l'erreur et la ligne de code sur laquelle elle s'est produite.)

Donc, je suppose que cela vient du fait que certaines personnes aiment vraiment que la gestion des erreurs soit à droite. Je ne suis tout simplement pas convaincu, même venant d'un contexte Perl et Ruby.

@lpar J'utilise errors.Wrap car il capture automatiquement la pile d'appels - je n'ai pas vraiment besoin de tous ces messages d'erreur. Je me soucie davantage de l'endroit où cela s'est produit et, peut-être, des arguments passés à la fonction qui a produit l'erreur. Vous dites même que vous faites la même chose - connectez une ligne de code pour fournir un contexte à vos messages d'erreur. Étant donné que nous pouvons penser à des moyens de réduire le passe-partout tout en donnant plus de contexte aux erreurs (c'est à peu près la proposition ici).

Quant aux erreurs étant à droite. Pour moi, il ne s'agit pas simplement de l'endroit mais de réduire la charge cognitive nécessaire pour lire un code jonché de gestion des erreurs. Je n'accepte pas l'argument selon lequel les erreurs sont si importantes que vous voudriez qu'elles prennent autant de place qu'elles le font. En fait, je préférerais qu'ils s'en aillent le plus possible. Ils ne sont pas l'histoire principale.

@creker

Il s'agit plus probablement d'une erreur de développeur triviale qu'une erreur dans un système de production générant une erreur due à une mauvaise entrée de l'utilisateur. Si tout ce dont vous avez besoin pour déterminer l'erreur est le numéro de ligne et le chemin du fichier, il y a de fortes chances que vous veniez d'écrire le code et que vous sachiez déjà ce qui ne va pas.

@car c'est similaire aux exceptions. Dans la plupart des cas, la pile d'appels et le message d'exception suffisent pour déterminer l'endroit et la cause de l'erreur. Dans les cas plus complexes, vous connaissez au moins l'endroit où l'erreur s'est produite. Les exceptions vous donnent cet avantage par défaut. Avec Go, vous devez soit enchaîner les erreurs, en émulant essentiellement la pile d'appels, soit inclure la pile d'appels réelle.

Dans un code correctement écrit, la plupart du temps, vous sauriez à partir du numéro de ligne et du chemin du fichier la cause exacte car l'erreur serait attendue. En fait, vous avez écrit du code pour vous préparer à ce que cela se produise. Si quelque chose d'inattendu se produisait, alors oui, la pile d'appels ne vous en donnerait pas la cause, mais cela réduirait considérablement l'espace de recherche.

@comme

D'après mon expérience, les erreurs de saisie de l'utilisateur sont traitées presque immédiatement. Les vraies erreurs de production problématiques se produisent profondément dans le code (par exemple, un service est en panne, ce qui fait qu'un autre service génère des erreurs), et il est très utile d'obtenir une trace de pile appropriée. Pour ce que ça vaut, les traces de la pile java sont extrêmement utiles lors du débogage des problèmes de production, pas les messages.

@creker
Les erreurs ne sont que des valeurs , et elles font partie des entrées et sorties de la fonction. Ils ne pouvaient pas être « inattendus ».
Si vous voulez savoir pourquoi la fonction vous a donné une erreur - utilisez les tests, la journalisation, etc.

@gladkikhartem dans le monde réel ce n'est pas si simple. Oui, vous vous attendez à des erreurs dans le sens où la signature de fonction inclut une erreur comme valeur de retour. Mais ce à quoi je pense en m'attendant, c'est de savoir exactement pourquoi cela s'est produit et ce qui l'a causé, donc vous savez réellement quoi faire pour le réparer ou ne pas le réparer du tout. Une mauvaise entrée utilisateur est généralement très simple à corriger en regardant simplement le message d'erreur. Si vous utilisez des tampons profocol et qu'un champ obligatoire n'est pas défini, c'est normal et très simple à corriger si vous validez correctement tout ce que vous recevez sur le fil.

À ce stade, je ne comprends plus de quoi nous discutons. La trace de la pile ou la chaîne de messages d'erreur sont assez similaires si elles sont correctement implémentées. Ils réduisent l'espace de recherche et vous fournissent un contexte utile pour reproduire et corriger une erreur. Ce dont nous avons besoin, c'est de réfléchir à des moyens de simplifier la gestion des erreurs tout en fournissant suffisamment de contexte. Je ne prétends en aucun cas que la simplicité est plus importante que le contexte approprié.

C'est l'argument Java — déplacez tout le code d'erreur ailleurs afin que vous n'ayez pas à le regarder. Je pense que c'est malavisé; non seulement parce que le mécanisme de Java pour le faire a largement échoué, mais parce que lorsque je regarde du code, comment il se comportera en cas d'erreur est aussi important pour moi que comment il se comportera lorsque tout fonctionnera.

Personne n'avance cet argument. Ne confondons pas ce qui est discuté ici avec la gestion des exceptions où toute la gestion des erreurs est en un seul endroit. L'appeler "largement raté" n'est qu'une opinion, mais je ne pense pas que Go y reviendra de toute façon. La gestion des erreurs de Go est juste différente et peut être améliorée.

@creker J'ai essayé de faire valoir le même point et j'ai demandé de clarifier ce qui est considéré comme un message d'erreur significatif/utile.

La vérité est que je donnerais n'importe quel jour un texte de message d'erreur de qualité variable (qui a le parti pris du développeur qui l'écrit à ce moment-là et avec cette connaissance) en échange de la pile d'appels et des arguments de fonction. Avec un texte de message d'erreur ( fmt.Errorf ou errors.New ) vous finissez par rechercher le texte dans le code source, tout en lisant les piles/backtraces d'appels (qui sont apparemment détestés et j'espère pas pour des raisons esthétiques) correspond à une recherche directe par numéro de fichier/ligne ( errors.Wrap et similaire).

Deux styles différents, mais le but est le même : essayer de reproduire dans votre esprit ce qui s'est passé à l'exécution dans ces conditions.

Sur ce sujet, le numéro #19991 fait peut-être un résumé valable pour une approche du deuxième style de définition des erreurs significatives.

déplacez tout le code d'erreur ailleurs afin que vous n'ayez pas à le regarder

@lpar , si vous

@gdm85

vous finissez par chercher le texte dans le code source

Exactement ce que je voulais dire par des traces de pile et des messages d'erreur enchaînés similaires. Ils enregistrent tous les deux le chemin emprunté par l'erreur. Ce n'est qu'en cas de messages que vous pourriez vous retrouver avec des messages complètement inutiles qui pourraient provenir de n'importe où dans le programme si vous ne les écrivez pas assez soigneusement. Le seul avantage des erreurs chaînées est la possibilité d'enregistrer les valeurs des variables. Et même cela pourrait être automatisé en cas d'arguments de fonction ou même de variables en général et couvrirait, au moins pour moi, presque tout ce dont j'ai besoin des erreurs. Ce seraient toujours des valeurs, vous pouvez toujours les taper switch si vous en avez besoin. Mais à un moment donné, vous les enregistrerez probablement et pouvoir voir la trace de la pile est extrêmement utile.

Il suffit de regarder ce que Go fait des paniques. Vous obtenez une trace complète de chaque goroutine. Je ne me souviens pas combien de fois cela m'a aidé à identifier la cause de l'erreur et à la réparer en un rien de temps. Cela m'a souvent étonné de voir à quel point c'est facile. Il s'écoule parfaitement, tout le langage étant très prévisible, vous n'avez même pas besoin de débogueur.

Il semble y avoir une stigmatisation autour de tout ce qui concerne Java et les gens n'apportent souvent aucun argument. C'est mauvais juste parce que. Je ne suis pas fan de Java mais ce genre de raisonnement n'aide personne.

Encore une fois, les erreurs ne sont pas au développeur pour corriger les bogues. C'est l'un des avantages de la gestion des erreurs. La méthode Java a appris aux développeurs que c'est ce qu'est la gestion des erreurs, et non. Des erreurs peuvent exister au niveau de la couche d'application et au-delà au niveau d'une couche de flux. Les erreurs dans Go sont couramment utilisées pour contrôler la stratégie de récupération d'un système - au moment de l'exécution, pas au moment de la compilation.

Cela peut être incompréhensible lorsque les langages paralysent leur contrôle de flux à la suite d'une erreur en démêlant la pile et en perdant la mémoire de tout ce qu'ils ont fait avant que l'erreur ne se produise. Les erreurs sont en fait utiles au moment de l'exécution dans Go ; Je ne vois pas pourquoi ils devraient porter des choses comme des numéros de ligne - le code en cours d'exécution ne s'en soucie guère.

@comme

Encore une fois, les erreurs ne sont pas au développeur pour corriger les bogues

C'est complètement et totalement faux. Les erreurs sont exactement pour cette raison. Ils ne se limitent pas à cela, mais c'est l'une des principales utilisations. Les erreurs indiquent qu'il y a un problème avec le système et que vous devez faire quelque chose à ce sujet. Pour les erreurs attendues et faciles, vous pouvez essayer de récupérer comme, par exemple, le délai d'attente TCP. Pour quelque chose de plus sérieux, vous le videz dans les journaux et déboguez plus tard le problème.

C'est l'un des avantages de la gestion des erreurs. La méthode Java a appris aux développeurs que c'est ce qu'est la gestion des erreurs, et non.

Je ne sais pas ce que Java vous a appris, mais j'utilise des exceptions pour la même raison - pour contrôler les prises du système de stratégie de récupération au moment de l'exécution. Go n'a rien de spécial en termes de gestion des erreurs.

Cela peut être incompréhensible lorsque les langues paralysent leur contrôle de flux à la suite d'une erreur en démêlant la pile et en perdant la mémoire de tout ce qu'elles ont fait avant que l'erreur ne se produise.

Peut-être pour quelqu'un, pas pour moi.

Les erreurs sont en fait utiles au moment de l'exécution dans Go ; Je ne vois pas pourquoi ils devraient porter des choses comme des numéros de ligne - le code en cours d'exécution ne s'en soucie guère.

Si vous vous souciez de corriger les bogues dans votre code, les numéros de ligne sont le moyen de le faire. Ce n'est pas Java qui nous a appris cela, C a __LINE__ et __FUNCTION__ exactement pour cette raison. Vous souhaitez enregistrer vos erreurs et enregistrer l'endroit exact où cela s'est produit. Et quand quelque chose ne va pas, vous avez au moins un point de départ. Ce n'est pas un message d'erreur aléatoire causé par une erreur irrécupérable. Si vous n'avez pas besoin de ce genre d'informations, ignorez-les. Cela ne vous fait pas de mal. Mais au moins, il est là et peut être utilisé en cas de besoin.

Je ne comprends pas pourquoi les gens ici continuent de déplacer la conversation entre les exceptions et les valeurs d'erreur. Personne ne faisait cette comparaison. La seule chose qui a été discutée est que les traces de pile sont très utiles et contiennent beaucoup d'informations contextuelles. Si c'est incompréhensible, alors vous vivez probablement dans un univers complètement différent où le traçage n'existe pas.

C'est complètement et totalement faux.

Mais le système de production auquel je fais référence est toujours en cours d'exécution et utilise des erreurs pour le contrôle de flux, est écrit en Go et remplace une implémentation plus lente dans un langage qui utilise des traces de pile pour la propagation des erreurs.

Si c'est incompréhensible, alors vous vivez probablement dans un univers complètement différent où le traçage n'existe pas.

Pour enchaîner les informations de la pile d'appels pour chaque fonction qui renvoie un type d'erreur, faites-le à votre discrétion. Les traces de pile sont plus lentes et ne conviennent pas à une utilisation en dehors des projets de jouets pour des raisons de sécurité. C'est une faute technique de faire d'eux des citoyens Go de première classe pour simplement aider les stratégies de propagation d'erreurs irréfléchies.

si vous n'avez pas besoin de ce genre d'informations, ignorez-les. Cela ne vous fait pas de mal.

Le gonflement des logiciels est la raison pour laquelle les serveurs sont réécrits en Go. Ce que vous ne voyez pas peut encore dégrader le débit de votre pipeline.

Je préférerais des exemples de logiciels réels qui bénéficient de cette fonctionnalité au lieu d'une leçon légèrement non pertinente sur la gestion des délais d'attente TCP et le vidage des journaux.

Les traces de pile sont plus lentes

Étant donné que les traces de pile sont générées dans le chemin d'erreur, personne ne se soucie de leur lenteur. Le fonctionnement normal du logiciel a déjà été interrompu.

et ne convient pas à une utilisation en dehors des projets de jouets pour des raisons de sécurité

Jusqu'à présent, je n'ai pas encore vu un seul système de production désactiver les traces de pile pour des "raisons de sécurité", ou pas du tout d'ailleurs. D'un autre côté, être capable d'identifier rapidement le chemin emprunté par le code pour produire une erreur a été extrêmement utile. Et c'est pour les grands projets, avec de nombreuses équipes différentes travaillant sur la base de code, et personne n'ayant une connaissance complète de l'ensemble du système.

Ce que vous ne voyez pas peut encore dégrader le débit de votre pipeline.

Non, vraiment pas. Comme je l'ai dit précédemment, les traces de pile sont générées en cas d'erreur. À moins que votre logiciel ne les rencontre constamment, le débit ne sera pas affecté du tout.

Étant donné que les traces de pile sont générées dans le chemin d'erreur, personne ne se soucie de leur lenteur. Le fonctionnement normal du logiciel a déjà été interrompu.

Faux.

  • Des erreurs peuvent survenir dans le cadre d'un fonctionnement normal.
  • Les erreurs peuvent être récupérées et le programme peut continuer, donc les performances sont toujours en question.
  • Ralentir une routine sape les ressources des autres routines qui _sont_ opérant dans le bon chemin.

@object88 imagine un vrai code de production. Combien d'erreurs vous attendez-vous à ce qu'il génère ? Je ne penserais pas beaucoup. Au moins dans une application correctement écrite. Si une goroutine est dans une boucle occupée et renvoie constamment des erreurs à chaque itération, il y a quelque chose qui ne va pas avec le code. Mais même si c'est le cas, étant donné que la majorité des applications Go sont liées aux E/S, cela ne poserait pas de problème grave.

@comme

Mais le système de production auquel je fais référence est toujours en cours d'exécution et utilise des erreurs pour le contrôle de flux, est écrit en Go et remplace une implémentation plus lente dans un langage qui utilise des traces de pile pour la propagation des erreurs.

Je suis désolé mais c'est une phrase absurde qui n'a rien à voir avec ce que j'ai dit. Je ne vais pas y répondre.

Les traces de pile sont plus lentes

Plus lent mais combien ? Est-ce que ça importe? Je ne pense pas. Les applications Go sont généralement liées aux E/S. Il est stupide de courir après les cycles du processeur dans ce cas. Vous avez des problèmes beaucoup plus importants dans l'exécution de Go qui consomment du CPU. Ce n'est pas un argument pour jeter une fonctionnalité utile qui aide à corriger les bogues.

ne convient pas à une utilisation en dehors des projets de jouets pour des raisons de sécurité.

Je ne vais pas m'embêter à couvrir des "raisons de sécurité" inexistantes. Mais je voudrais vous rappeler que généralement les traces d'applications sont stockées en interne et que seuls les développeurs y ont accès. Et essayer de cacher les noms de vos fonctions est de toute façon une perte de temps. Ce n'est pas de la sécurité. J'espère que je n'ai pas besoin de m'étendre là-dessus.

Si vous insistez sur des raisons de sécurité, j'aimerais que vous pensiez à macOS/iOS, par exemple. Ils n'ont aucun problème à lancer des paniques et des vidages sur incident qui contiennent des piles de tous les threads et des valeurs de tous les registres du processeur. Ne les voyez pas affectés par ces « raisons de sécurité ».

C'est une faute technique de faire d'eux des citoyens Go de première classe pour simplement aider les stratégies de propagation d'erreurs irréfléchies.

Pourriez-vous être plus subjectif ? « stratégies de propagation d'erreurs irréfléchies » où avez-vous vu cela ?

Le gonflement des logiciels est la raison pour laquelle les serveurs sont réécrits en Go. Ce que vous ne voyez pas peut encore dégrader le débit de votre pipeline.

Encore une fois, de combien ?

Je préférerais des exemples de logiciels réels qui bénéficient de cette fonctionnalité au lieu d'une leçon légèrement non pertinente sur la gestion des délais d'attente TCP et le vidage des journaux.

À ce stade, il semble que je parle avec n'importe qui sauf un programmeur. Le traçage profite à tous les logiciels. C'est une technique courante dans toutes les langues et tous les types de logiciels qui aide à corriger les bogues. Vous pouvez lire wikipedia si vous souhaitez plus d'informations à ce sujet.

Avoir autant de discussions improductives sans aucun consensus signifie qu'il n'y a pas de moyen élégant de résoudre ce problème.

@ objet88
Les traces de pile peuvent être lentes si vous souhaitez suivre toutes les goroutines, car Go doit attendre que les autres goroutines se débloquent.
Si vous ne faites que tracer la goroutine que vous exécutez actuellement, ce n'est pas si lent.

@creker
Le traçage profite à tous les logiciels, mais cela dépend de ce que vous tracez. Dans la plupart des projets Go auxquels j'ai participé, le traçage des piles n'était pas une bonne idée, car la concurrence est impliquée. Les données se déplacent d'avant en arrière, beaucoup de choses communiquent entre elles et certaines goroutines ne sont que quelques lignes de code. Avoir une trace de pile dans un tel cas ne sert à rien.
C'est pourquoi j'utilise des erreurs enveloppées d'informations contextuelles écrites dans le journal pour recréer la même trace de pile, mais qui n'est pas liée à la pile goroutine réelle, mais à la logique de l'application elle-même.
Pour que je puisse faire cat *.log | grep "orderID=xxx" et obtenez la trace de la pile de la séquence réelle d'actions qui a conduit à une erreur.
En raison de la nature concurrente de Go, les erreurs riches en contexte sont plus précieuses que les traces de pile.

@gladkikhartem merci d'avoir pris le temps d'écrire un argument approprié. Je commençais à être frustré par cette conversation.

Je comprends votre argument et suis en partie d'accord avec lui. Pourtant, je me retrouve à devoir faire face à des piles d'au moins 5 fonctions de profondeur. C'est déjà assez grand pour pouvoir comprendre ce qui se passe et où vous devriez commencer à chercher. Mais dans une application hautement concurrente avec beaucoup de très petites traces de piles de goroutines, elles perdent leurs avantages. Avec quoi je suis d'accord.

@creker

imaginez un vrai code de production. Combien d'erreurs vous attendez-vous à ce qu'il génère ? [...], étant donné que la majorité des applications Go sont liées par IO, même cela ne serait pas un problème grave.

C'est bien que vous mentionniez les opérations liées aux E/S. La méthode io.Reader Read renvoie une erreur saine à EOF. Cela se produira donc beaucoup dans le chemin heureux.

@urandom

Les traces de pile exposent involontairement des informations précieuses pour le profilage d'un système.

  • Noms d'utilisateur
  • Chemins du système de fichiers
  • Type/version de base de données principale
  • Flux de transactions
  • Structure de l'objet
  • Algorithmes de chiffrement

Je ne sais pas si l'application moyenne remarquerait la surcharge liée à la collecte de trames de pile dans les types d'erreur, mais je peux vous dire que pour les applications critiques en termes de performances, de nombreuses petites fonctions Go sont insérées manuellement en raison de la surcharge actuelle des appels de fonction. Le traçage ne fera qu'empirer les choses.

Je pense que l'objectif de Go est d'avoir un logiciel simple et rapide, et le traçage serait un pas en arrière. Nous devrions être capables d'écrire de petites fonctions et de renvoyer des erreurs à partir de ces fonctions sans dégradation des performances qui encourage les types d'erreurs non conventionnels et la mise en ligne manuelle.

@creker

J'éviterai de vous donner des exemples qui provoquent d'autres dissonances. Je suis désolé de vous avoir frustré.

Je proposerais d'utiliser un nouveau mot clé " returnif " dont le nom révèle instantanément sa fonction. De plus, il est suffisamment flexible pour pouvoir être utilisé dans plus de cas d'utilisation que la gestion des erreurs.

Exemple 1 (en utilisant le retour nommé) :

a, erreur = quelque chose (b)
si erreur != nil {
retourner
}

Deviendrait:

a, erreur = quelque chose (b)
returnif err != nil

Exemple 2 (sans utiliser de retour nommé) :

a, err := quelque chose(b)
si erreur != nil {
retourner un, euh
}

Deviendrait:

a, err := quelque chose(b)
returnif err != nil { a, err }

Concernant votre exemple de retour nommé, voulez-vous dire...

a, err = something(b)
returnif err != nil

@ambernardino
Pourquoi ne pas simplement mettre à jour l'outil fmt à la place et vous n'avez pas besoin de mettre à jour la syntaxe du langage et d'ajouter de nouveaux mots-clés inutiles

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

ou

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

@gladkikhartem l'idée n'est pas de taper cela à chaque fois que vous voulez propager l'erreur, je préfère cela et devrait fonctionner de la même manière

a, err? := something(b)

@mrkaspa
L'idée est de rendre le code plus lisible . La saisie de code n'est pas un problème, la lecture l'est.

@gladkikhartem rust utilise cette approche et je ne pense pas que cela la rende moins lisible

@gladkikhartem Je ne pense pas que ? rende moins lisible. Je dirais qu'il élimine complètement le bruit. Le problème pour moi est qu'avec le bruit, cela élimine également la possibilité de fournir un contexte utile. Je ne vois tout simplement pas où vous pourriez brancher le message d'erreur habituel ou envelopper les erreurs. La pile d'appels est une solution évidente mais, comme déjà mentionné, ne fonctionne pas pour tout le monde.

@mrkaspa
Et je pense que ça le rend moins lisible, quelle est la suite ? Essayons de trouver la meilleure solution ou partageons-nous simplement des opinions?

@creker
'?' le personnage ajoute une charge cognitive au lecteur, car ce qui va être renvoyé n'est pas si évident et bien sûr, la personne doit savoir ce qu'elle fait. Et bien sûr ? signe soulève des questions dans l'esprit du lecteur.

Comme je l'ai dit précédemment, si vous voulez vous débarrasser de err != nil, le compilateur peut détecter les paramètres d'erreur inutilisés et les transmettre lui-même.
Et

a, err? := doStuff(a,b)
err? := doAnotherStuff(b,z,d,g)
a, b, err? := doVeryComplexStuff(b)

deviendra plus lisible

a := doStuff(a,b)
doAnotherStuff(b,z,d,g)
a, b := doVeryComplexStuff(b)

même magie, juste moins de choses à taper et moins de choses à penser

@gladkikhartem eh bien, je ne pense pas qu'il existe une solution qui n'obligerait pas les lecteurs à apprendre quelque chose de nouveau. C'est la conséquence du changement de langue. Nous devons faire un compromis - soit nous vivons avec une syntaxe verbeuse dans votre visage qui montre exactement ce qui est fait en termes primitifs, soit nous introduisons une nouvelle syntaxe qui pourrait masquer la verbosité, ajouter du sucre de syntaxe, etc. Il n'y a pas d'autre moyen. Le simple fait de rejeter tout ce qui ajoute quelque chose à apprendre pour le lecteur est contre-productif. Nous pourrions aussi bien fermer tous les problèmes de Go2 et l'appeler un jour.

Quant à votre exemple, il introduit encore plus de choses magiques et masque tout point d'injection pour introduire une syntaxe qui permettrait au développeur de fournir un contexte aux erreurs. Et, plus important encore, il masque complètement toute information sur l'appel de fonction susceptible de générer une erreur. Cela sent de plus en plus les exceptions. Et si nous sommes sérieux au sujet de simplement relancer les erreurs, alors les traces de pile deviennent un must car c'est la seule façon de conserver le contexte dans ce cas.

La proposition originale couvre en fait déjà assez bien tout cela. Il est suffisamment détaillé et vous donne un bon endroit pour envelopper l'erreur et fournir un contexte utile. Mais l'un des principaux problèmes est cette « erreur » magique. Je pense que c'est moche parce que ce n'est pas assez magique et pas assez verbeux. C'est un peu au milieu. Ce qui pourrait l'améliorer, c'est d'introduire plus de magie.

Et si || produisait une nouvelle erreur qui encapsule automatiquement l'erreur d'origine. Donc l'exemple devient comme ça

func Chdir(dir string) error {
    syscall.Chdir(dir) || &PathError{"chdir", dir}
    return nil
}

err disparaît et tout l'emballage est géré implicitement. Nous avons maintenant besoin d'un moyen d'accéder à ces erreurs internes. Je pense que l'ajout d'une autre méthode comme Inner() error à l'interface error ne fonctionnera pas. Une façon consiste à introduire une fonction intégrée comme unwrap(error) []error . Il renvoie une tranche de toutes les erreurs internes dans l'ordre dans lequel elles ont été encapsulées. De cette façon, vous pouvez accéder à toute erreur interne ou plage sur eux.

L'implémentation de ceci est discutable étant donné que error n'est qu'une interface et vous avez besoin d'un endroit où mettre ces erreurs encapsulées pour tout type error .

Pour moi, cela coche toutes les cases mais pourrait être un peu trop magique. Mais, étant donné que l'interface d'erreur est assez spéciale par définition, la rapprocher du citoyen de première classe n'est peut-être pas une mauvaise idée. La gestion des erreurs est détaillée car il s'agit simplement d'un code Go normal, il n'y a rien de spécial à ce sujet. Cela peut être bon sur le papier et pour faire les gros titres flashy, mais les erreurs sont trop spéciales pour justifier un tel traitement. Ils ont besoin d'un boîtier spécial.

La proposition originale vise-t-elle à réduire le nombre de contrôles d'erreurs ou la durée de chaque contrôle individuel ?

Si c'est ce dernier, alors il est trivial de réfuter la nécessité des propositions au motif qu'il n'y a qu'une seule déclaration conditionnelle et une seule déclaration de répétition. Certaines personnes n'aiment pas les boucles for, devrions-nous également fournir une construction de boucle implicite ?

Les changements de syntaxe proposés jusqu'à présent ont servi d'expérience de pensée intéressante, mais aucun d'entre eux n'est aussi clair, prononçable ou simple que l'original. Go n'est pas bash et rien ne devrait être "magique" concernant les erreurs.

De plus en plus je lis ces propositions, de plus en plus je vois des gens dont les arguments ne sont que "ça ajoute quelque chose de nouveau, donc c'est mauvais, illisible, laissez tout tel quel".

@as la proposition donne le résumé de ce qu'elle essaie de réaliser. Ce qui est fait est assez bien défini.

aussi clair, prononçable ou simple que l'original

Toute proposition introduira une nouvelle syntaxe et le fait d'être nouveau pour certaines personnes sonne comme « illisible, compliqué, etc. etc ». Ce n'est pas parce qu'il est nouveau qu'il est moins clair, prononçable ou simple. "||" et "?" les exemples sont aussi clairs et simples que la syntaxe existante une fois que vous savez ce qu'elle fait. Ou devrions-nous commencer à nous plaindre que "->" et "<-" sont trop magiques et que le lecteur doit savoir ce qu'ils signifient ? Remplaçons-les par des appels de méthode.

Go n'est pas bash et rien ne devrait être "magique" concernant les erreurs.

C'est totalement infondé et ne compte pas comme argument pour quoi que ce soit. Qu'est-ce que cela a à voir avec Bash me dépasse.

@creker
Oui, je suis tout à fait d'accord avec toi qui a introduit un événement plus magique. Mon exemple est juste une continuation de ? idée de l'opérateur de taper moins de choses.

Je suis d'accord que nous devons sacrifier quelque chose et introduire du changement et bien sûr de la magie. C'est juste un équilibre entre les avantages de la convivialité et les inconvénients d'une telle magie.

Origine || la proposition est plutôt sympa et testée dans la pratique, mais le formatage est moche à mon avis, je suggérerais de changer le formatage en

syscal.Chdir(dir)
    || return &PathError{"chdir", dir}

PS : que pensez-vous d'une telle variante de la syntaxe magique ?

syscal.Chdir(dir) {
    return &PathError{"chdir", dir}
}

@gladkikhartem les deux ont l'air plutôt bien du point de vue de la lisibilité mais j'ai un mauvais pressentiment de ce dernier. Il introduit cette étrange portée de bloc dont je ne suis pas si sûr.

Je vous encourage à ne pas regarder la syntaxe isolément, mais plutôt dans le contexte d'une fonction. Cette méthode a quelques blocs de gestion des erreurs différents.

func (l *Loader) processDirectory(p *Package) (*build.Package, error) {
        absPath, err := p.preparePath()
        if err != nil {
                return nil, err
        }
    fis, err := l.context.ReadDir(absPath)
    if err != nil {
        return nil, err
    } else if len(fis) == 0 {
        return nil, nil
    }

    buildPkg, err := l.context.Import(".", absPath, 0)
    if err != nil {
        if _, ok := err.(*build.NoGoError); ok {
            // There isn't any Go code here.
            return nil, nil
        }
        return nil, err
    }

    return buildPkg, nil
}

Comment les changements proposés nettoient-ils cette fonction ?

func (l *Loader) processDirectory(p *Package) (*build.Package, error) {
    absPath, err? := p.preparePath()
    fis, err? := l.context.ReadDir(absPath)
    if len(fis) == 0 {
        return nil, nil
    }

    buildPkg, err := l.context.Import(".", absPath, 0)
    if err != nil {
         if _, ok := err.(*build.NoGoError); ok {
             // There isn't any Go code here.
             return nil, nil
         }
         return nil, err
    }

    return buildPkg, nil
}

La proposition @bcmills correspond mieux.

func (l *Loader) processDirectory(p *Package) (*build.Package, error) {
    absPath := p.preparePath()        =? err { return nil, err }
    fis := l.context.ReadDir(absPath) =? err { return nil, err }
    if len(fis) == 0 {
        return nil, nil
    }
    buildPkg := l.context.Import(".", absPath, 0) =? err {
        if _, ok := err.(*build.NoGoError); ok {
            // There isn't any Go code here.
            return nil, nil
        }
        return nil, err
    }
    return buildPkg, nil
}

@ objet88

func (l *Loader) processDirectory(p *Package) (p *build.Package, err error) {
        absPath, err := p.preparePath() {
        return nil, fmt.Errorf("prepare path: %v", err)
    }
    fis, err := l.context.ReadDir(absPath) {
        return nil, fmt.Errorf("read dir: %v", err)
    }
    if len(fis) == 0 {
        return nil, nil
    }

    buildPkg, err := l.context.Import(".", absPath, 0) {
        err, ok := err.(*build.NoGoError)
                if !ok {
            return nil, fmt.Errorf("buildpkg: %v",err)
        }
        return nil, nil
    }
    return buildPkg, nil
}

Continuer à fouiller là-dessus. Peut-être pouvons-nous trouver des exemples d'utilisation plus complets.

@erwbgy , celui-ci me semble le meilleur, mais je ne suis pas convaincu que le paiement soit si bon.

  • Quelles sont les valeurs de retour sans erreur ? Sont-ils toujours la valeur zéro ? S'il _était_ une valeur précédemment attribuée pour un retour nommé, est-ce que cela est remplacé ? Si la valeur nommée est utilisée pour stocker le résultat d'une fonction erronée, est-ce que cela est renvoyé ?
  • L'opérateur ? peut-il être appliqué à une valeur sans erreur ? Pourrais-je faire quelque chose comme (!ok)? ou ok!? (ce qui est un peu bizarre, car vous regroupez l'affectation et l'exploitation) ? Ou cette syntaxe n'est-elle bonne que pour error ?

@rodcorsi , celui-ci me ReadDir mais ReadBuildTargetDirectoryForFileInfo ou quelque chose de long comme ça. Ou peut-être avez-vous un grand nombre d'arguments. La gestion des erreurs pour preparePath serait également repoussée hors de l'écran. Sur un appareil avec une taille d'écran horizontale limitée (ou une fenêtre d'affichage qui n'est pas si large, comme Github), vous risquez de perdre la partie =? . Nous sommes très bons en défilement vertical ; pas tellement à l'horizontale.

@gladkikhartem , il semble qu'il soit lié à un argument (seulement le dernier ?) implémentant l'interface error . Cela ressemble beaucoup à une déclaration de fonction, et c'est juste... bizarre. Existe-t-il un moyen de le lier à une valeur de retour de style ok ? Dans l'ensemble, vous n'achetez qu'une seule ligne.

@ objet88
l'encapsulation de mots résout des problèmes de code très étendus. n'est-il pas largement utilisé ?

@ object88 concernant l'appel de fonction très long. Traitons le problème principal ici. Le problème n'est pas la gestion des erreurs poussée hors de l'écran. Le problème est un nom de fonction long et/ou une grande liste d'arguments. Cela doit être corrigé avant que l'on puisse argumenter sur le fait que la gestion des erreurs est hors écran.

Je n'ai pas encore vu d'éditeur de texte IDE ou convivial qui a été configuré pour le retour à la ligne par défaut. Et je n'ai pas trouvé de moyen de le faire avec Github à part le piratage manuel du CSS après le chargement de la page.

Et je pense que la largeur du code est un facteur important -- cela parle de _lisibilité_, qui est l'impulsion pour cette proposition. L'affirmation est qu'il y a "trop ​​de code" autour des erreurs. Non pas que la fonctionnalité n'est pas là, ou que les erreurs doivent être implémentées d'une autre manière, mais que le code ne se lit pas bien.

@ objet88
oui, ce code fonctionnera pour toute fonction renvoyant une interface d'erreur comme dernier paramètre.

En ce qui concerne l'économie de ligne, vous ne pouvez pas mettre plus d'informations dans moins de lignes. Le code doit être réparti uniformément, pas trop dense et ne pas avoir d'espace après chaque instruction.
Je suis d'accord que cela ressemble à une déclaration de fonction, mais en même temps, c'est très similaire à existant si ...; err != nil { instruction, afin que les gens ne soient pas trop confus.

La largeur du code est un facteur important. Que faire si j'ai un éditeur de 80 lignes et 80 lignes de code est un appel de fonction et après cela j'ai || erreur ? Je ne pourrai tout simplement pas identifier que cette fonction renvoie quelque chose, car ce que je lis sera un code go valide sans aucun retour.

Juste pour être complet, je vais lancer un exemple avec la syntaxe || , mon emballage d'erreur automatique et la mise à zéro automatique des valeurs de retour sans erreur

func (l *Loader) processDirectory(p *Package) (*build.Package, error) {
        absPath := p.preparePath() || errors.New("prepare path")
    fis := l.context.ReadDir(absPath) || errors.New("ReadDir")
    if len(fis) == 0 {
        return nil, nil
    }

    buildPkg, err := l.context.Import(".", absPath, 0)
    if err != nil {
        if _, ok := err.(*build.NoGoError); ok {
            // There isn't any Go code here.
            return nil, nil
        }
        return nil, err
    }

    return buildPkg, nil
}

Concernant votre question sur les autres valeurs de retour. En cas d'erreur, ils auront une valeur nulle dans tous les cas. J'ai déjà expliqué pourquoi je pense que c'est important.

Le problème est que votre exemple n'est pas si impliqué pour commencer. Mais cela montre encore ce que cette proposition, du moins pour moi, représente. Ce que je veux qu'il résolve, c'est l'idiome le plus courant et le plus utilisé

err := func()
if err != nil {
    return err
}

Nous pouvons tous être d'accord que ce type de code est une grande partie (sinon la plus grande) de la gestion des erreurs en général. Il est donc logique de résoudre ce cas. Et si vous voulez faire quelque chose de plus impliqué avec l'erreur, appliquez un peu de logique - allez-y. C'est là que la verbosité devrait être là où il y a une logique réelle à lire et à comprendre pour le programmeur. Ce dont nous n'avons pas besoin, c'est de perdre de l'espace et du temps à lire un texte passe-partout insensé. C'est une partie insensée mais toujours essentielle du code Go.

En ce qui concerne les discussions précédentes sur le retour implicite des valeurs zéro. Si vous devez retourner une valeur significative en cas d'erreur, allez-y. Encore une fois, la verbosité est bonne ici et aide à comprendre le code. Rien de mal à abandonner le sucre syntaxique si vous devez faire quelque chose de plus compliqué. Et || est suffisamment flexible pour résoudre les deux cas. Vous pouvez omettre les valeurs sans erreur et elles seront implicitement remises à zéro. Ou vous pouvez les spécifier explicitement si vous en avez besoin. Je me souviens qu'il existe même une proposition distincte pour cela qui implique également des cas où vous souhaitez renvoyer une erreur et mettre à zéro tout le reste.

@ objet88

L'affirmation est qu'il y a "trop ​​de code" autour des erreurs.

Ce n'est pas n'importe quel code. Le problème principal est qu'il y a trop de passe-partout sans signification autour des erreurs et des cas très courants de gestion des erreurs. Verbosité importante quand il y a quelque chose d'une valeur à lire. Il n'y a rien de valable dans if err == nil then return err sauf que vous voulez renvoyer l'erreur. Pour une logique aussi primitive, cela prend beaucoup de place. Et plus vous avez de logique, d'appels de bibliothèque, de wrappers, etc. qui pourraient tous très bien renvoyer une erreur, plus ce passe-partout commence à dominer des éléments importants - la logique réelle de votre code. Et cette logique peut en fait contenir une logique de gestion des erreurs importante. Mais il se perd dans cette nature répétitive de la plupart des passe-partout qui l'entourent. Et cela peut être résolu et d'autres langues modernes avec lesquelles Go est en concurrence tentent de résoudre cela. Parce que la gestion des erreurs est si importante, ce n'est pas seulement un code normal.

@creker
Je suis d'accord que si err != nil return err est trop passe-partout, ce que nous craignons, c'est que si nous créons un moyen facile de simplement transférer les erreurs vers le haut de la pile - statistiquement, les programmeurs, en particulier les juniors, utiliseront la méthode la plus simple, plutôt ce qui est approprié dans une certaine situation.
C'est la même idée avec la gestion des erreurs de Go - cela vous oblige à faire une chose décente.
Donc, dans cette proposition, nous voulons encourager les autres à gérer et à envelopper les erreurs de manière réfléchie.

Je dirais que nous devrions faire en sorte que la gestion des erreurs simples semble moche et longue à mettre en œuvre, mais une gestion des erreurs gracieuse avec des traces d'emballage ou de pile semble agréable et facile à faire.

@gladkikhartem J'ai toujours trouvé idiot cet argument sur les anciens éditeurs. Qui s'en soucie et pourquoi la langue devrait en souffrir ? Nous sommes en 2018, à peu près tout le monde a un grand écran et un éditeur décent. La très très petite minorité ne devrait pas influencer tout le monde. Cela devrait être l'inverse - la minorité devrait s'en occuper elle-même. Faites défiler, habillage de mots, n'importe quoi.

@gladkikhartem Go a déjà ce problème et je ne pense pas que nous puissions faire quoi que ce soit à ce sujet. Les développeurs seront toujours paresseux jusqu'à ce que vous les forciez avec une compilation échouée ou une panique à l'exécution, ce que Go ne fait pas.

Ce que Go fait en fait, c'est de ne rien forcer. L'idée que Go vous oblige à gérer les erreurs est trompeuse et l'a toujours été. Les auteurs de Go vous obligent à le faire dans leurs articles de blog et leurs conférences. La langue réelle vous laisse faire tout ce que vous voulez. Et le principal problème ici est ce que Go choisit par défaut - par défaut, l'erreur est ignorée en silence. Il y a même une proposition pour changer cela. Si Go visait à vous forcer à faire une chose décente, alors il devrait faire ce qui suit. Soit renvoyer une erreur au moment de la compilation, soit paniquer au moment de l'exécution si l'erreur renvoyée n'est pas gérée correctement. D'après ce que je comprends, Rust fait ceci - les erreurs paniquent par défaut. C'est ce que j'appelle forcer à bien faire.

Ce qui m'a forcé à gérer les erreurs dans Go, c'est ma conscience de développeur, rien d'autre. Mais il est toujours tentant de céder. Pour le moment, si je ne me lance pas explicitement dans la lecture de la signature de la fonction, personne ne me dira quoi que ce soit que cela renvoie une erreur. Il y a un exemple réel. Pendant longtemps, j'ignorais que fmt.Println renvoyait une erreur. Je n'ai aucune utilité pour sa valeur de retour, je veux juste imprimer des trucs. Je n'ai donc aucune incitation à regarder ce que cela renvoie. C'est le même problème que C. Les erreurs sont des valeurs et vous pouvez les ignorer autant que vous le souhaitez jusqu'à ce que votre code se brise au moment de l'exécution et vous n'en saurez rien car il n'y a pas de plantage avec une panique utile comme, par exemple, avec des exceptions non gérées.

@gladkikhartem d' après ce que je comprends de cette proposition, il ne s'agit pas d'encourager les développeurs à envelopper les erreurs de manière réfléchie. Il s'agit d'encourager ceux qui feront des propositions à ne pas oublier de couvrir cela. Parce que souvent, les gens proposent des solutions qui ne font que relancer l'erreur et oublient que vous voulez en fait lui donner plus de contexte et ensuite seulement la relancer.

J'écris cette proposition principalement pour encourager les personnes qui souhaitent simplifier la gestion des erreurs Go à réfléchir à des moyens de faciliter l'encapsulation du contexte autour des erreurs, et pas seulement de renvoyer l'erreur non modifiée.

@creker
Mon éditeur a une largeur de 100 caractères, car j'ai l'explorateur de fichiers, la console git et etc.... , dans notre équipe personne n'écrit un code de plus de 100 caractères, c'est juste idiot (à quelques exceptions près)

Go ne force pas la gestion des erreurs, contrairement aux linters. (Peut-être qu'on devrait juste écrire un linter pour ça ?)

D'accord, si nous ne pouvons pas trouver de solution et que chacun comprend la proposition à sa manière - pourquoi ne pas spécifier certaines exigences pour ce dont nous avons besoin ? s'entendre sur les exigences d'abord, puis développer une solution serait une tâche beaucoup plus facile.

Par example:

  1. La syntaxe de la nouvelle proposition devrait avoir une instruction return dans le texte, sinon ce qui se passe n'est pas évident pour le lecteur. ( d 'accord en désaccord )
  2. La nouvelle proposition doit prendre en charge les fonctions qui renvoient plusieurs valeurs ( d'accord / pas d'accord )
  3. La nouvelle proposition devrait prendre moins de place (1 ligne, 2 lignes, pas d'accord)
  4. La nouvelle proposition doit être capable de gérer des expressions très longues (d'accord / pas d'accord)
  5. La nouvelle proposition devrait permettre plusieurs déclarations en cas d'erreur (d'accord / pas d'accord)
  6. .....

@creker , environ 75% de mon développement se fait sur un ordinateur portable 15" en VSCode. Je maximise mon espace horizontal, mais il y a toujours une limite, surtout si je fais du montage côte à côte. Je parierais ça chez les étudiants , il y a beaucoup plus d'ordinateurs portables que d'ordinateurs de bureau. Je ne voudrais pas limiter l'accessibilité du langage car nous prévoyons que tout le monde aura des moniteurs grand format.

Et malheureusement, quelle que soit la taille de votre écran, github limite toujours la fenêtre d'affichage.

@gladkikhartem

La paresse des novices est applicable ici, mais l'utilisation libérale de errors.New dans certains de ces exemples démontre également un manque de compréhension de la langue. Les erreurs ne doivent pas être allouées dans les valeurs de retour à moins qu'elles ne soient dynamiques, et si ces erreurs étaient placées dans une variable de portée de package comparable, la syntaxe serait plus courte sur la page et réellement acceptable dans le code de production également. Ceux qui souffrent le plus de la « passe-partout » de la gestion des erreurs Go prennent le plus de raccourcis et n'ont pas assez d'expérience pour gérer correctement les erreurs.

Ce qui constitue simplifying error handling n'est pas évident, mais le précédent est que less runes != simple . Je pense qu'il existe quelques qualificatifs de simplicité qui peuvent mesurer une construction de manière quantifiable :

  • Le nombre de façons dont la construction est exprimée
  • La similitude de cette construction avec d'autres constructions et la cohésion entre ces constructions
  • Le nombre d'opérations logiques résumées par la construction
  • La similitude de la construction avec le langage naturel (c'est-à-dire l'absence de négation, etc.)

Par exemple, la proposition originale augmente le nombre de façons de propager les erreurs de 2 à 3. C'est similaire au OU logique, mais a une sémantique différente. Il résume un retour conditionnel de faible complexité (comparé à copy ou append , ou >> ). La nouvelle méthode est moins naturelle que l'ancienne, et si elle était prononcée à voix haute, ce serait probablement abs, err := path(foo) || return err -> if theres an error, it's returning err auquel cas ce serait un mystère pourquoi il est possible d'utiliser les barres verticales si vous peut l'écrire de la même manière que c'est dit à haute voix dans une revue de code.

@comme
Tout à fait d'accord pour dire que less runes != simple .
Par simple, je veux dire lisible et compréhensible.
Pour que toute personne qui n'est pas familiarisée avec go devrait le lire et comprendre ce qu'il fait.
Cela devrait être comme une blague - vous n'avez pas à l'expliquer.

La gestion des erreurs actuelle est en fait compréhensible, mais pas complètement lisible si vous avez trop de if err != nil return.

@ object88 ça va. J'ai dit plus en général car cet argument revient assez fréquemment. Comme, imaginons un ancien écran de terminal ridicule qui pourrait être utilisé pour écrire Go. Quel genre d'argument est-ce? Où est la limite de son ridicule ? Si nous sommes sérieux à ce sujet, nous devrions observer des faits concrets : quelle est la taille et la résolution d'écran les plus populaires. Et seulement de cela, nous pouvons tirer quelque chose. Mais l'argument imagine généralement une taille d'écran que personne n'utilise, mais il y a une petite possibilité que quelqu'un puisse le faire.

@gladkikhartem non, les linters ne vous obligent pas, suggèrent-ils. Il y a une grande différence ici. Ils sont facultatifs, ne font pas partie de la chaîne d'outils Go et ne font que des suggestions. Le forçage ne peut signifier que deux choses - une erreur de compilation ou d'exécution. Tout le reste est une suggestion et une option à choisir.

Je suis d'accord, nous devrions mieux formuler ce que nous voulons parce que la proposition ne couvre pas complètement tous les aspects.

@comme

La nouvelle méthode est moins naturelle que l'ancienne, et si elle était prononcée à voix haute, ce serait probablement des abdos, err := path(foo) || return err -> s'il y a une erreur, elle renvoie err, auquel cas ce serait un mystère pourquoi il est possible d'utiliser les barres verticales si vous pouvez l'écrire de la même manière que c'est dit à haute voix dans une revue de code.

La nouvelle méthode n'est moins naturelle que pour une seule raison - elle ne fait pas partie du langage en ce moment. Il n'y a pas d'autre raison. Imaginez Go déjà avec cette syntaxe - ce serait naturel parce que vous la connaissez. Tout comme vous êtes familier avec -> , select , go et d'autres choses qui ne sont pas présentes dans d'autres langues. Pourquoi est-il possible d'utiliser des barres verticales au lieu de retour ? Je réponds par une question. Pourquoi existe-t-il un moyen d'ajouter des tranches dans un seul appel alors que vous pouvez faire la même chose avec une boucle ? Pourquoi existe-t-il un moyen de copier des éléments de l'interface de lecture à l'interface d'écriture en un seul appel alors que vous pouvez faire la même chose avec la boucle ? etc etc etc Parce que vous voulez que votre code soit plus compact et plus lisible. Vous avancez ces arguments alors que Go les contredit déjà avec de nombreux exemples. Encore une fois, soyons plus ouverts et n'abattons rien simplement parce que c'est nouveau et pas déjà dans la langue. Nous n'obtiendrons rien avec cela. Il y a un problème, beaucoup de gens demandent une solution, traitons-le. Le go n'est pas une langue idéale sacrée qui sera profanée par tout ce qui y est ajouté.

Pourquoi existe-t-il un moyen d'ajouter des tranches dans un seul appel alors que vous pouvez faire la même chose avec une boucle ?

Écrire une vérification d'erreur d'instruction if est trivial, je serais intéressé de voir votre implémentation de append .

Pourquoi existe-t-il un moyen de copier des éléments de l'interface de lecture à l'interface d'écriture en un seul appel alors que vous pouvez faire la même chose avec la boucle ?

Un lecteur et un rédacteur font abstraction des sources et des destinations d'une opération de copie, de la stratégie de mise en mémoire tampon et parfois même des valeurs sentinelles dans la boucle. Vous ne pouvez pas exprimer cette abstraction avec une boucle et une tranche.

Vous avancez ces arguments alors que Go les contredit déjà avec de nombreux exemples.

Je ne crois pas que ce soit le cas, du moins pas avec ces exemples.

Encore une fois, soyons plus ouverts et n'abattons rien simplement parce que c'est nouveau et pas déjà dans la langue.

Étant donné que Go a une garantie de compatibilité, vous devriez scruter le plus les nouvelles fonctionnalités car vous devrez les gérer pour toujours si elles sont terribles. Ce que personne n'a fait jusqu'à présent, c'est de créer une véritable preuve de concept et de l'utiliser avec une petite équipe de développement.

Si vous regardez l'historique de certaines propositions (par exemple, les génériques), vous verrez qu'après avoir fait cela, la réalisation est souvent : "wow, ce n'est en fait pas une bonne solution, n'apportons pas encore de changements". L'alternative est un langage plein de suggestions et aucun moyen facile de les expulser rétroactivement.

À propos de l'écran large vs mince, une autre chose à considérer est le multitâche .

Vous pouvez avoir plusieurs fenêtres côte à côte pour garder occasionnellement une trace de quelque chose d'autre pendant que vous rassemblez un peu de code, plutôt que de simplement regarder l'éditeur, basculer complètement les contextes vers une autre fenêtre pour rechercher une fonction, peut-être StackOverflow, et revenez à l'éditeur.

@comme
Tout à fait d'accord pour dire que la plupart des fonctionnalités proposées ne sont pas pratiques, et je commence à penser que || et ? des trucs pourraient être le cas.

@creker
copy() et append() ne sont pas des tâches triviales à implémenter

J'ai des linters sur CI/CD et ils m'obligent littéralement à gérer toutes les erreurs. Ils ne font pas partie du langage, mais peu importe - j'ai juste besoin de résultats.
(et au fait, j'ai une opinion bien arrêtée - si quelqu'un n'utilise pas de linters dans Go - il est juste ........)

À propos de la taille de l'écran - ce n'est même pas drôle, sérieusement. Merci d'arrêter cette discussion sans intérêt. Votre écran peut être aussi large que vous le souhaitez - vous aurez toujours une probabilité qu'une partie du code || return &PathError{Err:err} ne soit pas visible. Il suffit de rechercher sur Google le mot "ide" et de voir quel type d'espace est disponible pour le code.

Et s'il vous plaît, lisez attentivement le texte des autres, je n'ai pas dit que Go vous oblige à gérer toutes les erreurs

C'est la même idée avec la gestion des erreurs de Go - cela vous oblige à faire une chose décente.

@gladkikhartem Go ne force rien en termes de gestion des erreurs, c'est le problème. Chose décente ou pas, ça n'a pas d'importance, c'est juste de la cueillette. Même si pour moi, cela signifie gérer toutes les erreurs dans tous les cas, à l'exception peut-être de choses comme fmt.Println .

si quelqu'un n'utilise pas de linters dans Go - il est juste

Peut-être être. Mais si quelque chose n'est pas vraiment forcé, alors ça ne va pas voler. Certains l'utiliseront, d'autres non.

À propos de la taille de l'écran - ce n'est même pas drôle, sérieusement. Merci d'arrêter cette discussion sans intérêt.

Je ne suis pas celui qui a commencé à lancer des nombres aléatoires qui devraient en quelque sorte affecter la prise de décision. J'affirme clairement que je comprends le problème mais qu'il doit être objectif. Pas "J'ai un IDE de 80 symboles, Go devrait en tenir compte et ignorer tout le monde".

Si nous parlons de la taille de mon écran. le code du studio visuel me donne 270 symboles d'espace horizontal. Je ne vais pas prétendre qu'il est normal de prendre autant de place. Mais mon code peut facilement dépasser 120 symboles lorsque l'on prend en compte les structs avec des commentaires et des types de champs particulièrement longs. Si je devais utiliser la syntaxe || elle entrerait facilement dans 100-120 en cas d'appel de fonction à 3-5 arguments et d'erreur enveloppée avec un message personnalisé.

Sinon, si quelque chose comme || devait être implémenté, gofmt ne devrait probablement pas vous forcer à l'écrire sur une seule ligne. Dans certains cas, cela peut très bien prendre trop de place.

@erwbgy , celui-ci me semble le meilleur, mais je ne suis pas convaincu que le paiement soit si bon.

@ object88 Le gain pour moi est qu'il supprime le passe-partout commun pour une gestion simple des erreurs et n'essaie pas d'en faire trop. Il ne fait que :

val, err := func()
if err != nil {
    return nil, errors.WithStack(err)
}

plus simple :

val, err? := func()

Rien n'empêche de traiter les erreurs plus complexes de la manière actuelle.

Quelles sont les valeurs de retour sans erreur ? Sont-ils toujours la valeur zéro ? S'il y avait une valeur précédemment attribuée pour un retour nommé, est-ce que cela est remplacé ? Si la valeur nommée est utilisée pour stocker le résultat d'une fonction erronée, est-ce que cela est renvoyé ?

Tous les autres paramètres de retour sont des valeurs nil appropriées. Pour les paramètres nommés, je m'attendrais à ce qu'ils conservent toute valeur précédemment attribuée car ils sont garantis d'avoir déjà une valeur.

Le ? opérateur soit appliqué à une valeur sans erreur ? Puis-je faire quelque chose comme (!ok) ? ou d'accord !? (ce qui est un peu bizarre, car vous regroupez l'affectation et l'exploitation) ? Ou cette syntaxe n'est-elle bonne qu'en cas d'erreur ?

Non, je ne pense pas qu'il soit logique d'utiliser cette syntaxe pour autre chose que les valeurs d'erreur.

Je pense que les fonctions "must" vont proliférer par désespoir pour un code plus lisible.

sqlx

db.MustExec(schema)

modèle html

var t = template.Must(template.New("name").Parse("html"))

Je propose l'opérateur de panique (je ne sais pas si je devrais l'appeler un "opérateur")

a,  😱 := someFunc(b)

comme, mais peut-être plus immédiat que

a, err := someFunc(b)
if err != nil {
  panic(err)
}

😱 est probablement trop difficile à taper, nous pourrions utiliser quelque chose comme !, ou !!, ou

a,  !! := someFunc(b)
!! = maybeReturnsError()

Peut-être !! panique et ! Retour

Temps pour mes 2 cents. Pourquoi ne pouvons-nous pas simplement utiliser la bibliothèque standard debug.PrintStack() pour les traces de pile ? L'idée est d'imprimer la trace de la pile uniquement au niveau le plus profond, là où l'erreur s'est produite.

Pourquoi ne pouvons-nous pas simplement utiliser la bibliothèque standard debug.PrintStack() pour les traces de pile ?

Les erreurs peuvent transiter sur de nombreuses piles. Ils peuvent être envoyés à travers les canaux, stockés dans des variables, etc. Il est souvent plus utile de connaître ces points de transition que de connaître le fragment où l'erreur a été générée pour la première fois.

De plus, la trace de la pile elle-même inclut souvent des fonctions d'assistance internes (non exportées). C'est pratique lorsque vous essayez de déboguer un plantage inattendu, mais pas utile pour les erreurs qui se produisent au cours d'un fonctionnement normal.

Quelle est l'approche la plus conviviale pour les débutants en programmation complète ?

Je suis trouvé de version plus simple. Nécessite un seul if !err
Rien de spécial, intuitif, pas de ponctuation supplémentaire, code beaucoup plus petit

``` va
absPath, err := p.preparePath()
renvoie zéro, err si err

err := doSomethingWith(absPath) si !err
faireSomethingElse() si !err

faireSomethingIndépendammentOfErr()

// Gère l'erreur à un seul endroit ; si besoin; catch-like sans indentation
si erreur {
renvoie "erreur sans pollution de code", err
}
```

err := doSomethingWith(absPath) if !err
doSomethingElse() if !err

Bon retour, bonnes vieilles conditions post MUMPS ;-)

Merci mais, non merci.

@dmajkic Cela ne fait rien pour aider à "retourner l'erreur avec des informations contextuelles supplémentaires".

@erwbgy le titre de ce numéro est _proposal : Go 2 : simplifier la gestion des erreurs avec || err suffix_ mon commentaire était dans ce contexte. Désolé si je suis intervenu dans la discussion précédente.

@cznic Ouais . Les conditions post ne sont pas Go-way, mais les conditions préalables semblent également polluées :

if !err; err := doSomethingWith(absPath)
if !err; doSomethingElse()

@dmajkic La proposition ne se limite pas au titre - ianlancetaylor décrit trois manières de gérer les erreurs et souligne spécifiquement que peu de propositions facilitent le renvoi de l'erreur avec des informations supplémentaires.

@erwbgy J'ai passé en revue tous les problèmes spécifiés par reposent tous sur l'ajout de nouveaux mots-clés (comme try() ) ou sur l'utilisation de caractères spéciaux non alphanumériques. Personnellement, je n'aime pas ça, car le code surchargé avec !"#$%& a tendance à paraître offensant, comme jurer.

Je suis d'accord et je ressens ce que les premières lignes de ce numéro indiquent : trop de code Go est affecté à la gestion des erreurs. La suggestion que j'ai faite est conforme à ce sentiment, avec une suggestion très proche de ce que Go ressent maintenant, sans avoir besoin de mots-clés ou de caractères clés supplémentaires.

Que diriez-vous d'un report conditionnel

func something() (int, error) {
    var error err
    var oth err

    defer err != nil {
        return 0, mycustomerror("More Info", err)
    }
    defer oth != nil {
        return 1, mycustomerror("Some other case", oth)
    }

    _, err = a()
    _, err = b()
    _, err = c()
    _, oth = d()
    _, err = e()

    return 2, nil
}


func something() (int, error) {
    var error err
    var oth err

    _, err = a()
    if err != nil {
        return 0, mycustomerror("More Info", err)
    }
    _, err = b()
    if err != nil {
        return 0, mycustomerror("More Info", err)
    }
    _, err = c()
    if err != nil {
        return 0, mycustomerror("More Info", err)
    }
    _, oth = d()
    if oth != nil {
        return 1, mycustomerror("Some other case", oth)
    }
    _, err = e()
    if err != nil {
        return 0, mycustomerror("More Info", err)
    }

    return 2, nil
}

Cela changerait considérablement la signification de defer -- c'est juste quelque chose qui est exécuté à la fin de la portée, pas quelque chose qui provoque la sortie prématurée d'une portée.

S'ils introduisent Try Catch dans cette langue, tous ces problèmes seront résolus très facilement.

Ils devraient introduire quelque chose comme ça. Si la valeur de error est définie sur autre chose que nil, cela peut interrompre le flux de travail actuel et déclencher automatiquement la section catch, puis la section finally et les bibliothèques actuelles peuvent également fonctionner sans changement. Problème résolu!

try (var err error){
     i, err:=DoSomething1()
     i, err=DoSomething2()
     i, err=DoSomething3()
} catch (err error){
   HandleError(err)
   // return err  // similar to throw err
} finally{
  // Do something
}

image

@sbinet C'est mieux que rien, mais s'ils utilisent simplement le même paradigme try-catch que tout le monde connaît, c'est bien mieux.

@KamyarM Vous semblez suggérer d'ajouter un mécanisme pour lever une exception chaque fois qu'une variable est définie sur une valeur non nulle. Ce n'est pas le "paradigme que tout le monde connaît". Je ne connais aucun langage qui fonctionne de cette façon.

Ressemble à Swift qui a également des "exceptions" qui ne fonctionnent pas tout à fait comme des exceptions.

Différentes langues ont montré que try catch est vraiment une solution de deuxième classe, alors que je suppose que Go ne sera pas en mesure de résoudre ce problème comme avec une monade Maybe et ainsi de suite.

@ianlancetaylor Je viens de faire référence au Try-Catch dans d'autres langages de programmation tels que C++, Java , C# ,... et non à la solution que j'avais ici. C'était mieux si GoLang avait le Try-Catch dès le premier jour, donc nous n'avions pas besoin de gérer cette façon de gérer les erreurs (qui n'était pas vraiment nouvelle. Vous pouvez écrire la même gestion des erreurs GoLang avec n'importe quel autre langage de programmation si vous le souhaitez pour coder comme ça) mais ce que je suggère était un moyen d'avoir une compatibilité descendante avec les bibliothèques actuelles qui peuvent renvoyer un objet d'erreur.

Les exceptions Java sont une épave de train, je dois donc être fermement en désaccord avec vous ici @KamyarM. Ce n'est pas parce que quelque chose est familier que c'est un bon choix.

Ce que je veux dire.

@KamyarM Merci pour la clarification. Nous avons explicitement considéré et rejeté les exceptions. Les erreurs ne sont pas exceptionnelles ; ils se produisent pour toutes sortes de raisons tout à fait normales. https://blog.golang.org/errors-are-values

Exceptionnel ou non, mais ils résolvent le problème de la surcharge du code en raison de la gestion des erreurs passe-partout. Le même problème a paralysé Objective-C qui fonctionne à peu près exactement comme Go. Les erreurs ne sont que des valeurs de type NSError, rien de spécial à leur sujet. Et il a le même problème avec des charges de ifs et d'emballage d'erreurs. C'est pourquoi Swift a changé les choses. Ils se sont retrouvés avec un mélange de deux - cela fonctionne comme des exceptions, ce qui signifie qu'il termine l'exécution et que vous devez intercepter l'exception. Mais il ne déroule pas la pile et fonctionne comme un retour régulier. Ainsi, l'argument technique contre l'utilisation d'exceptions pour le flux de contrôle ne s'applique pas là-bas - ces "exceptions" sont tout aussi rapides que le retour régulier. C'est plus un sucre syntaxique. Mais Swift a un problème unique avec eux. De nombreuses API Cocoa sont asynchrones (rappels et GCD) et ne sont tout simplement pas compatibles avec ce type de gestion des erreurs - les exceptions sont inutiles sans quelque chose comme wait. Mais pratiquement tout le code Go est synchrone et ces "exceptions" pourraient fonctionner.

@urandom
Les exceptions en Java ne sont pas mauvaises. Le problème vient des mauvais programmeurs qui ne savent pas s'en servir.

Si votre langue a des fonctionnalités terribles, quelqu'un finira par utiliser cette fonctionnalité. Si votre langue n'a pas une telle fonctionnalité, il y a 0% de chance. Ce sont des mathématiques simples.

@comme je ne suis pas d'accord avec vous pour dire que try-catch est une fonctionnalité terrible. C'est une fonctionnalité très utile et qui nous facilite la vie et c'est la raison pour laquelle nous commentons ici, donc peut-être que l'équipe Google GoLang ajoute une fonctionnalité similaire. Personnellement, je déteste ces codes de gestion des erreurs if-elses dans GoLang et je n'aime pas beaucoup ce concept de report-panic-recover (il est similaire à try-catch mais pas aussi organisé qu'avec les blocs Try-Catch-Finally) . Cela ajoute tellement de bruit dans le code que le code est illisible dans de nombreux cas.

La fonctionnalité pour gérer les erreurs sans passe-partout existe déjà dans la langue. Ajouter plus de fonctionnalités pour rassasier les débutants issus de langages basés sur des exceptions ne semble pas être une bonne idée.

Et qu'en est-il de qui vient de C/C++, Objective-C où nous avons exactement le même problème avec le passe-partout ? Et c'est frustrant de voir un langage moderne comme le Go souffrir exactement des mêmes problèmes. C'est pourquoi tout ce battage médiatique autour des erreurs en tant que valeurs semble si faux et stupide - c'est déjà fait depuis des années, des dizaines d'années. On dirait que Go n'a rien appris de cette expérience. Surtout en regardant Swift/Rust qui essaie en fait de trouver un meilleur moyen. Installez-vous avec une solution existante comme Java/C# avec des exceptions, mais au moins ce sont des langages beaucoup plus anciens.

@KamyarM Avez-vous déjà utilisé la programmation orientée ferroviaire ? Le rayon?

Vous ne feriez pas autant l'éloge des exceptions, si vous les utilisiez, à mon humble avis.

@ShalokShalom Pas grand chose. Mais n'est-ce pas juste une machine d'état ? En cas d'échec faire ceci et en cas de succès faire cela ? Eh bien, je pense que tous les types d'erreurs ne doivent pas être traités comme des exceptions. Lorsque seule une validation d'entrée utilisateur est nécessaire, il suffit de renvoyer une valeur booléenne avec le détail des erreurs de validation. Les exceptions doivent être limitées à l'accès IO ou réseau ou aux entrées de fonction incorrectes et dans le cas où une erreur est vraiment critique et que vous souhaitez arrêter le chemin d'exécution heureux à tout prix.

L'une des raisons pour lesquelles certaines personnes disent que Try-Catch n'est pas bon est à cause de ses performances. Cela est probablement dû à l'utilisation d'une table de mappage de gestionnaire pour chaque endroit où une exception peut se produire. J'ai lu quelque part que même les exceptions sont plus rapides (coût zéro lorsqu'aucune exception ne se produit mais a un coût beaucoup plus élevé lorsqu'elles se produisent réellement) en le comparant à la vérification en cas d'erreur (elle est toujours vérifiée, qu'il y ait ou non une erreur). En dehors de cela, je ne pense pas qu'il y ait de problème avec la syntaxe Try-Catch. C'est seulement la façon dont il est implémenté par le compilateur qui le rend différent pas sa syntaxe.

Les gens venant de C/C++ louent exactement Go pour ne PAS avoir d'exceptions et
pour avoir fait un choix judicieux, en résistant à ceux qui le prétendent « moderne » et
Dieu merci pour le workflow lisible (surtout après C++).

Le Mar 17 Avr 2018 à 03:46, Antonenko Artem [email protected]
a écrit:

Et qu'en est-il de qui vient de C/C++, Objective-C où nous avons le même
problème exact avec le passe-partout? Et frustrant de voir une langue moderne
comme Go souffrent exactement des mêmes problèmes. C'est pourquoi tout ce battage médiatique
autour des erreurs car les valeurs semblent si fausses et stupides - c'est déjà fait
pendant des années, des dizaines d'années. C'est comme si Go n'avait rien appris de ça
de l'expérience. Surtout en regardant Swift/Rust qui essaie en fait de trouver
une meilleure façon. Choisissez une solution existante telle que Java/C# réglée avec
exceptions, mais au moins ce sont des langues beaucoup plus anciennes.

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

@kirillx Je n'ai jamais dit que je voulais des exceptions comme en C++. Merci de relire mon commentaire. Et qu'est-ce que cela a à voir avec C où la gestion des erreurs est encore plus horrible ? Non seulement vous avez des tonnes de passe-partout, mais vous manquez également de valeurs différées et de plusieurs valeurs de retour, ce qui vous oblige à renvoyer des valeurs à l'aide d'arguments de pointeur et à utiliser goto pour organiser votre logique de nettoyage. Go utilise le même concept d'erreurs mais résout certains des problèmes liés au report et aux valeurs de retour multiples. Mais le passe-partout est toujours là. Les autres langages modernes ne veulent pas non plus d'exceptions mais ne veulent pas non plus se contenter du style C en raison de sa verbosité. C'est pourquoi nous avons cette proposition et tant d'intérêt pour ce problème.

Les personnes qui préconisent des exceptions doivent lire cet article : https://ckwop.me.uk/Why-Exceptions-Suck.html

La raison pour laquelle les exceptions de style Java/C++ sont intrinsèquement mauvaises n'a rien à voir avec les performances d'implémentations particulières. Les exceptions sont mauvaises car ce sont les "goto en cas d'erreur" de BASIC, avec les gotos invisibles dans le contexte où elles peuvent prendre effet. Les exceptions cachent la gestion des erreurs là où vous pouvez facilement l'oublier. Les exceptions vérifiées de Java étaient censées résoudre ce problème, mais dans la pratique, elles ne l'ont pas fait parce que les gens ont juste attrapé et mangé les exceptions ou ont vidé les traces de la pile partout.

J'écris Java presque toutes les semaines et je ne veux absolument pas voir d'exceptions de style Java dans Go, quelles que soient leurs performances.

@lpar N'est-ce pas toutes les boucles for, tandis que les boucles, if elses , switch case , interrompt et continue le genre de choses GoTo. Que reste-t-il alors d'un langage de programmation ?

Tandis que, for et if/else n'impliquent pas un flux d'exécution sautant de manière invisible vers un autre endroit sans marqueur pour indiquer qu'il le fera.

Qu'est-ce qui est différent si quelqu'un transmet simplement l'erreur à l'appelant précédent dans GoLang et que cet appelant la renvoie simplement à l'appelant précédent et ainsi de suite (à part beaucoup de bruit de code) ? Combien de codes devons-nous regarder et parcourir pour voir qui va gérer l'erreur ? Il en va de même avec le try-catch.

Qu'est-ce qui peut arrêter le programmeur ? Parfois, la fonction n'a pas vraiment besoin de gérer une erreur. nous voulons simplement transmettre l'erreur à l'interface utilisateur afin qu'un utilisateur ou l'administrateur système puisse la résoudre ou trouver une solution de contournement.

Si une fonction ne veut pas gérer une exception, elle n'utilise tout simplement pas le bloc try-catch afin que l'appelant précédent puisse la gérer. Je ne pense pas que la syntaxe ait un problème. C'est aussi beaucoup plus propre. Les performances et la manière dont elles sont implémentées dans un langage sont cependant différentes.

Comme vous pouvez le voir ci-dessous, nous devons ajouter 4 lignes de code pour ne pas gérer une erreur :

func myFunc1() error{
  // ...
  if (err){
      return err
  }
  return nil
}

Si vous souhaitez transmettre les erreurs à l'appelant pour qu'il les gère, ce n'est pas un problème. Le fait est qu'il est visible que vous le faites, au point où l'erreur vous a été renvoyée.

Envisager:

x, err := lib.SomeFunc(100, 4)
if err != nil {
  // A
}
// B

En regardant le code, vous savez qu'une erreur peut se produire lors de l'appel de la fonction. Vous savez que si l'erreur se produit, le flux de code se terminera au point A. Vous savez que le seul autre flux de code de lieu se terminera au point B. Il existe également un contrat implicite selon lequel si err est nul, x est une valeur valide, zéro ou autre.

Contraste avec Java :

x = SomeFunc(100, 4)

En regardant le code, vous n'avez aucune idée si une erreur peut se produire lorsque la fonction est appelée. Si une erreur se produit et est exprimée comme une exception, alors un goto se produit, et vous pourriez vous retrouver quelque part au bas du code environnant... ou si l'exception n'est pas interceptée, vous pourriez vous retrouver quelque part au bas d'un morceau complètement différent de votre code. Ou vous pourriez vous retrouver dans le code de quelqu'un d'autre. En fait, étant donné que le gestionnaire d'exceptions par défaut peut être remplacé, vous pourriez potentiellement vous retrouver littéralement n'importe où, en fonction de quelque chose fait par le code de quelqu'un d'autre.

De plus, il n'y a pas de contrat implicite que x est valide - il est courant que les fonctions renvoient null pour indiquer des erreurs ou des valeurs manquantes.

Avec Java, ces problèmes peuvent survenir à chaque appel -- pas seulement avec du mauvais code, c'est quelque chose dont vous devez vous soucier avec _tout_ le code Java. C'est pourquoi les environnements de développement Java ont une aide contextuelle pour vous montrer si la fonction sur laquelle vous pointez peut provoquer une exception ou non, et quelles exceptions elle peut provoquer. C'est pourquoi Java a ajouté des exceptions vérifiées, de sorte que pour les erreurs courantes, vous deviez au moins avoir un avertissement indiquant que l'appel de fonction pouvait déclencher une exception et détourner le flux du programme. Pendant ce temps, les valeurs nulles renvoyées et la nature non vérifiée de NullPointerException sont un tel problème qu'ils ont ajouté la classe Optional à Java 8 pour essayer de l'améliorer, même si le coût doit explicitement envelopper le valeur de retour sur chaque fonction qui renvoie un objet.

Mon expérience est que NullPointerException d'une valeur nulle inattendue qui m'a été remise est la façon la plus courante pour mon code Java de se bloquer, et je me retrouve généralement avec une grande trace qui est presque entièrement inutile, et un message d'erreur qui n'indique pas la cause car il a été généré loin du code fautif. En Go, honnêtement, je n'ai pas trouvé que les paniques de déréférencement nul soient un problème important, même si je suis beaucoup moins expérimenté avec Go. Cela, pour moi, indique que Java devrait apprendre de Go, plutôt que l'inverse.

Je ne pense pas que la syntaxe ait un problème.

Je ne pense pas que quiconque dise que la syntaxe est le problème avec les exceptions de style Java.

@lpar , Pourquoi les paniques de déréférencement nulles en Go sont-elles meilleures que NullPointerException en Java ? Quelle est la différence entre « Panique » et « Jeter » ? Quelle est la différence dans leur sémantique ?

Les paniques sont juste récupérables et les lancers sont attrapables ? À droite?

Je viens de me souvenir d'une différence, avec panique, vous pouvez paniquer un objet d'erreur ou un objet chaîne ou tout autre type d'objets (corrigez-moi si je me trompe) mais avec throw vous pouvez lancer un objet de type Exception ou une sous-classe d'exception seulement.

Pourquoi les paniques de déréférencement nulle en Go sont-elles meilleures que NullPointerException en Java ?

Parce que les premiers ne se produisent presque jamais dans mon expérience, alors que les seconds se produisent tout le temps, pour les raisons que j'ai expliquées.

@lpar Eh bien, je n'ai pas programmé avec Java récemment et je suppose que c'est une nouvelle chose (les 5 dernières années) mais C# a un opérateur de navigation sécurisé pour éviter les références nulles pour créer des exceptions mais qu'est-ce que Go a? Je ne suis pas sûr, mais je suppose qu'il n'a rien pour gérer ces situations. Donc, si vous voulez éviter la panique, vous devez toujours ajouter ces instructions if-not-nil-else imbriquées laides au code.

Vous n'avez généralement pas besoin de vérifier les valeurs de retour pour voir si elles sont nulles dans Go, tant que vous vérifiez la valeur de retour d'erreur. Donc pas de déclarations si imbriquées laides.

Le déréférencement nul était un mauvais exemple. Si vous ne l'attrapez pas, Go et Java fonctionnent exactement de la même manière - vous obtenez un plantage avec la trace de la pile. Comment la trace de pile peut-elle être inutile, je ne le sais pas maintenant. Vous connaissez l'endroit exact où cela s'est produit. Dans C# et Go pour moi, il est généralement trivial de corriger ce type de plantage, car le déréférencement nul dans mon expérience est dû à une simple erreur de programmeur. Dans ce cas particulier, il n'y a rien à apprendre de personne.

@lpar

Parce que les premiers ne se produisent presque jamais dans mon expérience, alors que les seconds se produisent tout le temps, pour les raisons que j'ai expliquées.

C'est accidentel et je n'ai vu aucune raison dans votre commentaire que Java soit en quelque sorte pire à nil/null que Go. J'ai observé de nombreux plantages de déréférencement nul dans le code Go. Ils sont exactement les mêmes que le déréférencement null en C#/Java. Il se peut que vous utilisiez plus de types de valeur dans Go, ce qui aide (C # les a également) mais ne change rien.

En ce qui concerne les exceptions, regardons Swift. Vous avez un mot-clé throws pour les fonctions qui pourraient générer une erreur. Fonctionner sans cela ne peut pas lancer. Du point de vue de la mise en œuvre, cela fonctionne comme un retour - un registre est probablement réservé au retour d'erreur et chaque fois que vous lancez une fonction, il renvoie normalement mais comporte une valeur d'erreur. Ainsi, le problème des erreurs inattendues est résolu. Vous savez exactement quelle fonction pourrait lancer, vous savez exactement où cela pourrait se produire. Les erreurs sont des valeurs et ne nécessitent pas de déroulement de la pile. Ils sont juste rendus jusqu'à ce que vous l'attrapiez.

Ou quelque chose de similaire à Rust où vous avez un type de résultat spécial qui porte un résultat et une erreur. Les erreurs peuvent être propagées sans aucune instruction conditionnelle explicite. Plus une tonne de motifs assortis, mais ce n'est probablement pas pour Go.

Ces deux langages prennent les deux solutions (C et Java) et les combinent en quelque chose de mieux. Propagation d'erreur à partir d'exceptions + valeurs d'erreur et flux de code évident à partir de C + pas de code passe-partout laid qui ne fait rien d'utile. Je pense donc qu'il est sage d'examiner ces implémentations particulières et de ne pas les rejeter complètement simplement parce qu'elles ressemblent à des exceptions d'une manière ou d'une autre. Il y a une raison pour laquelle les exceptions sont utilisées dans tant de langues, car elles ont un côté positif. Sinon, les langues les ignoreraient. Surtout après C++.

Comment la trace de pile peut-elle être inutile, je ne le sais pas maintenant.

J'ai dit "presque totalement inutile". Comme dans, je n'ai besoin que d'une seule ligne d'informations, mais cela fait des dizaines de lignes.

C'est accidentel et je n'ai vu aucune raison dans votre commentaire que Java soit en quelque sorte pire à nil/null que Go.

Alors vous n'écoutez pas. Revenez en arrière et lisez la partie sur les contrats implicites.

Les erreurs peuvent être propagées sans aucune instruction conditionnelle explicite.

Et c'est exactement le problème - les erreurs se propagent et le flux de contrôle change sans rien d'explicite pour indiquer que cela va se produire. Apparemment, vous ne pensez pas que ce soit un problème, mais d'autres ne sont pas d'accord.

Que les exceptions telles qu'implémentées par Rust ou Swift souffrent des mêmes problèmes que Java, je ne sais pas, je laisserai cela à quelqu'un d'expérimenté avec les langages en question.

@KamyarM En gros, vous rendez nil superflu et obtenez une sécurité de type complète pour cela :

https://fsharpforfunandprofit.com/posts/the-option-type/

Et c'est exactement le problème - les erreurs se propagent et le flux de contrôle change sans rien d'explicite pour indiquer que cela va se produire.

Cela sonne vrai pour moi. Si je développe un package qui consomme un autre package et que ce package lève une exception, maintenant _Je_ dois également en être conscient, que je souhaite ou non utiliser cette fonctionnalité. Il s'agit d'une facette rare parmi les caractéristiques linguistiques proposées ; la plupart sont des choses qu'un programmeur peut choisir, ou tout simplement ne pas utiliser à sa discrétion. Les exceptions, par leur intention même, traversent toutes sortes de frontières, attendues ou non.

J'ai dit "presque totalement inutile". Comme dans, je n'ai besoin que d'une seule ligne d'informations, mais cela fait des dizaines de lignes.

Et d'énormes traces de Go avec des centaines de goroutines sont en quelque sorte plus utiles ? Je ne comprends pas où vous voulez en venir. Java et Go sont exactement les mêmes ici. Et parfois, vous trouvez utile d'observer la pile complète pour comprendre comment votre code s'est retrouvé là où il s'est écrasé. Les traces C# et Go m'ont aidé à plusieurs reprises avec cela.

Alors vous n'écoutez pas. Revenez en arrière et lisez la partie sur les contrats implicites.

Je l'ai lu, rien n'a changé. D'après mon expérience, ce n'est pas un problème. C'est à ça que sert la documentation dans les deux langues ( net.ParseIP par exemple). Si vous oubliez de vérifier si votre valeur est nil/null ou non, vous avez exactement le même problème dans les deux langues. Dans la plupart des cas, Go renverra une erreur et C# lèvera une exception afin que vous n'ayez même pas à vous soucier de nil. Une bonne API ne vous renvoie pas simplement null sans lever d'exception ou quelque chose pour dire ce qui ne va pas. Dans d'autres cas, vous le vérifiez explicitement. Les types d'erreurs les plus courants avec null dans mon expérience sont lorsque vous avez des tampons de protocole où chaque champ est un pointeur/objet ou vous avez une logique interne où les champs de classe/struct peuvent être nuls en fonction de l'état interne et vous oubliez de le vérifier avant accès. C'est le modèle le plus courant pour moi et rien dans Go n'atténue considérablement ce problème. Je peux nommer deux choses qui aident un peu - les valeurs vides utiles et les types de valeur. Mais c'est plus une question de facilité de programmation car vous n'êtes pas obligé de construire chaque variable avant de l'utiliser.

Et c'est exactement le problème - les erreurs se propagent et le flux de contrôle change sans rien d'explicite pour indiquer que cela va se produire. Apparemment, vous ne pensez pas que ce soit un problème, mais d'autres ne sont pas d'accord.

C'est un problème, je n'ai jamais dit le contraire mais les gens ici sont tellement obsédés par les exceptions Java/C#/C++ qu'ils ignorent tout ce qui leur ressemble un peu. Exactement pourquoi Swift vous oblige à marquer les fonctions avec throws afin que vous puissiez voir exactement ce que vous devez attendre d'une fonction et où le flux de contrôle pourrait s'interrompre et dans Rust vous utilisez ? pour propager explicitement une erreur avec diverses méthodes d'assistance pour lui donner plus de contexte. Ils utilisent tous les deux le même concept d'erreurs que les valeurs, mais l'enveloppent dans du sucre syntaxique pour réduire le passe-partout.

Et d'énormes traces de Go avec des centaines de goroutines sont en quelque sorte plus utiles ?

Avec Go, vous gérez les erreurs en les enregistrant avec l'emplacement au moment où elles sont détectées. Il n'y a pas de backtrace à moins que vous ne choisissiez d'en ajouter un. Je n'ai eu besoin de le faire qu'une seule fois.

D'après mon expérience, ce n'est pas un problème.

Eh bien, mon expérience diffère, et je pense que les expériences de la plupart des gens diffèrent, et comme preuve de cela, je cite le fait que Java 8 a ajouté des types facultatifs.

Ce fil ici a discuté de nombreuses forces et faiblesses de Go et de son système de gestion des erreurs, y compris une discussion sur les exceptions ou non, je recommande fortement de le lire :

https://elixirforum.com/t/discussion-go-split-thread/13006/2

Mes 2 cents à la gestion des erreurs (désolé si une telle idée a été mentionnée ci-dessus).

Nous voulons renvoyer les erreurs dans la plupart des cas. Cela conduit à de tels extraits :

a, err := fn()
if err != nil {
    return err
}
use(a)
return nil

Renvoyons automatiquement l'erreur non nulle si elle n'a pas été affectée à une variable (sans syntaxe supplémentaire). Le code ci-dessus deviendra :

a := fn()
use(a)

// or just

use(fn())

Le compilateur enregistrera err dans une variable implicite (invisible), vérifiera la présence de nil et continuera (si err == nil) ou la renverra (si err != nil) et renverra nil à la fin de fonction si aucune erreur ne s'est produite pendant l'exécution de la fonction comme d'habitude mais automatiquement et implicitement.

Si err doit être manipulé, il doit être affecté à une variable explicite et utilisé :

a, err := fn()
if err != nil {
    doSomething(err)
} else {
    use(a)
}
return nil

L'erreur peut être supprimée de cette manière :

a, _ := fn()
use(a)

Dans de rares cas (fantastiques) avec plus d'une erreur renvoyée, une gestion explicite des erreurs sera obligatoire (comme maintenant) :

err1, err2 := fn2()
if err1 != nil || err2 != nil {
    return err1, err2
}
return nil, nil

C'est aussi mon argument - nous voulons renvoyer les erreurs dans la plupart des cas, c'est généralement le cas par défaut. Et peut-être lui donner un peu de contexte. À quelques exceptions près, le contexte est ajouté automatiquement par des traces de pile. Avec des erreurs comme dans Go, nous le faisons à la main en ajoutant un message d'erreur. Pourquoi ne pas faire plus simple. Et c'est exactement ce que d'autres langues essaient de faire tout en l'équilibrant avec la question de la clarté.

Je suis donc d'accord avec "Remettons automatiquement l'erreur non nulle si elle n'a pas été affectée à une variable (sans syntaxe supplémentaire)" mais cette dernière partie me dérange. C'est là que se trouve la racine du problème avec les exceptions et pourquoi, je pense, les gens sont tellement contre le fait de parler de quoi que ce soit qui s'y rapporte légèrement. Ils modifient le flux de contrôle sans aucune syntaxe supplémentaire. C'est une mauvaise chose.

Si vous regardez Swift, par exemple, ce code ne compilera pas

func a() throws {}
func b() throws {
  a()
}

a peut générer une erreur, vous devez donc écrire try a() pour même propager une erreur. Si vous supprimez throws de b il ne sera pas compilé même avec try a() . Vous devez gérer l'erreur dans b . C'est une bien meilleure façon de gérer les erreurs qui résout à la fois le problème du flux de contrôle peu clair des exceptions et la verbosité des erreurs Objective-C. Ce dernier étant à peu près exactement comme les erreurs dans Go et ce que Swift est censé remplacer. Ce que je n'aime pas, ce sont les try, catch que Swift utilise également. Je préférerais de loin laisser les erreurs dans le cadre d'une valeur de retour.

Donc, ce que je proposerais, c'est d'avoir en fait la syntaxe supplémentaire. Pour que le site d'appel indique par lui-même qu'il s'agit d'un endroit potentiel où le flux de contrôle pourrait changer. Ce que je proposerais également, c'est que ne pas écrire cette syntaxe supplémentaire produirait une erreur de compilation. Cela, contrairement à la façon dont Go fonctionne maintenant, vous obligerait à gérer l'erreur. Vous pouvez ajouter la possibilité de faire taire l'erreur avec quelque chose comme _ car dans certains cas, il serait très frustrant de gérer chaque petite erreur. Comme, printf . Je m'en fiche s'il ne parvient pas à enregistrer quelque chose. Go a déjà ces importations ennuyeuses. Mais cela a été résolu avec l'outillage au moins.

Il existe deux alternatives à l'erreur de temps de compilation auxquelles je peux penser en ce moment. Comme Go now, laissez l'erreur être silencieusement ignorée. Je n'aime pas ça et ça a toujours été mon problème avec la gestion des erreurs Go. Cela ne force rien, le comportement par défaut est d'ignorer silencieusement l'erreur. C'est mauvais, ce n'est pas ainsi que vous écrivez des programmes robustes et faciles à déboguer. J'ai eu trop de cas en Objective-C lorsque j'étais paresseux ou que je manquais de temps et que j'ignorais l'erreur juste pour être touché par un bogue dans ce même code, mais sans aucune information de diagnostic expliquant pourquoi cela s'est produit. Au moins, la journalisation me permettrait de résoudre le problème dans de nombreux cas.

L'inconvénient est que les gens peuvent commencer à ignorer les erreurs, placez try, catch(...) partout pour ainsi dire. C'est une possibilité mais, en même temps, avec des erreurs ignorées par défaut, c'est encore plus facile de le faire. Je pense que l'argument sur les exceptions ne s'applique pas ici. À quelques exceptions près, ce que certaines personnes essaient d'obtenir, c'est l'illusion que leur programme est plus stable. Le fait qu'une exception non gérée bloque le programme est le problème ici.

Une autre alternative serait de paniquer. Mais c'est juste frustrant et cela rappelle les exceptions. Cela conduirait certainement les gens à faire du codage "défensif" afin que leur programme ne plante pas. Pour moi, le langage moderne devrait faire autant de choses que possible au moment de la compilation et laisser le moins de décisions possible à l'exécution. Là où la panique peut être appropriée, c'est au sommet de la pile d'appels. Par exemple, ne pas gérer une erreur dans la fonction principale produirait automatiquement une panique. Cela s'applique-t-il également aux goroutines? Ne devrait probablement pas.

Pourquoi envisager des compromis ?

@nick-korsakov la proposition originale (ce numéro) veut ajouter plus de contexte aux erreurs :

Il est déjà facile (peut-être trop facile) d'ignorer l'erreur (voir #20803). De nombreuses propositions existantes pour la gestion des erreurs facilitent le renvoi de l'erreur non modifiée (par exemple, #16225, #18721, #21146, #21155). Peu facilitent le retour de l'erreur avec des informations supplémentaires.

Voir aussi ce commentaire .

Dans ce commentaire, je suggère que pour progresser dans cette discussion (plutôt que de tourner en boucle), nous devrions mieux définir les objectifs, par exemple ce qu'est un message d'erreur soigneusement traité. Le tout est assez intéressant à lire mais il semble affecté par un problème de mémoire de poisson rouge de trois secondes (pas beaucoup de concentration/avancée, répétition de jolis changements de syntaxe créative et arguments sur les exceptions/paniques, etc.).

Un autre hangar à vélos :

func makeFile(url string) (size int, err error){
    rsp, err := http.Get(url)
    try err
    defer rsp.Body.Close()

    var data dataStruct
    dec := json.NewDecoder(rsp.Body)
    err := dec.Decode(&data)
    try errors.Errorf("could not decode %s: %v", url, err)

    f, err := os.Create(data.Path)
    try errors.Errorf("could not open file %s: %v", data.Path, err)
    defer f.Close()

    return f.Write([]byte(data.Rows))
}

try signifie "retourner si ce n'est pas la valeur vide". En cela, je suppose que errors.Errorf renverra nil lorsque err est nul. Je pense que c'est à peu près autant d'économies que nous pouvons nous attendre tout en gardant l'objectif d'un emballage facile.

Les types de scanner de la bibliothèque standard stockent l'état d'erreur à l'intérieur d'une structure dont les méthodes peuvent vérifier de manière responsable l'existence d'une erreur avant de continuer.

type Scanner struct{
    err error
}
func (s *Scanner) Scan() bool{
   if s.err != nil{
       return false
   }
   // scanning logic
}
func (s *Scanner) Err() error{ return s.err }

En utilisant des types pour stocker l'état d'erreur, il est possible de garder le code qui utilise un tel type exempt de contrôles d'erreur redondants.

Il ne nécessite également aucun changement de syntaxe créatif et bizarre ni aucun transfert de contrôle inattendu dans la langue.

Je dois également suggérer quelque chose comme try/catch, où err est défini dans try{}, et si err est défini sur une valeur non nulle, le flux passe de try{} aux blocs du gestionnaire d'erreurs (s'il y en a).

En interne, il n'y a pas d'exceptions, mais le tout devrait être plus proche
à la syntaxe qui effectue des vérifications if err != nil break après chaque ligne où l'erreur peut être affectée.
Par exemple:

...
try(err) {
   err = doSomethig()
   err, value := doSomethingElse()
   doSomethingObliviousToErr()
   err = thirdErrorProneThing()
} 
catch(err SomeErrorType) {
   handleSomeSpecificErr(err)
}
catch(err Error) {
  panic(err)
}

Je sais que cela ressemble à du C++, mais c'est aussi bien connu et plus propre que le manuel if err != nil {...} après chaque ligne.

@comme

Le type de scanner fonctionne car il effectue tout le travail et peut donc se permettre de suivre sa propre erreur en cours de route. Ne nous leurrons pas sur le fait qu'il s'agit d'une solution universelle, s'il vous plaît.

@carlmjohnson

Si nous voulons gérer une seule ligne pour une erreur simple, nous pouvons modifier la syntaxe pour permettre à l'instruction return d'être le début d'un bloc d'une ligne.
Cela permettrait aux gens d'écrire :

func makeFile(url string) (size int, err error){
    rsp, err := http.Get(url)
    if err != nil return err
    defer rsp.Body.Close()

    var data dataStruct
    dec := json.NewDecoder(rsp.Body)
    err := dec.Decode(&data)
    if err != nil return errors.Errorf("could not decode %s: %v", url, err)

    f, err := os.Create(data.Path)
    if err != nil return errors.Errorf("could not open file %s: %v", data.Path, err)
    defer f.Close()

    return f.Write([]byte(data.Rows))
}

Je pense que la spécification devrait être changée en quelque chose comme (cela pourrait être assez naïf :))

Block = "{" StatementList "}" | "return" Expression .

Je ne pense pas que le retour de casse spécial soit vraiment mieux que de simplement changer gofmt pour simplifier si err vérifie une ligne au lieu de trois.

@urandom

L'erreur fusionnant au-delà d'un type boxable et ses actions ne doivent pas être encouragées. Pour moi, cela indique un manque d'effort pour envelopper ou ajouter un contexte d'erreur entre les erreurs provenant de différentes actions non liées.

L'approche du scanner est l'une des pires choses que j'ai lues dans le contexte de tout ce mantra "les erreurs sont des valeurs":

  1. C'est inutile dans à peu près tous les cas d'utilisation qui nécessitent beaucoup de passe-partout pour la gestion des erreurs. Les fonctions qui appellent plusieurs packages externes n'en bénéficieront pas.
  2. C'est un concept alambiqué et inconnu. L'introduire ne fera que dérouter les futurs lecteurs et rendre votre code plus compliqué qu'il ne devrait l'être juste pour que vous puissiez contourner les déficiences de la conception du langage.
  3. Il cache la logique et essaie d'être similaire aux exceptions en en tirant le pire (flux de contrôle complexe) sans en tirer aucun avantage.
  4. Dans certains cas, cela gaspillera des ressources de calcul. Chaque appel devra perdre du temps sur une vérification d'erreur inutile qui s'est produite il y a longtemps.
  5. Il cache l'endroit exact où l'erreur s'est produite. Imaginez un cas où vous analysez ou sérialisez un format de fichier. Vous auriez une chaîne d'appels en lecture/écriture. Imaginez que le premier échoue. Comment sauriez-vous où exactement l'erreur s'est produite ? Quel champ était-il en train d'analyser ou de sérialiser ? "Erreur IO", "timeout" - ces erreurs seraient inutiles dans ce cas. Vous pouvez fournir un contexte à chaque lecture/écriture (nom de champ, par exemple). Mais à ce stade, il vaut mieux abandonner toute l'approche car elle travaille contre vous.

Dans certains cas, cela gaspillera des ressources de calcul.

Des repères ? Qu'est-ce qu'une « ressource de calcul » exactement ?

Il cache l'endroit exact où l'erreur s'est produite.

Non, ce n'est pas le cas, car les erreurs non nulles ne sont pas écrasées

Les fonctions qui appellent plusieurs packages externes n'en bénéficieront pas.
C'est un concept alambiqué et inconnu
L'approche du scanner est l'une des pires choses que j'ai lues dans le contexte de cet ensemble "les erreurs sont des valeurs"

J'ai l'impression que vous ne comprenez pas l'approche. C'est logiquement équivalent à une vérification d'erreur régulière dans un type autonome, je vous suggère d'étudier l'exemple de près afin que ce soit peut-être la pire chose que vous compreniez plutôt que la pire chose que vous ayez _lu_.

Désolé, je vais ajouter ma propre proposition à la pile. J'ai lu la plupart de ce qui est ici, et bien que j'aime certaines des propositions, j'ai l'impression qu'elles essaient d'en faire trop. Le problème est une erreur passe-partout. Ma proposition est simplement d'éliminer ce passe-partout au niveau de la syntaxe, et de laisser les façons dont les erreurs sont transmises seules.

Proposition

Réduisez les erreurs passe-partout en activant l'utilisation du jeton _! comme sucre syntaxique pour provoquer une panique lorsqu'une error non nulle est attribuée

val, err := something.MayError()
if err != nil {
    panic(err)
}

pourrait devenir

val, _! := something.MayError()

et

if err := something.MayError(); err != nil {
    panic(err)
}

pourrait devenir

_! = something.MayError()

Bien sûr, le symbole particulier est sujet à débat. J'ai aussi pensé à _^ , _* , @ , et d'autres. J'ai choisi _! comme suggestion de facto parce que je pensais que ce serait la plus familière en un coup d'œil.

Syntaxiquement, _! (ou le jeton choisi) serait un symbole de type error disponible dans le scope dans lequel il est utilisé. Il commence par nil , et à chaque fois qu'il est affecté, un contrôle nil est effectué. S'il est défini sur une valeur non nulle de error , une panique est déclenchée. Étant donné que _! (ou, encore une fois, le jeton choisi) ne serait pas un identifiant syntaxiquement valide dans go, la collision de noms ne serait pas un problème. Cette variable éthérée ne serait introduite que dans les portées où elle est utilisée, de la même manière que les valeurs de retour nommées. Si un identifiant syntaxiquement valide est nécessaire, peut-être qu'un espace réservé pourrait être utilisé qui serait réécrit en un nom unique au moment de la compilation.

Justification

L'une des critiques les plus courantes que je vois au go est la verbosité de la gestion des erreurs. Les erreurs aux limites de l'API ne sont pas une mauvaise chose. Cependant, devoir ramener les erreurs aux limites de l'API peut être pénible, en particulier pour les algorithmes profondément récursifs. Pour contourner la propagation d'erreur de verbosité ajoutée introduit au code récursif, les paniques peuvent être utilisées. Je pense que c'est une technique assez couramment utilisée. Je l'ai utilisé dans mon propre code et je l'ai vu utilisé dans la nature, y compris dans l'analyseur de go. Parfois, vous avez effectué une validation ailleurs dans votre programme et vous vous attendez à ce qu'une erreur soit nulle. Si une erreur non nulle était reçue, cela violerait votre invariant. Lorsqu'un invariant est violé, il est acceptable de paniquer. Dans un code d'initialisation complexe, il est parfois judicieux de transformer les erreurs en panique et de les récupérer pour être renvoyées quelque part avec une meilleure connaissance du contexte. Dans tous ces scénarios, il est possible de réduire les erreurs passe-partout.

Je me rends compte que c'est la philosophie de go d'éviter au maximum les paniques. Ils ne constituent pas un outil de propagation d'erreurs au-delà des limites de l'API. Cependant, ils sont une caractéristique du langage et ont des cas d'utilisation légitimes, tels que ceux décrits ci-dessus. Les paniques sont un moyen fantastique de simplifier la propagation des erreurs dans le code privé, et une simplification de la syntaxe contribuerait grandement à rendre le code plus propre et, sans doute, plus clair. Je pense qu'il est plus facile de reconnaître _! (ou @ , ou `_^, etc...) en un coup d'œil que la forme "if-error-panic". Un jeton peut réduire considérablement la quantité de code qui doit être écrit/lu pour transmettre/comprendre :

  1. il pourrait y avoir une erreur
  2. s'il y a une erreur, on ne s'y attend pas
  3. s'il y a une erreur, elle est probablement traitée en amont de la chaîne

Comme pour toute fonctionnalité de syntaxe, il existe un potentiel d'abus. Dans ce cas, la communauté go dispose déjà d'un ensemble de bonnes pratiques pour gérer les paniques. Étant donné que cet ajout de syntaxe est du sucre syntaxique pour la panique, cet ensemble de meilleures pratiques peut être appliqué à son utilisation.

En plus de la simplification des cas d'utilisation acceptables pour la panique, cela facilite également le prototypage rapide. Si j'ai une idée que je veux noter dans le code et que je veux juste que des erreurs fassent planter le programme pendant que je joue, je pourrais utiliser cet ajout de syntaxe plutôt que la forme "if-erreur-panique". Si je peux m'exprimer en moins de lignes au début du développement, cela me permet de mettre mes idées dans le code plus rapidement. Une fois que j'ai une idée complète du code, je reviens en arrière et refactorise mon code pour renvoyer les erreurs aux limites appropriées. Je ne laisserais pas les paniques libres dans le code de production, mais elles peuvent être un puissant outil de développement.

Les paniques ne sont que des exceptions sous un autre nom, et une chose que j'aime chez Go, c'est que les exceptions sont exceptionnelles. Je ne veux pas encourager plus d'exceptions en leur donnant du sucre syntaxique.

@carlmjohnson L'une des deux choses doit être vraie :

  1. Les paniques font partie du langage avec des cas d'utilisation légitimes, ou
  2. Les paniques n'ont pas de cas d'utilisation légitimes et doivent donc être supprimées du langage

Je suppose que la réponse est 1.
Je ne suis pas d'accord non plus sur le fait que "les paniques ne sont que des exceptions sous un autre nom". Je pense que ce genre de geste de la main empêche une vraie discussion. Il existe des différences clés entre les paniques et les exceptions, comme on le voit dans la plupart des autres langues.

Je comprends la réaction instinctive « la panique est mauvaise », mais les sentiments personnels concernant l'utilisation de la panique ne changent pas le fait que les paniques sont utilisées et sont en fait utiles. Le compilateur go utilise des paniques pour renflouer les processus profondément récursifs à la fois dans l'analyseur et dans la phase de vérification de type (la dernière fois que j'ai regardé).
Les utiliser pour propager des erreurs via un code profondément récursif semble non seulement être une utilisation acceptable, mais une utilisation approuvée par les développeurs de go.

Les paniques communiquent quelque chose de spécifique :

quelque chose s'est mal passé ici qu'ici n'était pas prêt à gérer

Il y aura toujours des endroits dans le code où cela est vrai. Surtout au début du développement. Go a été modifié pour améliorer l'expérience de refactorisation avant : l'ajout d'alias de type. Être capable de propager des erreurs indésirables avec panique jusqu'à ce que vous sachiez si et comment les gérer à un niveau plus proche de la source peut rendre l'écriture et la refactorisation progressive du code beaucoup moins bavard.

J'ai l'impression que la plupart des propositions ici proposent de grands changements dans la langue. C'est l'approche la plus transparente que j'ai pu trouver. Il permet à l'ensemble du modèle cognitif actuel de gestion des erreurs dans go de rester intact, tout en permettant une réduction de la syntaxe pour un cas spécifique, mais courant. Les meilleures pratiques dictent actuellement que "le code d'accès ne devrait pas paniquer au-delà des limites de l'API". Si j'ai des méthodes publiques dans un package, elles devraient renvoyer des erreurs si quelque chose ne va pas, sauf dans de rares cas où l'erreur est irrécupérable (violations invariantes, par exemple). Cet ajout au libellé ne remplacerait pas cette pratique exemplaire. C'est simplement un moyen de réduire le passe-partout dans le code interne et de rendre les idées d'esquisse plus claires. Cela rend certainement le code plus facile à lire de manière linéaire.

var1, _! := trySomeTask1()
var2, _! := trySomeTask2(var1)
var3, _! := trySomeTask3(var2)
var4, _! := trySomeTask4(var3)

est beaucoup plus lisible que

var1, err := trySomeTask1()
if err != nil {
    panic(err)
}
var2, err := trySomeTask2(var1)
if err != nil {
    panic(err)
}
var3, err := trySomeTask3(var2)
if err != nil {
    panic(err)
}
var4, err := trySomeTask4(var3)
if err != nil {
    panic(err)
}

Il n'y a vraiment pas de différence fondamentale entre une panique dans Go et une exception dans Java ou Python, etc. à part la syntaxe et l'absence de hiérarchie d'objets (ce qui est logique car Go n'a pas d'héritage). Leur fonctionnement et leur utilisation sont les mêmes.

Bien sûr, les paniques ont une place légitime dans la langue. Les paniques servent à gérer les erreurs qui ne devraient se produire qu'en raison d'une erreur du programmeur qui sont autrement irrécupérables. Par exemple, si vous divisez par zéro dans un contexte d'entier, il n'y a pas de valeur de retour possible et c'est de votre faute si vous n'avez pas vérifié le zéro en premier, donc ça panique. De même, si vous lisez une tranche hors limites, essayez d'utiliser nil comme valeur, etc. Ces choses sont causées par une erreur du programmeur - et non par une condition anticipée, comme une panne du réseau ou un fichier ayant de mauvaises autorisations - alors ils paniquent simplement. et exploser la pile. Go fournit des fonctions d'assistance qui paniquent comme template.Doit parce qu'il est prévu que celles-ci seront utilisées avec des chaînes codées en dur où toute erreur devrait être causée par une erreur du programmeur. Le manque de mémoire n'est pas une faute du programmeur en soi, mais c'est aussi irrécupérable et peut arriver n'importe où, donc ce n'est pas une erreur mais une panique.

Les gens utilisent aussi parfois les paniques comme moyen de court-circuiter la pile, mais cela est généralement mal vu pour des raisons de lisibilité et de performances, et je ne vois aucune chance que Go change pour encourager son utilisation.

Les Go panics et les exceptions non vérifiées de Java sont à peu près identiques et existent pour les mêmes raisons et pour gérer les mêmes cas d'utilisation. N'encouragez pas les gens à utiliser des paniques pour d'autres cas, car ces cas ont les mêmes problèmes que les exceptions dans d'autres langues.

Les gens utilisent aussi parfois les paniques comme moyen de court-circuiter la pile, mais cela est généralement mal vu pour des raisons de lisibilité et de performances.

Tout d'abord, le problème de lisibilité est quelque chose que ce changement de syntaxe résout directement :

// clearly, linearly shows that these steps must occur in order,
// and any errors returned cause a panic, because this piece of
// code isn't responsible for reporting or handling possible failures:
// - IO Error: either network or disk read/write failed
// - External service error: some unexpected response from the external service
// - etc...
// It's not this code's responsibility to be aware of or handle those scenarios.
// That's perhaps the parent process's job.
var1, _! := trySomeTask1()
var2, _! := trySomeTask2(var1)
var3, _! := trySomeTask3(var2)
var4, _! := trySomeTask4(var3)

vs

var1, err := trySomeTask1()
if err != nil {
    panic(err)
}
var2, err := trySomeTask2(var1)
if err != nil {
    panic(err)
}
var3, err := trySomeTask3(var2)
if err != nil {
    panic(err)
}
var4, err := trySomeTask4(var3)
if err != nil {
    panic(err)
}

La lisibilité de côté pour le moment, l'autre raison invoquée est la performance.
Oui, il est vrai que l'utilisation d'instructions panics et defer entraîne une baisse des performances, mais dans de nombreux cas, cette différence est négligeable pour l'opération effectuée. Les E/S disque et réseau vont, en moyenne, prendre beaucoup plus de temps que n'importe quelle magie de pile potentielle pour gérer les retards/paniques.

J'entends souvent répéter ce point lors des discussions sur les paniques, et je pense qu'il est fallacieux de dire que les paniques sont une dégradation des performances. Ils PEUVENT certainement l'être, mais ils n'ont pas à l'être. Comme beaucoup d'autres choses dans une langue. Si vous paniquez à l'intérieur d'une boucle serrée où la performance serait vraiment importante, vous ne devriez pas non plus différer dans cette boucle. En fait, toute fonction qui choisit elle-même de paniquer ne devrait généralement pas attraper sa propre panique. De même, une fonction go écrite aujourd'hui ne renverrait pas à la fois une erreur et une panique. Ce n'est pas clair, idiot et ce n'est pas la meilleure pratique. C'est peut-être ainsi que nous sommes habitués à voir des exceptions utilisées dans Java, Python, Javascript, etc. augmenter la pile d'appels via la panique va changer la façon dont les gens utilisent la panique. Ils utilisent la panique de toute façon. Le but de cette extension de syntaxe est de reconnaître le fait que les développeurs utilisent la panique, et elle a des usages parfaitement légitimes, et de réduire le passe-partout qui l'entoure.

Pouvez-vous me donner quelques exemples de code problématique que cette fonctionnalité de syntaxe permettrait, selon vous, qui ne sont actuellement pas possibles/contre les meilleures pratiques ? Si quelqu'un communique des erreurs aux utilisateurs de son code via panique/récupération, cela est actuellement mal vu et continuerait évidemment de l'être, même si une syntaxe comme celle-ci était ajoutée. Si vous le pouvez, veuillez répondre aux questions suivantes :

  1. À votre avis, quels abus découleraient d'une extension de syntaxe comme celle-ci ?
  2. Que signifie var1, err := trySomeTask1(); if err != nil { panic(err) } que var1, _! := trySomeTask1() ne transmet pas ? Pourquoi?

Il me semble que le nœud de votre argument est que « les paniques sont mauvaises et nous ne devrions pas les utiliser ».
Je ne peux pas déballer et discuter des raisons derrière cela si elles ne sont pas partagées.

Ces choses sont causées par une erreur du programmeur, et non par une condition anticipée, comme une panne du réseau ou un fichier ayant de mauvaises autorisations, alors ils paniquent et font exploser la pile.

Comme la plupart des gaufres, j'aime l'idée des erreurs en tant que valeurs. Je pense que cela aide à communiquer clairement quelles parties d'une API garantissent un résultat par rapport à lesquelles peuvent échouer, sans avoir à consulter la documentation.

Il permet des choses comme la collecte des erreurs et l'augmentation des erreurs avec plus d'informations. Tout cela est très important aux limites de l'API, là où votre code croise le code utilisateur. Cependant, dans ces limites d'API, il n'est souvent pas nécessaire de faire tout cela avec vos erreurs. Surtout si vous attendez le chemin heureux et que vous avez un autre code responsable de la gestion de l'erreur si ce chemin échoue.

Il y a des moments où ce n'est pas le travail de votre code de gérer une erreur.
Si j'écris une bibliothèque, peu m'importe si la pile réseau est en panne - c'est hors de mon contrôle en tant que développeur de bibliothèque. Je vais renvoyer ces erreurs au code utilisateur.

Même dans mon propre code, il y a des moments où j'écris un morceau de code dont le seul travail est de renvoyer les erreurs à une fonction parente.

Par exemple, disons que vous avez un http.HandlerFunc qui lit un fichier à partir du disque comme réponse - cela fonctionnera presque toujours, et s'il échoue, il est probable que le programme ne soit pas correctement écrit (erreur du programmeur) ou qu'il y ait un problème avec le système de fichiers en dehors du champ de responsabilité du programme. Une fois que le http.HandlerFunc panique, il se ferme et un gestionnaire de base interceptera cette panique et écrira un 500 au client. Si, à un moment donné, je souhaite gérer cette erreur différemment, je peux remplacer _! par err et faire ce que je veux avec la valeur d'erreur. Le fait est que pendant toute la durée du programme, je n'aurai probablement pas besoin de le faire. Si je rencontre des problèmes comme celui-là, le gestionnaire n'est pas la partie du code responsable de la gestion de cette erreur.

Je peux, et je le fais normalement, écrire if err != nil { panic(err) } ou if err != nil { return ..., err } dans mes gestionnaires pour des choses comme les échecs d'E/S, les échecs de réseau, etc. Lorsque j'ai besoin de vérifier une erreur, je peux toujours le faire. La plupart du temps, cependant, je n'écris que if err != nil { panic(err) } .

Ou, un autre exemple, si je recherche récursivement un trie (disons dans une implémentation de routeur http), je déclarerai une fonction func (root *Node) Find(path string) (found Value, err error) . Cette fonction différera une fonction pour récupérer toutes les paniques générées en descendant l'arborescence. Que faire si le programme crée des essais malformés ? Que faire si certaines E/S échouent parce que le programme ne s'exécute pas en tant qu'utilisateur avec les autorisations appropriées ? Ces problèmes ne sont pas le problème de mon algorithme de recherche trie - à moins que je ne le fasse explicitement plus tard - mais ce sont des erreurs possibles que je peux rencontrer. Les renvoyer tout le long de la pile entraîne beaucoup de verbosité supplémentaire, y compris le maintien de ce qui sera idéalement plusieurs valeurs d'erreur nulles sur la pile. Au lieu de cela, je peux choisir de paniquer une erreur jusqu'à cette fonction API publique et la renvoyer à l'utilisateur. Pour le moment, cela entraîne toujours cette verbosité supplémentaire, mais ce n'est pas nécessaire.

D'autres propositions discutent de la façon de traiter une valeur de retour comme spéciale. C'est essentiellement la même pensée, mais au lieu d'utiliser des fonctionnalités déjà intégrées dans le langage, ils cherchent à modifier le comportement du langage pour certains cas. En termes de facilité de mise en œuvre, ce type de proposition (sucre syntaxique pour quelque chose déjà soutenu) va être le plus simple.

Modifier pour ajouter :
Je ne suis pas marié à la proposition que j'ai faite telle qu'elle est écrite, mais je pense qu'il est important d'examiner le problème de gestion des erreurs sous un nouvel angle. Personne ne suggère quoi que ce soit d'aussi nouveau, et je veux voir si nous pouvons recadrer notre compréhension du problème. Le problème est qu'il y a trop d'endroits où les erreurs sont explicitement traitées alors qu'elles n'ont pas besoin de l'être, et les développeurs aimeraient un moyen de les propager dans la pile sans code passe-partout supplémentaire. Il s'avère que Go a déjà cette fonctionnalité, mais il n'y a pas de syntaxe agréable pour cela. Il s'agit d'une discussion sur l'encapsulation des fonctionnalités existantes dans une syntaxe moins verbeuse pour rendre le langage plus ergonomique sans modifier le comportement. N'est-ce pas une victoire, si nous pouvons l'accomplir ?

@mccolljr Merci, mais l'un des objectifs de cette proposition est d'encourager les gens à développer de nouvelles façons de gérer les trois cas de gestion des erreurs : ignorer l'erreur, renvoyer l'erreur non modifiée, renvoyer l'erreur avec des informations contextuelles supplémentaires. Votre proposition de panique n'aborde pas le troisième cas. C'est une question importante.

@mccolljr Je pense que les limites de l'API sont beaucoup plus courantes que vous ne semblez le supposer. Je ne considère pas les appels intra-API comme le cas courant. Si quoi que ce soit, ce pourrait être l'inverse (certaines données seraient intéressantes ici). Je ne suis donc pas sûr que le développement d'une syntaxe spéciale pour les appels intra-API soit la bonne direction. De plus, l'utilisation d'erreurs d'édition return , plutôt que d'erreurs d'édition panic , au sein d'une API est généralement une bonne solution (surtout si nous proposons un plan pour ce problème). panic erreurs

Je ne pense pas que l'ajout d'un opérateur spécifiquement pour le cas de propagation d'une erreur dans la pile d'appels via la panique va changer la façon dont les gens utilisent la panique.

Je pense que vous vous trompez. Les gens vont chercher votre opérateur sténographique parce que c'est très pratique, et ils finiront par paniquer beaucoup plus qu'avant.

Que les paniques soient utiles parfois ou rarement, et qu'elles soient utiles à travers ou dans les limites de l'API, sont des faux-fuyants. Il y a beaucoup d'actions que l'on peut entreprendre en cas d'erreur. Nous cherchons un moyen de raccourcir le code de gestion des erreurs sans privilégier une action par rapport aux autres.

mais dans de nombreux cas, cette différence est négligeable pour l'opération effectuée

Bien que vrai, je pense que c'est une route dangereuse à prendre. Semblant négligeable au début, cela s'accumulerait et finirait par provoquer des goulots d'étranglement plus tard sur la route lorsqu'il est déjà tard. Je pense que nous devrions garder à l'esprit la performance dès le début et essayer de trouver de meilleures solutions. Swift et Rust déjà mentionnés ont une propagation d'erreur mais l'implémentent comme, fondamentalement, de simples retours enveloppés dans du sucre syntaxique. Oui, c'est facile de réutiliser une solution existante mais je préférerais tout laisser tel quel que de simplifier et d'encourager les gens à utiliser des paniques cachées derrière un sucre syntaxique inconnu qui essaie de cacher le fait qu'il s'agit essentiellement d'exceptions.

Semblant négligeable au début, cela s'accumulerait et finirait par provoquer des goulots d'étranglement plus tard sur la route lorsqu'il est déjà tard.

Non merci. Les goulots d'étranglement de performance imaginaires sont des goulots d'étranglement de performance géométriquement négligeables.

Non merci. Les goulots d'étranglement de performance imaginaires sont des goulots d'étranglement de performance géométriquement négligeables.

Veuillez laisser vos sentiments personnels en dehors de ce sujet. Vous avez évidemment un problème avec moi et ne voulez rien apporter d'utile, alors ignorez simplement mes commentaires et laissez un vote négatif comme vous l'avez fait avec à peu près tous les commentaires auparavant. Inutile de continuer à publier ces réponses absurdes.

Je n'ai pas de problème avec vous, vous faites juste des réclamations sur les goulots d'étranglement des performances sans aucune donnée pour le sauvegarder et je le souligne avec des mots et des pouces.

Chers amis, veuillez garder la conversation respectueuse et sur le sujet. Ce problème concerne la gestion des erreurs Go.

https://golang.org/conduct

J'aimerais revoir la partie "retourner l'erreur/avec un contexte supplémentaire", car je suppose que le fait d'ignorer l'erreur est déjà couvert par le _ déjà existant.

Je propose un mot-clé de deux mots qui peut être suivi d'une chaîne (éventuellement). La raison pour laquelle il s'agit d'un mot-clé à deux mots est double. Premièrement, contrairement à un opérateur qui est intrinsèquement cryptique, il est plus facile de saisir ce qu'il fait sans trop de connaissances préalables. J'ai choisi "ou bulle", car j'espère que le mot or sans erreur assignée signifiera à l'utilisateur que l'erreur est traitée ici, si elle n'est pas nulle. Certains utilisateurs associeront déjà le or à la gestion d'une fausse valeur d'autres langages (perl, python), et la lecture de data := Foo() or ... pourrait leur dire inconsciemment que data est inutilisable si le or partie de la déclaration est atteinte. Deuxièmement, le mot-clé bubble bien qu'étant relativement court, peut signifier à l'utilisateur que quelque chose monte (la pile). Le mot up pourrait également convenir, bien que je ne sois pas sûr que l'ensemble or up soit suffisamment compréhensible. Enfin, le tout est un mot-clé, d'abord parce qu'il est plus lisible, et deuxièmement parce que ce comportement ne peut pas être écrit par une fonction elle-même (vous pouvez peut-être appeler panic pour échapper à la fonction dans laquelle vous vous trouvez, mais alors vous pouvez ' ne vous arrêtez pas, quelqu'un d'autre devra récupérer).

Ce qui suit est uniquement destiné à la propagation des erreurs, et ne peut donc être utilisé que dans les fonctions qui renvoient une erreur et les valeurs zéro de tout autre argument de retour :

Pour renvoyer une erreur sans la modifier en aucune façon :

func Worker(path string) ([]byte, error) {
    data := ioutil.ReadFile(path) or bubble

    return data;
}

Pour renvoyer une erreur avec un message supplémentaire :

func Worker(path string) ([]byte, error) {
    data := ioutil.ReadFile(path) or bubble fmt.Sprintf("reading file %s", path)

    modified := modifyData(data) or bubble "modifying the data"

    return data;
}

Et enfin, introduisez un mécanisme d'adaptateur global pour une modification d'erreur personnalisée :

// Default Bubble Processor
errors.BubbleProcessor(func(msg string, err error) error {
    return fmt.Errorf("%s: %v", msg, err)
})

// Some program might register the following:
errors.BubbleProcessor(func(msg string, err error) error {
    return errors.WithMessage(err, msg)
})

Enfin, pour les quelques endroits où une manipulation vraiment complexe est nécessaire, la voie verbeuse déjà existante est déjà la meilleure voie.

Intéressant. Avoir un gestionnaire de bulles global donne aux personnes qui veulent des traces de pile un endroit pour placer l'appel d'une trace, ce qui est un bon avantage de cette méthode. OTOH, s'il a la signature func(string, error) error , cela signifie que le bouillonnement doit être effectué avec le type d'erreur intégré et non avec un autre type, tel qu'un type concret implémentant error .

De plus, l'existence de or bubble suggère la possibilité de or die ou de or panic . Je ne sais pas si c'est une fonctionnalité ou un bug.

contrairement à un opérateur qui est intrinsèquement cryptique, il est plus facile de saisir ce qu'il fait sans trop de connaissances préalables

C'est peut-être bien lorsque vous le rencontrez pour la première fois. Mais le lire et l'écrire encore et encore - cela semble trop verbeux et prend trop de place pour transmettre une chose assez simple - une erreur non gérée avec une bulle dans la pile. Les opérateurs sont énigmatiques au début, mais ils sont concis et contrastent bien avec tous les autres codes. Ils séparent clairement la logique principale de la gestion des erreurs car il s'agit en fait d'un séparateur. Avoir autant de mots sur une ligne nuira à la lisibilité à mon avis. Fusionnez-les au moins dans orbubble ou supprimez-en un. Je ne vois pas l'intérêt d'avoir deux mots-clés ici. Ça transforme Go en langue parlée et on sait comment ça se passe (VB, par exemple)

Je ne suis pas vraiment fan d'un adaptateur global. Si mon package définit un processeur personnalisé et que le vôtre définit également un processeur personnalisé, qui gagne ?

@ objet88
Je pense que c'est similaire à l'enregistreur par défaut. Vous ne définissez la sortie qu'une seule fois (dans votre programme) et cela affecte tous les packages que vous utilisez.

La gestion des erreurs est très différente de la journalisation ; l'un décrit la sortie informative du programme, l'autre gère le flux du programme. Si je configure l'adaptateur pour faire une chose dans mon package, que j'ai besoin de gérer correctement le flux logique, et qu'un autre package ou programme modifie cela, vous êtes dans un mauvais endroit.

S'il vous plaît, rapportez le Try Catch Enfin et nous n'avons plus besoin de combats. Cela rend tout le monde heureux. Il n'y a rien de mal à emprunter des fonctionnalités et des syntaxes à d'autres langages de programmation. Java l'a fait et C# l'a fait aussi et ce sont tous les deux des langages de programmation très réussis. La communauté GO (ou les auteurs) s'il vous plaît soyez ouvert aux changements lorsque cela est nécessaire.

@KamyarM , je ne suis pas d'accord avec respect ; try/catch ne rend _pas_ tout le monde heureux. Même si vous vouliez implémenter cela dans votre code, une exception levée signifie que tous ceux qui utilisent votre code doivent gérer les exceptions. Ce n'est pas un changement de langue qui peut être localisé dans votre code.

@ objet88
En fait, il me semble qu'un processeur à bulles décrit la sortie d'erreur informative du programme, ce qui ne le rend pas si différent d'un enregistreur. Et j'imagine que vous voulez une seule représentation d'erreur dans toute votre application, et ne pas varier d'un package à l'autre.

Bien que vous puissiez peut-être fournir un court exemple, il y a peut-être quelque chose qui m'échappe.

Merci beaucoup pour vos pouces vers le bas. C'est exactement le problème dont je parle. La communauté GO n'est pas ouverte aux changements et je le sens et je n'aime vraiment pas ça.

Ce n'est probablement pas lié à ce cas mais je cherchais un autre jour l'équivalent Go de l'opérateur ternaire de C++ et je suis tombé sur cette approche alternative :

v := map[bool]int{true : first_expression, false : second_expression} [condition]
au lieu de simplement
v= état ? first_expression : seconde_expression;

Laquelle des 2 formes préférez-vous ? Un code illisible ci-dessus (Go My Way) avec probablement beaucoup de problèmes de performances ou la deuxième syntaxe simple en C++ (Highway) ? Je préfère les gars de l'autoroute. Je ne sais pas pour vous.

Donc pour résumer merci d'apporter de nouvelles syntaxes, empruntez-les à d'autres langages de programmation. Il n'y a rien de mal à cela.

Meilleures salutations,

La communauté GO n'est pas ouverte aux changements et je le sens et je n'aime vraiment pas ça.

Je pense que cela dénature l'attitude qui sous-tend ce que vous vivez. Oui, la communauté produit beaucoup de push back lorsque quelqu'un propose try/catch ou ?:. Mais la raison n'est pas que nous soyons résistants aux nouvelles idées. Nous avons presque tous l'habitude d'utiliser des langages avec ces fonctionnalités. Nous les connaissons assez bien et quelqu'un d'entre nous les utilise quotidiennement depuis des années. Notre résistance est basée sur le fait qu'il s'agit de _vieilles idées_, pas de nouvelles. Nous avons déjà adopté un changement : un changement loin de try/catch et un changement loin d'utiliser ?:. Ce à quoi nous sommes réticents, c'est de changer _back_ pour utiliser ces choses que nous utilisions déjà et que nous n'aimions pas.

En fait, il me semble qu'un processeur à bulles décrit la sortie d'erreur informative du programme, ce qui ne le rend pas si différent d'un enregistreur. Et j'imagine que vous voulez une seule représentation d'erreur dans toute votre application, et ne pas varier d'un package à l'autre.

Et si quelqu'un voulait utiliser le bouillonnement pour transmettre les traces de pile, puis l'utiliser pour prendre une décision. Par exemple, si l'erreur provient d'une opération de fichier, échouez, mais si elle provient du réseau, attendez et réessayez. Je pourrais voir la construction d'une logique pour cela dans un gestionnaire d'erreurs, mais s'il n'y a qu'un seul gestionnaire d'erreurs par exécution, ce serait une recette pour un conflit.

@urandom , c'est peut-être un exemple trivial, mais disons que mon adaptateur renvoie une autre structure qui implémente error , que je prévois de consommer ailleurs dans mon code. Si un autre adaptateur arrive et remplace mon adaptateur, mon code cesse de fonctionner correctement.

@KamyarM La langue et ses idiomes vont de pair. Lorsque nous considérons les modifications apportées à la gestion des erreurs, nous ne parlons pas seulement de changer la syntaxe, mais (potentiellement) également la structure même du code.

Try-catch-finally serait un tel changement très invasif : il changerait fondamentalement la façon dont les programmes de Go sont structurés. En revanche, la plupart des autres propositions que vous voyez ici sont locales à chaque fonction : les erreurs sont toujours des valeurs renvoyées explicitement, le flux de contrôle évite les sauts non locaux, etc.

Pour reprendre votre exemple d'opérateur ternaire : oui, vous pouvez en simuler un aujourd'hui en utilisant une carte, mais j'espère que vous ne le trouverez pas réellement dans le code de production. Il ne suit pas les idiomes. Au lieu de cela, vous verrez généralement quelque chose comme :

    var v int
    if condition {
        v = first_expression
    } else {
        v = second_expression
    }

Ce n'est pas que nous ne voulons pas emprunter la syntaxe, c'est que nous devons considérer comment elle s'intégrerait avec le reste du langage et le reste du code qui existe déjà aujourd'hui.

@KamyarM J'utilise à la fois Go et Java, et je ne veux absolument pas que Go copie la gestion des exceptions à partir de Java. Si vous voulez Java, utilisez Java. Et s'il vous plaît, portez la discussion sur les opérateurs ternaires à un problème approprié, par exemple #23248.

@lpar Donc, si je travaille pour une entreprise et pour une raison inconnue, ils ont choisi GoLang comme langage de programmation, je dois simplement quitter mon emploi et postuler pour un Java !? Allez mec!

@bcmills Vous pouvez compter le code que vous y avez suggéré. Je pense que cela fait 6 lignes de code au lieu d'une et vous obtenez probablement quelques points de complexité cyclomatique du code pour cela (vous utilisez Linter, n'est-ce pas ?).

@carlmjohnson et @bcmills Toute syntaxe ancienne et mature ne signifie pas qu'elle est mauvaise. En fait, je pense que la syntaxe if else est bien plus ancienne que la syntaxe de l'opérateur ternaire.

C'est bien que vous ayez apporté cette chose d'idiome GO. Je pense que ce n'est qu'un des problèmes de cette langue. Chaque fois qu'il y a une demande de changement, quelqu'un dit oh non, c'est contre l'idiome Go. Je le vois comme juste une excuse pour résister aux changements et bloquer toute nouvelle idée.

@KamyarM s'il vous plaît soyez poli. Si vous souhaitez en savoir plus sur certaines réflexions pour garder le langage petit, je vous recommande https://commandcenter.blogspot.com/2012/06/less-is-exponentially-more.html.

Aussi, un commentaire général, sans rapport avec la récente discussion sur try/catch.

Il y a eu beaucoup de propositions dans ce fil. Pour ma part, je n'ai toujours pas l'impression d'avoir une bonne compréhension du ou des problèmes à résoudre. J'aimerais en savoir plus sur eux.

Je serais également ravi si quelqu'un voulait assumer la tâche peu enviable mais importante de maintenir une liste organisée et résumée des problèmes qui ont été discutés.

@josharian Je parlais juste là franchement. Je voulais montrer les problèmes exacts dans la langue ou la communauté. Considérez cela comme plus de critique. GoLang est ouvert à la critique, n'est-ce pas ?

@KamyarM Si vous travailliez pour une entreprise qui avait choisi Rust pour son langage de programmation,

La raison pour laquelle les programmeurs Go ne veulent pas d'exceptions de style Java n'a rien à voir avec un manque de familiarité avec elles. J'ai rencontré des exceptions pour la première fois en 1988 via Lisp, et je suis sûr qu'il y a d'autres personnes dans ce fil qui les ont rencontrées encore plus tôt - l'idée remonte au début des années 1970.

C'est encore plus vrai pour les expressions ternaires. Renseignez-vous sur l'histoire de Go -- Ken Thompson, l'un des créateurs de Go, a implémenté l'opérateur ternaire dans le langage B (prédécesseur de C) aux Bell Labs en 1969. Je pense qu'il est sûr de dire qu'il était conscient de ses avantages et de ses inconvénients lors de l'examen s'il faut l'inclure dans Go.

Go est ouvert à la critique, mais nous exigeons que les discussions sur les forums Go soient polies. Être franc n'est pas la même chose qu'être impoli. Voir la section « Valeurs Gopher » de https://golang.org/conduct. Merci.

@lpar Oui, si Rust avait un tel forum, je le ferais ;-) Sérieusement, je le ferais. Parce que je veux que ma voix soit entendue.

@ianlancetaylor Ai-je utilisé des mots ou un langage vulgaires ? Ai-je utilisé un langage discriminatoire ou des actes d'intimidation envers quelqu'un ou des avances sexuelles importunes ? Je ne pense pas.
Allez mec, nous ne parlons ici que du langage de programmation Go. Il ne s'agit pas d'une religion ou de la politique ou quelque chose comme ça.
J'étais franc. Je voulais que ma voix soit entendue. Je pense que c'est pour ça qu'il y a ce forum. Pour que les voix soient entendues. Vous n'aimerez peut-être pas ma suggestion ou ma critique. C'est OK. Mais je suppose que vous devez me laisser parler et discuter, sinon nous pouvons tous conclure que tout est parfait et qu'il n'y a pas de problème et donc pas besoin de discussions supplémentaires.

@josharian Merci pour l'article, je vais y jeter un œil.

Eh bien, j'ai regardé mes commentaires pour voir s'il y avait quelque chose de mal là-dedans. La seule chose que j'aurais pu insulter (j'appelle toujours cela critique d'ailleurs) est les idiomes du langage de programmation GoLang ! Hahah !

Pour revenir à notre sujet, si vous entendez ma voix, s'il vous plaît, les auteurs Go envisagent de ramener les blocs Try catch. Laissez au programmeur le soin de décider de l'utiliser au bon endroit ou non (vous avez déjà quelque chose de similaire, je veux dire la récupération différée panique alors pourquoi pas Try Catch qui est plus familier aux programmeurs ?).
J'ai suggéré une solution de contournement pour la gestion actuelle des erreurs Go pour la compatibilité descendante. Je ne dis pas que c'est la meilleure option mais je pense que c'est viable.

Je vais me retirer de discuter plus sur ce sujet.

Merci pour l'opportunité.

@KamyarM Vous confondez nos demandes de rester poli avec notre désaccord avec vos arguments. Lorsque les gens ne sont pas d'accord avec vous, vous répondez en termes personnels avec des commentaires tels que "Merci beaucoup pour votre pouce vers le bas. C'est exactement le problème dont je parle. La communauté GO n'est pas ouverte aux changements et je le sens et je ne le fais vraiment pas. c'est pas comme ça."

Encore une fois : soyez poli. Tenez-vous en aux arguments techniques. Évitez les arguments ad hominem qui attaquent les gens plutôt que les idées. Si vous ne comprenez pas vraiment ce que je veux dire, je suis prêt à en discuter hors ligne ; Envoyez moi un email. Merci.

Je vais lancer mon 2c, et j'espère qu'il ne répète pas littéralement quelque chose dans les cent autres commentaires (ou ne marche pas sur la discussion de la proposition d'Urandom).

J'aime l'idée originale qui a été publiée, mais avec deux modifications principales :

  • Bikeshedding syntaxique : je crois fermement que tout ce qui a un flux de contrôle implicite devrait être un opérateur en soi, plutôt qu'une surcharge d'un opérateur existant. Je vais lancer ?! , mais je suis satisfait de tout ce qui n'est pas facilement confondu avec un opérateur existant dans Go.

  • Le RHS de cet opérateur doit prendre une fonction plutôt qu'une expression avec une valeur injectée arbitrairement. Cela permettrait aux développeurs d'écrire un code de gestion des erreurs assez laconique, tout en étant clair sur leur intention et flexible avec ce qu'ils peuvent faire, par exemple

func returnErrorf(s string, args ...interface{}) func(error) error {
  return func(err error) error {
    return errors.New(fmt.Sprintf(s, args...) + ": " + err.Error())
  }
}

func foo(r io.ReadCloser, callAfterClosing func() error, bs []byte) ([]byte, error) {
  // If r.Read fails, returns `nil, errors.New("reading from r: " + err.Error())`
  n := r.Read(bs) ?! returnErrorf("reading from r")
  bs = bs[:n]
  // If r.Close() fails, returns `nil, errors.New("closing r after reading [[bs's contents]]: " + err.Error())`
  r.Close() ?! returnErrorf("closing r after reading %q", string(bs))
  // Not that I advocate this inline-func approach, but...
  callAfterClosing() ?! func(err error) error { return errors.New("oh no!") }
  return bs, nil
}

Le RHS ne doit jamais être évalué si une erreur ne se produit pas, donc ce code n'allouera aucune fermeture ou quoi que ce soit sur le chemin heureux.

Il est également assez simple de "surcharger" ce modèle pour qu'il fonctionne dans des cas plus intéressants. J'ai trois exemples en tête.

Premièrement, nous pourrions avoir le return conditionnel si le RHS est un func(error) (error, bool) , comme ceci (si nous le permettons, je pense que nous devrions utiliser un opérateur distinct des retours inconditionnels. Je vais utilisez ?? , mais ma déclaration "Je m'en fiche tant que c'est distinct" s'applique toujours):

func maybeReturnError(err error) (error, bool) {
  if err == io.EOF {
    return nil, false
  }
  return err, true
}

func id(err error) error { return err }

func ignoreError(err error) (error, bool) { return nil, false }

func foo(n int) error {
  // Does nothing
  id(io.EOF) ?? ignoreError
  // Still does nothing
  id(io.EOF) ?? maybeReturnError
  // Returns the given error
  id(errors.New("oh no")) ?? maybeReturnError
  return nil
}

Alternativement, nous pourrions accepter les fonctions RHS dont les types de retour correspondent à ceux de la fonction externe, comme ceci :

func foo(r io.Reader) ([]int, error) {
  returnError := func(err error) ([]int, error) { return []int{0}, err }
  // returns `[]int{0}, err` on a Read failure
  n := r.Read(make([]byte, 4)) ?! returnError
  return []int{n}, nil
}

Et enfin, si nous le voulons vraiment , nous pouvons généraliser cela pour travailler avec plus que de simples erreurs en changeant le type d'argument :

func returnOpFailed(name string) func(bool) error {
  return func(_ bool) error {
    return errors.New(name + " failed")
  }
}

func returnErrOpFailed(name string) func(error) error {
  return func(err error) error {
    return errors.New(name + " failed: " + err.Error())
  }
}

func foo(c chan int, readInt func() (int, error), d map[int]string) (string, error) {
  n := <-c ?! returnOpFailed("receiving from channel")
  m := readInt() ?! returnErrOpFailed("reading an int")
  result := d[n + m] ?! returnOpFailed("looking up the number")
  return result, nil
}

... Ce que je trouverais personnellement très utile lorsque je dois faire quelque chose de terrible, comme décoder à la main un map[string]interface{} .

Pour être clair, je montre principalement les extensions à titre d'exemples. Je ne sais pas lequel d'entre eux (le cas échéant) offre un bon équilibre entre simplicité, clarté et utilité générale.

J'aimerais revoir la partie "retourner l'erreur/avec un contexte supplémentaire", car je suppose que l'ignorance de l'erreur est déjà couverte par le _ déjà existant.

Je propose un mot-clé de deux mots qui peut être suivi d'une chaîne (éventuellement).

@urandom la première partie de votre proposition est agréable, on peut toujours commencer par ça et laisser le BubbleProcessor pour une deuxième révision. Les préoccupations soulevées par @object88 sont valides OMI ; J'ai récemment vu des conseils comme "vous ne devriez pas écraser le client/transport par défaut de http ", cela deviendrait un autre de ceux-ci.

Il y a eu beaucoup de propositions dans ce fil. Pour ma part, je n'ai toujours pas l'impression d'avoir une bonne compréhension du ou des problèmes à résoudre. J'aimerais en savoir plus sur eux.

Je serais également ravi si quelqu'un voulait assumer la tâche peu enviable mais importante de maintenir une liste organisée et résumée des problèmes qui ont été discutés.

Ça pourrait être toi @josharian si @ianlancetaylor te nomme ? :blush: Je ne sais pas comment d'autres problèmes sont planifiés/discutés mais peut-être que cette discussion est juste utilisée comme une « boîte à suggestions » ?

@KamyarM

@bcmills Vous pouvez compter le code que vous y avez suggéré. Je pense que cela fait 6 lignes de code au lieu d'une et vous obtenez probablement quelques points de complexité cyclomatique du code pour cela (vous utilisez Linter, n'est-ce pas ?).

Cacher la complexité cyclomatique la rend plus difficile à voir mais ne la supprime pas (rappelez-vous strlen ?). Tout comme le fait de « raccourcir » la gestion des erreurs rend la sémantique de gestion des erreurs plus facile à ignorer, mais plus difficile à voir.

Toute déclaration ou expression dans la source qui redirige le contrôle de flux doit être évidente et laconique, mais s'il s'agit d'une décision entre évident ou laconique, l'évidence doit être préférée dans ce cas.

C'est bien que vous ayez apporté cette chose d'idiome GO. Je pense que ce n'est qu'un des problèmes de cette langue. Chaque fois qu'il y a une demande de changement, quelqu'un dit oh non, c'est contre l'idiome Go. Je le vois comme juste une excuse pour résister aux changements et bloquer toute nouvelle idée.

Il y a une différence entre nouveau et bénéfique. Croyez-vous que parce que vous avez une idée, l'existence même de celle-ci mérite l'approbation ? À titre d'exercice, veuillez regarder le traqueur de problèmes et essayez d'imaginer Go aujourd'hui si chaque idée était approuvée, indépendamment de ce que la communauté pensait.

Peut-être pensez-vous que votre idée est meilleure que les autres. C'est là qu'intervient la discussion. Au lieu de dégénérer la conversation pour parler de la façon dont tout le système est cassé à cause d'idiomes, adressez les critiques directement, point par point, ou trouvez un terrain d'entente entre vous et vos pairs.

@gdm85
J'ai ajouté le processeur pour une sorte de personnalisation de la part de l'erreur renvoyée. Et bien que je pense que c'est un peu comme utiliser l'enregistreur par défaut, dans la mesure où vous pouvez vous en sortir la plupart du temps, j'ai dit que j'étais ouvert aux suggestions. Et pour mémoire, je ne pense pas que l'enregistreur par défaut et le client http par défaut soient même à distance dans la même catégorie.

J'aime aussi la proposition de @gburgessiv , bien que je ne sois pas un grand fan de l'opérateur cryptique lui-même (peut-être au moins choisissez ? comme dans Rust, même si je pense toujours que c'est cryptique). Cela semblerait-il plus lisible :

func foo(r io.ReadCloser, callAfterClosing func() error, bs []byte) ([]byte, error) {
  // If r.Read fails, returns `nil, errors.New("reading from r: " + err.Error())`
  n := r.Read(bs) or returnErrorf("reading from r")
  bs = bs[:n]
  // If r.Close() fails, returns `nil, errors.New("closing r after reading [[bs's contents]]: " + err.Error())`
  r.Close() or returnErrorf("closing r after reading %q", string(bs))
  // Not that I advocate this inline-func approach, but...
  callAfterClosing() or func(err error) error { return errors.New("oh no!") }
  return bs, nil
}

Et j'espère que sa proposition inclura également une implémentation par défaut d'une fonction similaire à son returnErrorf quelque part dans le package errors . Peut-être errors.Returnf() .

@KamyarM
Vous avez déjà exprimé votre opinion ici et n'avez reçu aucun commentaire ou réaction favorable à la cause de l'exception. Je ne vois pas ce que répéter la même chose accomplira, tbh, en plus de perturber les autres discussions. Et si tel est votre objectif, ce n'est tout simplement pas cool.

@josharian , je vais essayer de résumer brièvement la discussion. Ce sera biaisé, puisque j'ai une proposition dans le mix, et incomplète, puisque je ne suis pas à la hauteur de relire tout le fil.

Le problème que nous essayons de résoudre est l'encombrement visuel causé par la gestion des erreurs Go. Voici un bon exemple ( source ) :

func (ds *GitDataSource) Fetch(from, to string) ([]string, error) {
    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
    }
    return dirs, nil
}

Plusieurs commentateurs de ce fil ne pensent pas que cela doive être corrigé ; ils sont heureux que la gestion des erreurs soit intrusive, car la gestion des erreurs est tout aussi importante que la gestion du cas sans erreur. Pour eux, aucune des propositions ici ne vaut la peine.

Les propositions qui tentent de simplifier le code comme celui-ci se divisent en quelques groupes.

Certains proposent une forme de gestion des exceptions. Étant donné que Go aurait pu choisir la gestion des exceptions au début et ne pas le faire, il semble peu probable qu'elles soient acceptées.

De nombreuses propositions ici choisissent une action par défaut, comme revenir de la fonction (la proposition d'origine) ou paniquer, et suggèrent un peu de syntaxe qui rend cette action facile à exprimer. À mon avis, toutes ces propositions échouent, car elles privilégient une action au détriment des autres. J'utilise régulièrement des retours, t.Fatal et log.Fatal pour gérer les erreurs, parfois le même jour.

D'autres propositions ne fournissent aucun moyen d'augmenter ou d'envelopper l'erreur d'origine, ou de la rendre beaucoup plus difficile à envelopper qu'autrement. Celles-ci sont également inadéquates, car le wrapping est le seul moyen d'ajouter du contexte aux erreurs, et si nous le rendons trop facile à ignorer, cela sera fait encore moins fréquemment qu'aujourd'hui.

La plupart des propositions restantes ajoutent un peu de sucre et parfois un peu de magie pour simplifier les choses sans contraindre les actions possibles ou la capacité d'envelopper. Les propositions de My et @bcmills ajoutent une quantité minimale de sucre et zéro magie pour augmenter légèrement la lisibilité, et aussi pour éviter une sorte de bogue désagréable .

Quelques autres propositions ajoutent une sorte de flux de contrôle non local contraint, comme une section de gestion des erreurs au début ou à la fin d'une fonction.

Enfin, @mpvl reconnaît que la gestion des erreurs peut devenir très délicate en présence de panique. Il suggère un changement plus radical de la gestion des erreurs Go pour améliorer l'exactitude ainsi que la lisibilité. Il a un argument convaincant, mais au final, je pense que ses cas ne nécessitent pas de changements drastiques et peuvent être traités avec les mécanismes existants .

Toutes nos excuses à tous ceux dont les idées ne sont pas représentées ici.

J'ai l'impression que quelqu'un va me demander quelle est la différence entre le sucre et la magie. (Je me le demande moi-même.)

Sugar est un peu de syntaxe qui raccourcit le code sans modifier fondamentalement les règles du langage. L'opérateur d'affectation courte := est le sucre. Il en va de même pour l'opérateur ternaire de C ?: .

La magie est une perturbation plus violente du langage, comme introduire une variable dans une portée sans la déclarer, ou effectuer un transfert de contrôle non local.

La ligne est définitivement floue.

Merci d'avoir fait ça, @jba. Très utile. Juste pour souligner les points saillants, les problèmes identifiés jusqu'à présent sont les suivants :

l'encombrement visuel causé par la gestion des erreurs Go

et

la gestion des erreurs peut devenir très délicate en présence de panique

S'il y a d'autres problèmes fondamentalement différents (pas de solutions) que @jba et moi avons manqués, veuillez intervenir (n'importe qui). FWIW, je considérerais l'ergonomie, le fouillis de code, la complexité cyclomatique, le bégaiement, le passe-partout, etc. comme des variantes du problème du "fouillis visuel" (ou groupe de problèmes).

@josharian Voulez-vous considérer les problèmes de portée (https://github.com/golang/go/issues/21161#issuecomment-319277657) comme une variante du problème de « l'encombrement visuel », ou comme un problème distinct ?

@bcmills me semble distinct, car il s'agit de problèmes d'exactitude subtils, par opposition à l'esthétique/l'ergonomie (ou tout au plus des problèmes d'exactitude impliquant du code en vrac). Merci! Vous voulez modifier mon commentaire et en ajouter un résumé d'une ligne ?

J'ai l'impression que quelqu'un va me demander quelle est la différence entre le sucre et la magie. (Je me le demande moi-même.)

J'utilise cette définition de la magie : en regardant un peu de code source, si vous pouvez comprendre ce qu'il est censé faire par une variante de l'algorithme suivant :

  1. Recherchez tous les identificateurs, mots-clés et constructions grammaticales présents sur la ligne ou dans la fonction.
  2. Pour les constructions grammaticales et les mots-clés, consultez la documentation de la langue officielle.
  3. Pour les identifiants, il devrait y avoir un mécanisme clair pour les localiser en utilisant les informations du code que vous regardez, en utilisant les portées dans lesquelles le code se trouve actuellement, tel que défini par le langage, à partir duquel vous pouvez obtenir la définition de l'identifiant, qui sera être précis au moment de l'exécution.

Si cet algorithme produit _de manière fiable_ une compréhension correcte de ce que le code va faire, ce n'est pas magique. Si ce n'est pas le cas, c'est qu'il contient une certaine quantité de magie. La récursivité avec laquelle vous devez l'appliquer lorsque vous essayez de suivre les références de la documentation et les définitions d'identifiant jusqu'à d'autres définitions d'identifiant affecte la _complexité_, mais pas la _magie_, des constructions/code en question.

Exemples de magie : Identificateurs sans chemin clair vers leur origine parce que vous les avez importés sans espace de noms (importations de points dans Go, surtout si vous en avez plusieurs). Toute capacité qu'une langue peut avoir à définir de manière non locale ce à quoi un opérateur résoudra, comme dans les langues dynamiques où le code peut redéfinir complètement une référence de fonction de manière non locale, ou redéfinir ce que la langue fait pour des identifiants inexistants. Objets construits par des schémas chargés à partir d'une base de données au moment de l'exécution, donc au moment du code, on espère plus ou moins aveuglément qu'ils seront là.

La bonne chose à ce sujet est que cela élimine presque toute la subjectivité de la question.

De retour sur le sujet à l'étude, il semble qu'il y ait déjà une tonne de propositions faites, et les chances que quelqu'un d'autre résolve le problème avec une autre proposition qui fasse dire à tout le monde « Oui ! C'est ça ! » approche de zéro.

Il me semble que la conversation devrait peut-être aller dans le sens d'une catégorisation des différentes dimensions des propositions faites ici, et d'une idée de la priorisation. J'aimerais particulièrement voir cela dans le but de révéler des exigences contradictoires vaguement appliquées par les gens ici.

Par exemple, j'ai vu des plaintes concernant l'ajout de sauts supplémentaires dans le flux de contrôle. Mais pour moi, dans le jargon de la proposition très originale, j'apprécie de ne pas avoir à ajouter || &PathError{"chdir", dir, err} huit fois dans une fonction si elles sont communes. (Je sais que Go n'est pas aussi allergique au code répété que d'autres langages, mais néanmoins, le code répété présente un risque très élevé de bugs de divergence.) le code ne peut pas circuler de haut en bas, de gauche à droite, sans sauts. Qu'est-ce qui est généralement considéré comme le plus important ? Je soupçonne qu'un examen attentif des exigences que les gens imposent implicitement au code révélerait d'autres exigences mutuellement contradictoires.

Mais en général, j'ai l'impression que si la communauté pouvait se mettre d'accord sur les exigences après toute cette analyse, la bonne solution pourrait très bien en sortir clairement, ou à tout le moins, l'ensemble de solutions correct sera si évidemment contraint que le problème devient traitable.

(Je ferais également remarquer qu'étant donné qu'il s'agit d'une proposition, le comportement actuel devrait en général être soumis à la même analyse que les nouvelles propositions. L'objectif est une amélioration significative, pas la perfection ; rejeter deux ou trois améliorations significatives car aucune d'entre elles sont parfaits est un chemin vers la paralysie. Toutes les propositions sont de toute façon rétrocompatibles, donc dans les cas où l'approche actuelle est déjà la meilleure de toute façon (à mon humble avis, le cas où chaque erreur est gérée légitimement différemment, ce qui, d'après mon expérience, est rare mais arrive) , l'approche actuelle sera toujours disponible.)

J'y réfléchis depuis la deuxième fois que j'ai écrit if err !=nil dans une fonction, il me semble qu'une solution assez simple serait de permettre un retour conditionnel qui ressemble à la première partie de ternaire avec la compréhension étant if la condition échoue, nous ne revenons pas.

Je ne sais pas dans quelle mesure cela fonctionnerait en termes d'analyse/compilation, mais il semble que cela devrait être assez facile à interpréter comme une instruction if où le '?' est vu sans rompre la compatibilité là où il n'est pas vu, alors j'ai pensé le jeter là-bas en option.

De plus, il y aurait d'autres utilisations pour cela au-delà de la gestion des erreurs.

donc tu peux faire quelque chose comme ça :

func example1() error {
    err := doSomething()
    return err != nil ? err
    //more code
}

func example2() (*Mything, error) {
    err := doSomething()
    return err != nil ? nil, err
    //more code
}

Nous pourrions également faire des choses comme ceci lorsque nous avons du code de nettoyage, en supposant que handleErr a renvoyé une erreur :

func example3() error {
    err := doSomething()
    return err !=nil ? handleErr(err)
    //more code
}

func example4() (*Mything, error) {
    err := doSomething()
    return err != nil ? nil, handleErr(err)
    //more code
}

Peut-être s'ensuit-il alors également que vous seriez en mesure de réduire cela à une seule ligne si vous le vouliez :

func example5() error {
    return err := doSomething(); err !=nil ? handleErr(err)
    //more code
}

func example6() (*Mything, error) {
    return err := doSomething(); err !=nil ? nil, handleErr(err)
    //more code
}

L'exemple de récupération précédent de @jba pourrait potentiellement ressembler à ceci :

func (ds *GitDataSource) Fetch(from, to string) ([]string, error) {

    return err := createFolderIfNotExist(to); err != nil ? nil, err
    return err := clearFolder(to); err != nil ? nil, err
    return err := cloneRepo(to, from); err != nil ? nil, err
    dirs, err := getContentFolders(to)

    return dirs, err
}

Serait intéressé par les réactions à cette suggestion, peut-être pas une énorme victoire pour économiser le passe-partout, mais reste assez explicite et, espérons-le, ne nécessite qu'un petit changement rétrocompatible (peut-être des hypothèses massivement inexactes sur ce front).

Vous pourriez peut-être séparer cela avec un retour séparé? mot-clé qui peut ajouter à la clarté et rendre la vie plus simple en termes de ne pas avoir à se soucier de la compatibilité avec return (en pensant à tous les outils), ceux-ci pourraient alors simplement être réécrits en interne comme des instructions if/return, nous donnant ceci :

func (ds *GitDataSource) Fetch(from, to string) ([]string, error) {

    return? err := createFolderIfNotExist(to); err != nil ? nil, err
    return? err := clearFolder(to); err != nil ? nil, err
    return? err := cloneRepo(to, from); err != nil ? nil, err
    dirs, err := getContentFolders(to)

    return dirs, err
}

Il ne semble pas y avoir beaucoup de différence entre

return err != nil ? err

et

 if err != nil { return err }

De plus, vous pouvez parfois vouloir faire autre chose que revenir, comme appeler panic ou log.Fatal .

Je tourne sur ça depuis que j'ai fait une proposition la semaine dernière, et je suis arrivé à la conclusion que je suis d'accord avec @thejerf : nous avons discuté proposition après proposition sans vraiment prendre du recul et examiner ce que nous aimons à propos de chacun, l'aversion à propos de chacun, et quelles sont les priorités pour une solution « correcte ».

Les exigences les plus fréquemment énoncées sont qu'à la fin de la journée, Go doit être capable de gérer 4 cas de gestion d'erreurs :

  1. Ignorer l'erreur
  2. Renvoyer l'erreur non modifiée
  3. Renvoyer l'erreur avec le contexte ajouté
  4. Paniquer (ou tuer le programme)

Les propositions semblent appartenir à l'une des trois catégories suivantes :

  1. Revenez à un style try-catch-finally de gestion des erreurs.
  2. Ajouter une nouvelle syntaxe/intégrés pour gérer les 4 cas énumérés ci-dessus
  3. Affirmer que cela gère assez bien certains cas et proposer une syntaxe/des fonctions intégrées pour aider dans les autres cas.

Les critiques des propositions données semblent être partagées entre les préoccupations concernant la lisibilité du code, les sauts non évidents, l'ajout implicite de variables aux portées et la concision. Personnellement, je pense qu'il y a eu beaucoup d'opinions personnelles dans les critiques des propositions. Je ne dis pas que c'est une mauvaise chose, mais il me semble qu'il n'y a pas vraiment de critères objectifs pour évaluer les propositions.

Je ne suis probablement pas la personne qui essaie de créer cette liste de critères, mais je pense qu'il serait très utile que quelqu'un dresse cette liste. J'ai essayé de décrire ma compréhension du débat jusqu'à présent comme point de départ pour décomposer 1. ce que nous avons vu, 2. ce qui ne va pas, 3. pourquoi ces choses ne vont pas et 4. ce que nous voudrions aime voir à la place. Je pense avoir capturé une quantité décente des 3 premiers éléments, mais j'ai du mal à trouver une réponse pour l'élément 4 sans recourir à "ce que Go a actuellement".

@jba a un autre bon commentaire de résumé ci-dessus, pour plus de contexte. Il dit une grande partie de ce que j'ai dit ici, avec des mots différents.

@ianlancetaylor , ou toute autre personne impliquée plus étroitement que moi dans le projet, vous

Je ne pense pas pouvoir rédiger un ensemble formel complet de critères. Le mieux que je puisse faire est une liste incomplète de choses importantes qui ne devraient être ignorées que s'il y a un avantage significatif à le faire.

  • Bon support pour 1) ignorer une erreur ; 2) renvoyer une erreur non modifiée ; 3) encapsuler une erreur avec un contexte supplémentaire.
  • Alors que le code de gestion des erreurs doit être clair, il ne doit pas dominer la fonction. Il doit être facile de lire le code de gestion sans erreur.
  • Le code Go 1 existant doit continuer à fonctionner, ou à tout le moins, il doit être possible de traduire mécaniquement Go 1 vers la nouvelle approche avec une fiabilité totale.
  • La nouvelle approche devrait encourager les programmeurs à gérer correctement les erreurs. Il devrait idéalement être facile de faire la bonne chose, quelle que soit la bonne chose dans n'importe quelle situation.
  • Toute nouvelle approche doit être plus courte et/ou moins répétitive que l'approche actuelle, tout en restant claire.
  • La langue fonctionne aujourd'hui, et chaque changement a un coût. Le bénéfice du changement doit clairement valoir le coût. Cela ne devrait pas être juste un lavage, cela devrait être clairement mieux.

J'espère recueillir les notes à un moment donné ici moi-même, mais je veux aborder ce qui est à mon humble avis une autre pierre d'achoppement majeure dans cette discussion, la trivialité du ou des exemples.

J'ai extrait cela d'un de mes projets réels et je l'ai nettoyé pour une publication externe. (Je pense que stdlib n'est pas la meilleure source, car il manque des problèmes de journalisation, entre autres..)

func NewClient(...) (*Client, error) {
    listener, err := net.Listen("tcp4", listenAddr)
    if err != nil {
        return nil, err
    }
    defer func() {
        if err != nil {
            listener.Close()
        }
    }()

    conn, err := ConnectionManager{}.connect(server, tlsConfig)
    if err != nil {
        return nil, err
    }
    defer func() {
        if err != nil {
            conn.Close()
        }
    }()

    if forwardPort == 0 {
        env, err := environment.GetRuntimeEnvironment()
        if err != nil {
            log.Printf("not forwarding because: %v", err)
        } else {
            forwardPort, err = env.PortToForward()
            if err != nil {
                log.Printf("env couldn't provide forward port: %v", err)
            }
        }
    }
    var forwardOut *forwarding.ForwardOut
    if forwardPort != 0 {
        u, _ := url.Parse(fmt.Sprintf("http://127.0.0.1:%d", forwardPort))
        forwardOut = forwarding.NewOut(u)
    }

    client := &Client{...}

    toServer := communicationProtocol.Wrap(conn)
    err = toServer.Send(&client.serverConfig)
    if err != nil {
        return nil, err
    }

    err = toServer.Send(&stprotocol.ClientProtocolAck{ClientVersion: Version})
    if err != nil {
        return nil, err
    }

    session, err := communicationProtocol.FinalProtocol(conn)
    if err != nil {
        return nil, err
    }
    client.session = session

    return client, nil
}

(Ne déformons pas trop le code. Je ne peux pas vous arrêter bien sûr, mais rappelez-vous que ce n'est pas mon vrai code et qu'il a été un peu mutilé. Et je ne peux pas vous empêcher de publier votre propre exemple de code.)

Remarques :

  1. Ce n'est pas un code parfait ; Je renvoie beaucoup d'erreurs nues parce que c'est si simple, exactement le genre de code avec lequel nous avons des problèmes. Les propositions doivent être notées à la fois par leur concision et par la facilité avec laquelle elles démontrent qu'elles corrigent ce code.
  2. La clause if forwardPort == 0 continue _délibérément_ à travers les erreurs, et oui, c'est le vrai comportement, pas quelque chose que j'ai ajouté pour cet exemple.
  3. Ce code SOIT renvoie un client connecté valide OU il renvoie une erreur et aucune fuite de ressource, donc la gestion autour de .Close() (uniquement si la fonction est en erreur) est délibérée. Notez également que les erreurs de Close disparaissent, comme c'est assez typique dans le vrai Go.
  4. Le numéro de port est limité ailleurs, donc url.Parse ne peut pas échouer (par examen).

Je ne prétends pas que cela démontre tous les comportements d'erreur possibles, mais cela couvre toute une gamme. (Je défends souvent Go sur HN et autres en soulignant qu'au moment où mon code a fini de cuire, c'est souvent le cas dans mes serveurs de réseau que j'ai _toutes sortes_ de comportements erronés ; en examinant mon propre code de production, à partir du 1/3 jusqu'à la moitié des erreurs ont fait autre chose que simplement être renvoyé.)

Je vais également (re-)publier ma propre proposition (mise à jour) telle qu'appliquée à ce code (à moins que quelqu'un ne me convainque qu'il a quelque chose d'encore mieux avant), mais dans l'intérêt de ne pas monopoliser la conversation, je vais attendre au moins le week-end. (Ceci est moins de texte qu'il n'y paraît car c'est un gros morceau de source, mais quand même....)

L'utilisation de trytry n'est qu'un raccourci pour if != nil return réduit le code de 6 lignes sur 59, soit environ 10 %.

func NewClient(...) (*Client, error) {
    listener, err := net.Listen("tcp4", listenAddr)
    try err

    defer func() {
        if err != nil {
            listener.Close()
        }
    }()

    conn, err := ConnectionManager{}.connect(server, tlsConfig)
    try err

    defer func() {
        if err != nil {
            conn.Close()
        }
    }()

    if forwardPort == 0 {
        env, err := environment.GetRuntimeEnvironment()
        if err != nil {
            log.Printf("not forwarding because: %v", err)
        } else {
            forwardPort, err = env.PortToForward()
            if err != nil {
                log.Printf("env couldn't provide forward port: %v", err)
            }
        }
    }
    var forwardOut *forwarding.ForwardOut
    if forwardPort != 0 {
        u, _ := url.Parse(fmt.Sprintf("http://127.0.0.1:%d", forwardPort))
        forwardOut = forwarding.NewOut(u)
    }

    client := &Client{...}

    toServer := communicationProtocol.Wrap(conn)
    err = toServer.Send(&client.serverConfig)
    try err

    err = toServer.Send(&stprotocol.ClientProtocolAck{ClientVersion: Version})
    try err

    session, err := communicationProtocol.FinalProtocol(conn)
    try err

    client.session = session

    return client, nil
}

Notamment, à plusieurs endroits, je voulais écrire try x() mais je ne pouvais pas car j'avais besoin que err soit défini pour que les différés fonctionnent correctement.

Un de plus. Si nous avons try quelque chose qui peut arriver sur les lignes d'affectation, cela descend à 47 lignes.

func NewClient(...) (*Client, error) {
    try listener, err := net.Listen("tcp4", listenAddr)

    defer func() {
        if err != nil {
            listener.Close()
        }
    }()

    try conn, err := ConnectionManager{}.connect(server, tlsConfig)

    defer func() {
        if err != nil {
            conn.Close()
        }
    }()

    if forwardPort == 0 {
        env, err := environment.GetRuntimeEnvironment()
        if err != nil {
            log.Printf("not forwarding because: %v", err)
        } else {
            forwardPort, err = env.PortToForward()
            if err != nil {
                log.Printf("env couldn't provide forward port: %v", err)
            }
        }
    }
    var forwardOut *forwarding.ForwardOut
    if forwardPort != 0 {
        u, _ := url.Parse(fmt.Sprintf("http://127.0.0.1:%d", forwardPort))
        forwardOut = forwarding.NewOut(u)
    }

    client := &Client{...}

    toServer := communicationProtocol.Wrap(conn)
    try err = toServer.Send(&client.serverConfig)

    try err = toServer.Send(&stprotocol.ClientProtocolAck{ClientVersion: Version})

    try session, err := communicationProtocol.FinalProtocol(conn)

    client.session = session

    return client, nil
}
import "github.com/pkg/errors"

func Func3() (T1, T2, error) {...}

type PathError {
    err Error
    x   T3
    y   T4
}

type MiscError {
    x   T5
    y   T6
    err Error
}


func Foo() (T1, T2, error) {
    // Old school
    a, b, err := Func(3)
    if err != nil {
        return nil
    }

    // Simplest form.
    // If last unhandled arg's type is same 
    // as last param of func,
    // then use anon variable,
    // check and return
    a, b := Func3()
    /*    
    a, b, err := Func3()
    if err != nil {
         return T1{}, T2{}, err
    }
    */

    // Simple wrapper
    // If wrappers 1st param TypeOf Error - then pass last and only unhandled arg from Func3() there
    a, b, errors.WithStack() := Func3() 
    /*
    a, b, err := Func3()
    if err != nil {
        return T1{}, T2{}, errors.WithStack(err)
    }
    */

    // Bit more complex wrapper
    a, b, errors.WithMessage("unable to get a and b") := Func3()
    /*
    a, b, err := Func3()
    if err != nil {
        return T1{}, T2{}, errors.WithMessage(err, "unable to get a and b")
    }
    */

    // More complex wrapper
    // If wrappers 1nd param TypeOf is not Error - then pass last and only unhandled arg from Func3() as last
    a, b, fmt.Errorf("at %v Func3() return error %v", time.Now()) := Func3()
    /*
    a, b, err := Func3()
    if err != nil {
        return T1{}, T2{}, fmt.Errorf("at %v Func3() return error %v", time.Now(), err)
    }
    */

    // Wrapping with error types
    a, b, &PathError{x,y} := Func3()
    /*
    a, b, err := Func3()
    if err != nil {
        return T1{}, T2{}, &PathError{err, x, y}
    }
    */
    a, b, &MiscError{x,y} := Func3()
    /*
    a, b, err := Func3()
    if err != nil {
        return T1{}, T2{}, &MiscError{x, y, err}
    }
    */

    return a, b, nil
}

Légèrement magique (n'hésitez pas à -1) mais prend en charge la traduction mécanique

Voici à quoi ressemblerait (un peu mis à jour) ma proposition :

func NewClient(...) (*Client, error) {
    defer annotateError("client couldn't be created")

    listener := pop net.Listen("tcp4", listenAddr)
    defer closeOnErr(listener)
    conn := pop ConnectionManager{}.connect(server, tlsConfig)
    defer closeOnErr(conn)

    if forwardPort == 0 {
        env, err := environment.GetRuntimeEnvironment()
        if err != nil {
            log.Printf("not forwarding because: %v", err)
        } else {
            forwardPort, err = env.PortToForward()
            if err != nil {
                log.Printf("env couldn't provide forward port: %v", err)
            }
        }
    }
    var forwardOut *forwarding.ForwardOut
    if forwardPort != 0 {
         forwardOut = forwarding.NewOut(pop url.Parse(fmt.Sprintf("http://127.0.0.1:%d", forwardPort)))
    }

    client := &Client{listener: listener, conn: conn, forward: forwardOut}

    toServer := communicationProtocol.Wrap(conn)
    pop toServer.Send(&client.serverConfig)
    pop toServer.Send(&stprotocol.ClientProtocolAck{ClientVersion: Version})
    session := pop communicationProtocol.FinalProtocol(conn)
    client.session = session

    return client, nil
}

func closeOnErr(c io.Closer) {
    if err := erroring(); err != nil {
        closeErr := c.Close()
        if err != nil {
            seterr(multierror.Append(err, closeErr))
        }
    }
}

func annotateError(annotation string) {
    if err := erroring(); err != nil {
        log.Printf("%s: %v", annotation, err)
        seterr(errwrap.Wrapf(annotation +": {{err}}", err))
    }
}

Définitions :

pop est légal pour les expressions de fonction où la valeur la plus à droite est une erreur. Il est défini comme « si l'erreur n'est pas nulle, sortir de la fonction avec les valeurs zéro pour toutes les autres valeurs dans les résultats et cette erreur, sinon produire un ensemble de valeurs sans l'erreur ». pop n'a pas d'interaction privilégiée avec erroring() ; un retour normal d'une erreur sera toujours visible à erroring() . Cela signifie également que vous pouvez renvoyer des valeurs non nulles pour les autres valeurs de retour, tout en utilisant la gestion des erreurs différée. La métaphore fait sortir l'élément le plus à droite de la "liste" des valeurs de retour.

erroring() est défini comme remontant la pile jusqu'à la fonction différée en cours d'exécution, puis l'élément de pile précédent (la fonction dans laquelle le différé s'exécute, NewClient dans ce cas), pour accéder à la valeur de l'erreur renvoyée actuellement en cours. Si la fonction n'a pas ce paramètre, paniquez ou retournez nil (ce qui est le plus logique). Cette valeur d'erreur n'a pas besoin de provenir de pop ; c'est tout ce qui renvoie une erreur de la fonction cible.

seterr(error) vous permet de modifier la valeur d'erreur renvoyée. Ce sera alors l'erreur vue par tous les futurs appels erroring() , ce qui, comme indiqué ici, permet le même chaînage basé sur le report que celui qui peut être fait maintenant.

J'utilise ici le wrapping et la multierror hashicorp ; insérez vos propres paquets intelligents comme vous le souhaitez.

Même avec la fonction supplémentaire définie, la somme est plus courte. Je m'attends à amortir les deux fonctions sur des utilisations supplémentaires, elles ne devraient donc compter que partiellement.

Observez que je laisse simplement la gestion de forwardPort seule, plutôt que d'essayer de brouiller davantage la syntaxe autour de celle-ci. À titre exceptionnel, il est normal que cela soit plus détaillé.

La chose la plus intéressante à propos de cette proposition IMHO ne peut être vue que si vous imaginez essayer d'écrire cela avec des exceptions conventionnelles. Il finit par s'imbriquer assez profondément, et gérer la _collection_ des erreurs qui peuvent se produire est assez fastidieux avec la gestion des exceptions. (Tout comme dans le vrai code Go, les erreurs .Close ont tendance à être ignorées, les erreurs qui se produisent dans les gestionnaires d'exceptions elles-mêmes ont tendance à être ignorées dans le code basé sur les exceptions.)

Cela étend les modèles Go existants tels que defer et l'utilisation d'erreurs en tant que valeurs pour créer facilement des modèles de gestion des erreurs qui, dans certains cas, sont difficiles à exprimer avec le Go actuel ou les exceptions, ne nécessite pas de chirurgie radicale au runtime (je ne pense pas), et aussi, ne nécessite pas réellement un Go 2.0.

Les inconvénients incluent la revendication de erroring , pop , et seterr comme mots-clés, ce qui entraîne la surcharge de defer pour ces fonctionnalités, le fait que la gestion des erreurs factorisées saute à certains aux fonctions de manipulation, et qu'il ne fait rien pour "forcer" une manipulation correcte. Bien que je ne sois pas sûr que le dernier soit possible, car par l'exigence (correcte) d'être rétrocompatible, vous pouvez toujours faire la chose actuelle.

Discussion très intéressante ici.

Je voudrais garder la variable d'erreur sur le côté gauche afin qu'aucune variable apparaissant par magie ne soit introduite. Comme la proposition originale, j'aimerais que les erreurs soient traitées sur la même ligne. Je n'utiliserais pas l'opérateur || car il me semble "trop ​​​​booléen" et cache en quelque sorte le "retour".

Je le rendrais donc plus lisible en utilisant le mot clé étendu "retour?". En C#, le point d'interrogation est utilisé à certains endroits pour créer des raccourcis. Par exemple. au lieu d'écrire :

if(foo != null)
{ foo.Bar(); }

tu peux juste écrire :
foo?.Bar();

Donc pour Go 2 je voudrais proposer cette solution :

func foobar() error {
    return fmt.Errorf("Some error happened")
}

// Implicitly return err (there must be exactly one error variable on the left side)
err := foobar() return?
// Explicitly return err
err := foobar() return? err
// Return extended error info
err := foobar() return? &PathError{"chdir", dir, err}
// Return tuple
err := foobar() return? -1, err
// Return result of function (e. g. for logging the error)
err := foobar() return? handleError(err)
// This doesn't compile as you ignore the error intentionally
foobar() return?

Juste une pensée:

foo, err := myFunc()
erreur != néant ? retour wrap (err)

Ou

si erreur != nil ? retour wrap (err)

Si vous êtes prêt à mettre des accolades autour de cela, nous n'avons pas besoin de changer quoi que ce soit !

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

Vous pouvez avoir _tout_ la gestion personnalisée que vous voulez (ou pas du tout), vous avez enregistré deux lignes de code du cas typique, c'est 100% rétrocompatible (car il n'y a pas de changement de langue), c'est plus compact, et c'est facile. Il touche de nombreux points de conduite d' gofmt ?

J'ai écrit ceci avant de lire la suggestion de Carlmjohnson, qui est similaire...

Juste un # avant une erreur.

Mais dans une application du monde réel, vous devrez toujours écrire le if err != nil { ... } normal pour pouvoir consigner les erreurs, ce qui rend inutile la gestion des erreurs minimaliste, à moins que vous ne puissiez ajouter un middleware de retour via des annotations appelées after , qui s'exécute après le retour des fonctions... (comme defer mais avec des arguments).

@after(func (data string, err error) {
  if err != nil {
    log.Error("error", data, " - ", err)
  }
})
func alo() (string, error) {
  // this is the equivalent of
  // data, err := db.Find()
  // if err != nil { 
  //   return "", err 
  // }
  str, #err := db.Find()

  // ...

  #err = db.Create()

  // ...

  return str, nil
}

func alo2() ([]byte, []byte, error, error) {
  // this is the equivalent of
  // data, data2, err, errNotFound := db.Find()
  // if err != nil { 
  //   return nil, nil, err, nil
  // } else if errNotFound != nil {
  //   return nil, nil, nil, errNotFound
  // }
  data, data2, #err, #errNotFound := db.Find()

  // ...

  return data, data2, nil, nil
}

plus propre que:

func alo() (string, error) {
  str, err := db.Find()
  if err != nil {
    log.Error("error on find in database", err)
    return "", err
  }

  // ...

  if err := db.Create(); err != nil {
    log.Error("error on create", err)
    return "", err
  }

  // ...

  return str, nil
}

func alo2() ([]byte, []byte, error, error) {
  data, data2, err, errNotFound := db.Find()
  if err != nil { 
    return nil, nil, err, nil
  } else if errNotFound != nil {
    return nil, nil, nil, errNotFound
  }

  // ...

  return data, data2, nil, nil
}

Que diriez-vous d'une déclaration Swift comme guard , sauf qu'au lieu de guard...else c'est guard...return :

file, err := os.Open("fails.txt")
guard err return &FooError{"Couldn't foo fails.txt", err}

guard os.Remove("fails.txt") return &FooError{"Couldn't remove fails.txt", err}

J'aime l'explicitation de la gestion des erreurs de go. Le seul problème, à mon humble avis, c'est l'espace que cela prend. Je suggérerais 2 ajustements:

  1. autorise l'utilisation d'éléments nil-able dans un contexte booléen, où nil est équivalent à false , non- nil à true
  2. prend en charge les opérateurs d'instructions conditionnelles sur une seule ligne, comme && et ||

Alors

file, err := os.Open("fails.txt")
if err != nil {
    return &FooError{"Couldn't foo fails.txt", err}
}

peut devenir

file, err := os.Open("fails.txt")
if err {
    return &FooError{"Couldn't foo fails.txt", err}
}

ou même plus court

file, err := os.Open("fails.txt")
err && return &FooError{"Couldn't foo fails.txt", err}

et, nous pouvons faire

i,ok := v.(int)
ok || return fmt.Errorf("not a number")

ou peut-être

i,ok := v.(int)
ok && s *= i

Si la surcharge de && et || crée trop d'ambiguïté, peut-être que d'autres caractères (pas plus de 2) peuvent être sélectionnés, par exemple ? et # ou ?? et ## , ou ?? et !! , peu importe. Le but étant de prendre en charge une instruction conditionnelle sur une seule ligne avec un minimum de caractères "bruyants" (pas de parenthèses, d'accolades, etc.) nécessaires. Les opérateurs && et || sont bons car cet usage a des précédents dans d'autres langues.

Il ne s'agit expressions conditionnelles complexes sur une seule ligne, mais uniquement d'instructions conditionnelles sur une seule ligne.

De plus, il ne s'agit ne prendraient en charge que nil/non-nil, ou les booléens.

Pour ces opérateurs conditionnels, il peut même être approprié de restreindre à des variables uniques et de ne pas prendre en charge les expressions. Tout ce qui est plus complexe ou avec une clause else serait traité avec des constructions if ... standard.

Pourquoi ne pas inventer une roue et utiliser la forme connue de try..catch comme @mattn l' a

try {
    a := foo() // func foo(string, error)
    b := bar() // func bar(string, error)
} catch (err) {
    // handle error
}

Il semble qu'il n'y ait aucune raison de distinguer la source de l'erreur détectée, car si vous en avez vraiment besoin, vous pouvez toujours utiliser l'ancienne forme de if err != nil sans try..catch .

Aussi, je ne suis pas vraiment sûr de ça, mais peut-être ajouter la possibilité de "lancer" une erreur si elle n'est pas gérée ?

func foo() (string, error) {
    f := bar() // similar to if err != nil { return "", err }
}

func baz() string {
    // Compilation error.
    // bar's error must be handled because baz() does not return error.
    return bar()
}

@gobwas du point de vue de la lisibilité, il est très important de comprendre complètement le flux de contrôle. En regardant votre exemple, on ne sait pas quelle ligne peut provoquer un saut vers le bloc catch. C'est comme une déclaration goto cachée. Pas étonnant que les langages modernes essaient d'être explicites à ce sujet et exigent que le programmeur marque explicitement les endroits où le flux de contrôle pourrait diverger en raison d'une erreur. Un peu comme return ou goto mais avec une syntaxe beaucoup plus agréable.

@creker oui, je suis tout à fait d'accord avec toi. Je pensais au contrôle de flux dans l'exemple ci-dessus, mais je ne savais pas comment le faire sous une forme simple.

Peut-être quelque chose comme :

try {
    a ::= foo() // func foo(string, error)
    b ::= bar() // func bar(string, error)
} catch (err) {
    // handle error
}

Ou d'autres suggestions avant comme try a := foo() ..?

@gobwa

Lorsque j'atterris dans le bloc catch, comment savoir quelle fonction du bloc try a causé l'erreur ?

@urandom si vous avez besoin de le savoir, vous voudrez probablement faire if err != nil sans try..catch .

@robert-wallis: J'ai mentionné la déclaration de garde de Swift plus tôt dans le fil, mais la page est si grande que Github ne la charge plus par défaut. :-PI pense toujours que c'est une bonne idée, et en général, je soutiens la recherche d'exemples positifs/négatifs dans d'autres langages.

@pdk

autoriser l'utilisation d'éléments compatibles avec nil dans un contexte booléen, où nil équivaut à false, non-nil à true

Je vois cela conduire à beaucoup de bogues en utilisant le package flag où les gens écriront if myflag { ... } mais veulent écrire if *myflag { ... } et cela ne sera pas détecté par le compilateur.

try/catch n'est plus court que if/else lorsque vous essayez plusieurs choses dos à dos, ce qui est plus ou moins quelque chose que tout le monde s'accorde à dire est mauvais à cause des problèmes de flux de contrôle, etc.

FWIW, le try/catch de Swift résout au moins le problème visuel de ne pas savoir quelles instructions pourraient lancer :

do {
    let dragon = try summonDefaultDragon() 
    try dragon.breathFire()
} catch DragonError.dragonIsMissing {
    // ...
} catch DragonError.halatosis {
    // ...
}

@robert-wallis , vous avez un exemple :

file, err := os.Open("fails.txt")
guard err return &FooError{"Couldn't foo fails.txt", err}

guard os.Remove("fails.txt") return &FooError{"Couldn't remove fails.txt", err}

Lors de la première utilisation de guard , cela ressemble énormément à if err != nil { return &FooError{"Couldn't foo fails.txt", err}} , donc je ne sais pas si c'est une grosse victoire.

Lors de la deuxième utilisation, on ne sait pas immédiatement d'où vient le err . On dirait presque que c'est ce qui est revenu de os.Open , ce qui, je suppose, n'était pas votre intention ? Serait-ce plus précis ?

guard err = os.Remove("fails.txt") return &FooError{"Couldn't remove fails.txt", err}

Auquel cas, il semble que...

if err = os.Remove("fails.txt"); err != nil { return &FooError{"Couldn't remove fails.txt", err}}

Mais il a toujours moins d'encombrement visuel. if err = , ; err != nil { - même s'il s'agit d'une seule ligne, il se passe encore trop de choses pour une chose aussi simple

D'accord pour dire qu'il y a moins d'encombrement. Mais sensiblement moins pour justifier un ajout au langage ? Je ne suis pas sûr d'être d'accord là-dessus.

Je pense que la lisibilité des blocs try-catch dans Java/C#/... est très bonne car vous pouvez suivre la séquence "happy path" sans aucune interruption par la gestion des erreurs. L'inconvénient est que vous avez essentiellement un mécanisme de goto caché.

Dans Go, je commence à insérer des lignes vides après le gestionnaire d'erreurs pour rendre plus visible la poursuite de la logique du "chemin heureux". Donc sur cet échantillon de golang.org (9 lignes)

record := new(Record)
err := datastore.Get(c, key, record) 
if err != nil {
    return &appError{err, "Record not found", 404}
}
err := viewTemplate.Execute(w, record)
if err != nil {
    return &appError{err, "Can't display record", 500}
}

je fais souvent ça (11 lignes)

record := new(Record)

err := datastore.Get(c, key, record) 
if err != nil {
    return &appError{err, "Record not found", 404}
}

err := viewTemplate.Execute(w, record)
if err != nil {
    return &appError{err, "Can't display record", 500}
}

Revenons maintenant à la proposition, car j'ai déjà posté quelque chose comme ça serait bien (3 lignes)

record := new(Record)
err := datastore.Get(c, key, record) return? &appError{err, "Record not found", 404}
err := viewTemplate.Execute(w, record) return? &appError{err, "Can't display record", 500}

Maintenant, je vois clairement le chemin heureux. Mes yeux sont toujours conscients que sur le côté droit, il y a un code pour la gestion des erreurs, mais je n'ai besoin de "l'analyser" que lorsque cela est vraiment nécessaire.

Une question à tous sur le côté : ce code doit-il compiler ?

func foobar() error {
    return fmt.Errorf("Some error")
}
func main() {
    foobar()
}

À mon humble avis, l'utilisateur devrait être obligé de dire qu'il ignore intentionnellement l'erreur avec :

func main() {
    _ := foobar()
}

Ajout d'un mini rapport d'expérience lié au point 3 de la publication originale de renvoyez l'erreur avec des informations contextuelles supplémentaires .

Lors du développement d'une bibliothèque flac pour Go, nous voulions ajouter des informations contextuelles aux erreurs à l'aide du package @davecheney pkg/errors (https://github.com/mewkiz/flac/issues/22). Plus précisément, nous encapsulons les erreurs renvoyées à l'aide d'

Étant donné que l'erreur est annotée, elle doit créer un nouveau type sous-jacent pour stocker ces informations supplémentaires, dans le cas d'errors.WithStack, le type est error.withStack .

type withStack struct {
    error
    *stack
}

Maintenant, pour récupérer l'erreur d'origine, la convention est d'utiliser error.Cause . Cela vous permet de comparer l'erreur d'origine avec par exemple io.EOF .

Un utilisateur de la bibliothèque peut alors écrire quelque chose du genre https://github.com/mewkiz/flac/blob/0884ed715ef801ce2ce0c262d1e674fdda6c3d94/cmd/flac2wav/flac2wav.go#L78 en utilisant errors.Cause pour vérifier l'erreur d'origine valeur:

frame, err := stream.ParseNext()
if err != nil {
    if errors.Cause(err) == io.EOF {
        break
    }
    return errors.WithStack(err)
}

Cela fonctionne bien dans presque tous les cas.

Lors de la refactorisation de notre gestion des erreurs pour utiliser de manière cohérente pkg/errors pour des informations contextuelles supplémentaires, nous avons cependant rencontré un problème assez sérieux. Pour valider le remplissage par zéro, nous avons implémenté un io.Reader qui vérifie simplement si les octets lus sont nuls et signale une erreur dans le cas contraire. Le problème est qu'après avoir effectué une refactorisation automatique pour ajouter des informations contextuelles à nos erreurs, nos cas de test ont soudainement commencé à échouer .

Le problème était que le type sous-jacent de l'erreur renvoyée par zeros.Read est maintenant error.withStack, plutôt que io.EOF. Ainsi, par la suite, nous avons causé des problèmes lorsque nous avons utilisé ce lecteur en combinaison avec io.Copy , qui vérifie spécifiquement io.EOF et ne sait pas utiliser errors.Cause pour "déballer" une erreur annotée avec information contextuelle. Comme nous ne pouvons pas mettre à jour la bibliothèque standard, la solution consistait à renvoyer l'erreur sans informations annotées (https://github.com/mewkiz/flac/commit/6805a34d854d57b12f72fd74304ac296fd0c07be).

Alors que perdre les informations annotées pour les interfaces qui renvoient des valeurs concrètes est une perte, il est possible de vivre avec.

La conclusion de notre expérience a été que nous avons eu de la chance, car nos cas de test l'ont détecté. Le compilateur n'a produit aucune erreur, puisque le type zeros implémente toujours l'interface io.Reader . Nous ne pensions pas non plus que nous rencontrerions un problème, car l'annotation d'erreur ajoutée était une réécriture générée par la machine, il suffit d'ajouter des informations contextuelles aux erreurs ne devrait pas affecter le comportement du programme dans un état normal.

Mais il l'a fait, et pour cette raison, nous souhaitons contribuer notre rapport d'expérience pour examen ; lorsque l'on réfléchit à la manière d'intégrer l'ajout d'informations contextuelles dans la gestion des erreurs pour Go 2, de telle sorte que la comparaison des erreurs (telle qu'utilisée dans les contrats d'interface) se maintienne de manière transparente.

Gentiment,
Robin

@mewmew , gardons ce problème sur les aspects du flux de contrôle de la gestion des erreurs. La meilleure façon d'envelopper et de déballer les erreurs doit être discutée ailleurs, car il est largement orthogonal de contrôler le flux.

Je ne connais pas votre base de code et je me rends compte que vous avez dit qu'il s'agissait d'une refactorisation automatisée, mais pourquoi avez-vous eu besoin d'inclure des informations contextuelles avec EOF ? Bien qu'il soit traité comme une erreur par le système de types, EOF est vraiment plus une valeur de signal, pas une erreur réelle. Dans une implémentation io.Reader en particulier, c'est une valeur attendue la plupart du temps. Une meilleure solution n'aurait-elle pas été de n'encapsuler l'erreur que si ce n'était pas io.EOF ?

Oui, je propose qu'on laisse les choses comme elles sont. J'avais l'impression que le système d'erreur de Go a été délibérément conçu de cette façon pour décourager les développeurs de remonter des erreurs dans la pile d'appels. Ces erreurs sont censées être résolues là où elles se produisent et pour savoir quand il est plus approprié d'utiliser la panique lorsque vous ne le pouvez pas.

Je veux dire, est-ce que try-catch-throw n'est pas essentiellement le même comportement de panic () et de recover () de toute façon?

Soupir, si nous allons vraiment commencer à essayer de suivre cette voie. Pourquoi ne pouvons-nous pas simplement faire quelque chose comme

_, ? := foo()
x?, err? := bar()

ou peut-être même quelque chose comme

_, err := foo(); return err?
x, err := bar(); return x? || err?
x, y, err := baz(); return (x? && y?) || err?

où le ? devient un alias abrégé pour le if var != nil{ return var }.

Nous pouvons même définir une autre interface intégrée spéciale qui est satisfaite par la méthode

func ?() bool //looks funky but avoids breakage.

que nous pouvons utiliser pour remplacer essentiellement le comportement par défaut du nouvel opérateur conditionnel amélioré.

@mortdeus

Je pense que je suis d'accord.
Si le problème est d'avoir une bonne façon de présenter le chemin heureux, un plugin pour un IDE pourrait plier/déplier chaque instance de if err != nil { return [...] } avec un raccourci ?

J'ai l'impression que chaque partie est maintenant importante. err != nil est important. return ... est important.
C'est un peu compliqué à écrire, mais il faut l'écrire. Et cela ralentit-il vraiment les gens ? Ce qui prend du temps, c'est de penser à l'erreur et à ce qu'il faut retourner, pas de l'écrire.

Je serais bien plus intéressé par une proposition permettant de limiter la portée de la variable err .

Je pense que mon idée conditionnelle est la meilleure façon de résoudre ce problème. Je viens de penser à quelques autres choses qui rendraient cette fonctionnalité suffisamment digne d'être incluse dans Go. Je vais écrire mon idée dans une proposition séparée.

Je ne vois pas comment cela pourrait fonctionner :

x, y, erreur := baz(); retour ( x? && y? ) || err?

où le ? devient un alias abrégé pour le if var == nil{ return var }.

x, y, erreur := baz(); retour ( if x == nil{ return x} && if y== nil{ return y} ) || if err == nil{ return err}

x, y, erreur := baz(); renvoie (x? && y?) || se tromper?

devient

x, y, erreur := baz();
if ((x != nil && y !=nil) || err !=nil)){
retourner x,y, erreur
}

quand tu vois x ? && y ? || se tromper? vous devriez penser « x et y sont-ils valides ? Qu'en est-il de l'erreur ? »

sinon, la fonction de retour ne s'exécute pas. Je viens d'écrire une nouvelle proposition sur cette idée qui pousse l'idée un peu plus loin avec un nouveau type d'interface intégré spécial

Je suggère à Go d'ajouter la gestion des erreurs par défaut dans Go version 2.

Si l'utilisateur ne gère pas l'erreur, le compilateur renvoie err s'il n'est pas nul, donc si l'utilisateur écrit :

func Func() error {
    func1()
    func2()
    return nil
}

func func1() error {
    ...
}

func func2() error {
    ...
}

compilez-le transformez-le en :

func Func() error {
    err := func1()
    if err != nil {
        return err
    }

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

    return nil
}

func func1() error {
    ...
}

func func2() error {
    ...
}

Si l'utilisateur gère l'erreur ou l'ignore en utilisant _, le compilateur ne fera rien :

_ = func1()

ou

err := func1()

pour plusieurs valeurs de retour, c'est similaire :

func Func() (*YYY, error) {
    ch, x := func1()
    return yyy, nil
}

func func1() (chan int, *XXX, error) {
    ...
}

le compilateur se transformera en :

func Func() (*YYY, error) {
    ch, x, err := func1()
    if err != nil {
        return nil, err
    }

    return yyy, nil
}

func func1() (chan int, *XXX, error) {
    ...
}

Si la signature de Func() ne renvoie pas d'erreur mais appelle des fonctions qui renvoient une erreur, le compilateur signalera l'erreur : "Veuillez gérer votre erreur dans Func()"
Ensuite, l'utilisateur peut simplement enregistrer l'erreur dans Func ()

Et si l'utilisateur veut encapsuler des informations dans l'erreur :

func Func() (*YYY, error) {
    ch, x := func1() ? Wrap(err, "xxxxx", ch, "abc", ...)
    return yyy, nil
}

ou

func Func() (*YYY, error) {
    ch, x := func1() ? errors.New("another error")
    return yyy, nil
}

L'avantage est,

  1. le programme échoue simplement au point où l'erreur se produit, l'utilisateur ne peut pas ignorer l'erreur implicitement.
  2. peut notamment réduire les lignes de code.

ce n'est pas si facile car Go peut avoir plusieurs valeurs de retour et le langage ne doit pas attribuer ce qui équivaut essentiellement à des valeurs par défaut pour les arguments de retour sans que le développeur soit explicitement au courant de ce qui se passe.

Je pense que mettre la gestion des erreurs dans la syntaxe d'affectation ne résout pas la racine du problème, à savoir "la gestion des erreurs est répétitive".

L'utilisation de if (err != nil) { return nil } (ou similaire) après plusieurs lignes de code (là où cela a du sens) va à l'encontre du principe DRY (ne vous répétez pas). Je crois que c'est pourquoi nous n'aimons pas cela.

Il y a aussi des problèmes avec try ... catch . Vous n'avez pas besoin de gérer explicitement l'erreur dans la même fonction où elle se produit. Je crois que c'est une raison notable pour laquelle nous n'aimons pas try...catch .

Je ne crois pas que ceux-ci s'excluent mutuellement ; nous pouvons avoir une sorte de try...catch sans throws .

Une autre chose que je n'aime pas personnellement à propos de try...catch est la nécessité arbitraire du mot-clé try . Il n'y a aucune raison pour que vous ne puissiez pas catch après un limiteur de portée, en ce qui concerne une grammaire de travail. (quelqu'un l'appelle si je me trompe)

C'est ce que je propose :

  • en utilisant ? comme espace réservé pour une erreur renvoyée, où _ serait utilisé pour l'ignorer
  • au lieu de catch comme dans mon exemple ci-dessous, error? pourrait être utilisé à la place pour une compatibilité descendante totale

^ Si mon hypothèse selon laquelle ils sont rétrocompatibles est incorrecte, veuillez le signaler.

func example() {
    {
        // The following line will invoke the catch block
        data, ? := foopkg.buyIt()
        // The next two lines handle an error differently
        otherData, err := foopkg.useIt()
        if err != nil {
            // Here we eliminated deeper indentation
            otherData, ? = foopkg.breakIt()
        }
        if data == "" || otherData == "" {
        }
    } catch (err) {
        return errors.Label("fix it", err)
        // Aside: why not add Label() to the error package?
    }
}

J'ai pensé à un argument contre cela : si vous l'écrivez de cette façon, changer ce bloc catch pourrait avoir des effets involontaires sur le code dans des portées plus profondes. C'est le même problème que nous avons avec try...catch .

Je pense que si vous ne pouvez le faire que dans le cadre d'une seule fonction, le risque est gérable - peut-être le même que le risque actuel d'oublier de modifier une ligne de code de gestion des erreurs lorsque vous avez l'intention d'en modifier plusieurs. Je vois cela comme la même différence entre les conséquences de la réutilisation du code et les conséquences de ne pas suivre DRY (c'est-à-dire pas de déjeuner gratuit, comme on dit)

Edit : j'ai oublié de spécifier un comportement important pour mon exemple. Dans le cas où ? est utilisé dans une portée sans catch , je pense que cela devrait être une erreur du compilateur (plutôt que de déclencher une panique, ce qui était certes la première chose à laquelle j'ai pensé)

Edit 2: Idée folle: peut-être que le bloc catch n'affecterait tout simplement pas le flux de contrôle... ce serait littéralement comme copier et coller le code à l'intérieur de catch { ... } à la ligne après l'erreur est ? ed (enfin pas tout à fait - il aurait toujours sa propre portée). Cela semble étrange car aucun d'entre nous n'y est habitué, donc catch ne devrait certainement pas être le mot-clé si c'est fait de cette façon, mais sinon... pourquoi pas ?

@mewmew , gardons ce problème sur les aspects du flux de contrôle de la gestion des erreurs. La meilleure façon d'envelopper et de déballer les erreurs doit être discutée ailleurs, car il est largement orthogonal de contrôler le flux.

Ok, gardons ce fil pour contrôler le flux. Je l'ai ajouté simplement parce qu'il s'agissait d'un problème lié à l'utilisation concrète du point 3 renvoie l'erreur avec des informations contextuelles supplémentaires .

@jba Connaissez-vous un problème spécifiquement dédié à l'emballage/déballage des informations contextuelles pour les erreurs ?

Je ne connais pas votre base de code et je me rends compte que vous avez dit qu'il s'agissait d'une refactorisation automatisée, mais pourquoi avez-vous eu besoin d'inclure des informations contextuelles avec EOF ? Bien qu'il soit traité comme une erreur par le système de types, EOF est vraiment plus une valeur de signal, pas une erreur réelle. Dans une implémentation io.Reader en particulier, c'est une valeur attendue la plupart du temps. Une meilleure solution n'aurait-elle pas été de n'encapsuler l'erreur que s'il ne s'agissait pas de io.EOF ?

@DeedleFake Je peux élaborer un peu, mais pour rester dans le sujet, je le ferai dans le numéro susmentionné consacré à l'emballage/déballage des informations contextuelles pour les erreurs.

Plus je lis toutes les propositions (y compris la mienne) moins je pense qu'on a vraiment un problème de gestion des erreurs au go.

Ce que je voudrais, c'est une certaine application pour ne pas ignorer accidentellement une valeur de retour d'erreur mais appliquer au moins
_ := returnsError()

Je sais qu'il existe des outils pour trouver ces problèmes, mais un support de premier niveau du langage pourrait détecter certains bogues. Ne pas gérer une erreur du tout, c'est comme avoir une variable inutilisée pour moi - ce qui est déjà une erreur. Cela aiderait également à la refactorisation, lorsque vous introduisez un type de retour d'erreur dans une fonction, car vous êtes obligé de le gérer à tous les endroits.

Le principal problème que la plupart des gens essaient de résoudre ici semble être la "quantité de frappe" ou le "nombre de lignes". Je serais d'accord avec toute syntaxe qui réduit le nombre de lignes, mais c'est surtout un problème de gofmt. Autorisez simplement les "étendues à une seule ligne" en ligne et nous sommes bons.

Une autre suggestion pour économiser de la frappe est la vérification implicite de nil comme avec les booléens :

err := returnsError()
if err { return err }

ou même

if err := returnsError(); err { return err }

Cela fonctionnerait avec tous les types de pointeurs de cause.

Mon sentiment est que tout ce qui réduit l'appel de fonction + la gestion des erreurs en une seule ligne conduira à un code moins lisible et à une syntaxe plus complexe.

code moins lisible et syntaxe plus complexe.

Nous avons déjà du code moins lisible à cause de la gestion des erreurs verbeuse. L'ajout de l'astuce de l'API Scanner déjà mentionnée, censée masquer cette verbosité, aggrave encore les choses. L'ajout d'une syntaxe plus complexe pourrait aider à la lisibilité, à quoi sert le sucre syntaxique en fin de compte. Sinon, cette discussion ne sert à rien. Le schéma consistant à générer une erreur et à renvoyer une valeur zéro pour tout le reste est suffisamment courant pour justifier un changement de langue, à mon avis.

Le modèle consistant à générer une erreur et à renvoyer une valeur zéro pour tout

19642 rendrait cela plus facile.


Merci également @mewmew pour le rapport d'expérience. Il est certainement lié à ce fil, dans la mesure où il concerne les dangers de certains types de conceptions de gestion des erreurs. J'aimerais en voir plus.

Je n'ai pas l'impression d'avoir très bien expliqué mon idée, alors j'ai créé un résumé (et révisé bon nombre des lacunes que je viens de remarquer)

https://gist.github.com/KernelDeimos/384aabd36e1789efe8cbce3c17ffa390

Il y a plus d'une idée dans cette idée, alors j'espère qu'elles pourront être discutées séparément les unes des autres

Laissant de côté pour un instant l'idée que la proposition ici doit être explicitement sur la gestion des erreurs, et si Go introduisait quelque chose comme une instruction collect ?

Une instruction collect serait de la forme collect [IDENT] [BLOCK STMT] , où ident doit être une variable dans la portée d'un type nil -able. Dans une instruction collect , une variable spéciale _! est disponible comme alias pour la variable à laquelle la collecte est effectuée. _! ne peut être utilisé que comme une affectation, comme _ . Chaque fois que _! est affecté à, une vérification implicite nil est effectuée, et si _! n'est pas nul, le bloc cesse son exécution et continue avec le reste du code.

Théoriquement, cela ressemblerait à quelque chose comme ceci:

func TryComplexOperation() (*Result, error) {
    var result *Result
    var err error

    collect err {
        intermediate1, _! := Step1()
        intermediate2, _! := Step2(intermediate1, "something")
        // assign to result from the outer scope
        result, _! = Step3(intermediate2, 12)
    }
    return result, err
}

ce qui équivaut à

func TryComplexOperation() (*Result, error) {
    var result *Result
    var err error

    {
        var intermediate1 SomeType
        intermediate1, err = Step1()
        if err != nil { goto collectEnd }

        var intermediate2 SomeOtherType
        intermediate2, err = Step2(intermediate1, "something")
        if err != nil { goto collectEnd }

        result, err = Step3(intermediate2, 12)
        // if err != nil { goto collectEnd }, but since we're at the end already we can omit this
    }

collectEnd:
    return result, err
}

Quelques autres choses intéressantes qu'une fonctionnalité de syntaxe comme celle-ci permettrait :

// try several approaches for acquiring a value
func GetSomething() (s *Something) {
    collect s {
        _! = fetchOrNil1()
        _! = fetchOrNil2()
        _! = new(Something)
    }
    return s
}

Nouvelles fonctionnalités de syntaxe requises :

  1. mot-clé collect
  2. identifiant spécial _! (j'ai joué avec ça dans l'analyseur, ce n'est pas difficile de faire cette correspondance en tant qu'identifiant sans casser quoi que ce soit d'autre)

La raison pour laquelle je suggère quelque chose comme ça est que l'argument "la gestion des erreurs est trop répétitive" peut être réduit à "les vérifications nulles sont trop répétitives". Go dispose déjà de nombreuses fonctionnalités de gestion des erreurs qui fonctionnent telles quelles. Vous pouvez ignorer une erreur avec _ (ou simplement ne pas capturer les valeurs de retour), vous pouvez renvoyer une erreur non modifiée avec if err != nil { return err } , ou ajouter un contexte et renvoyer avec if err != nil { return wrap(err) } . Aucune de ces méthodes, à elle seule, n'est trop répétitive. La répétitivité ( évidemment ) vient du fait d'avoir à répéter ces instructions ou des instructions de syntaxe similaires dans tout le code. Je pense qu'introduire un moyen d'exécuter des instructions jusqu'à ce qu'une valeur non nulle soit rencontrée est un bon moyen de garder la même gestion des erreurs, mais de réduire la quantité de passe-partout nécessaire pour le faire.

  • Bon support pour 1) ignorer une erreur ; 3) encapsuler une erreur avec un contexte supplémentaire.

vérifier, car il reste le même (la plupart du temps)

  • Alors que le code de gestion des erreurs doit être clair, il ne doit pas dominer la fonction.

vérifier, car le code de gestion des erreurs peut désormais aller à un seul endroit si nécessaire, tandis que le cœur de la fonction peut se produire de manière linéairement lisible

  • Le code Go 1 existant doit continuer à fonctionner, ou à tout le moins, il doit être possible de traduire mécaniquement Go 1 vers la nouvelle approche avec une fiabilité totale.

vérifier, c'est un ajout et non une modification

  • La nouvelle approche devrait encourager les programmeurs à gérer correctement les erreurs.

check, je pense, puisque les mécanismes de gestion des erreurs ne sont pas différents - nous aurions juste une syntaxe pour "collecter" la première valeur non nulle à partir d'une série d'exécutions et d'affectations, qui peut être utilisée pour limiter le nombre de endroits où nous devons écrire notre code de gestion des erreurs dans une fonction

  • Toute nouvelle approche doit être plus courte et/ou moins répétitive que l'approche actuelle, tout en restant claire.

Je ne suis pas sûr que cela s'applique ici, car la fonctionnalité suggérée s'applique à plus que la simple gestion des erreurs. Je pense que cela peut raccourcir et clarifier le code qui peut générer des erreurs, sans encombrer les contrôles nuls et les retours anticipés

  • La langue fonctionne aujourd'hui, et chaque changement a un coût. Cela ne devrait pas être juste un lavage, cela devrait être clairement mieux.

D'accord, et il semble donc qu'un changement dont la portée s'étend au-delà de la simple gestion des erreurs puisse être approprié. Je crois que le problème sous-jacent est que les vérifications de nil deviennent répétitives et verbeuses, et il se trouve que error est un type nil -able.

@KernelDeimos Nous venons essentiellement de x, ? := doSomething() ne fonctionnait pas vraiment bien dans la pratique. Bien que ce soit intéressant de voir que je ne suis pas la seule personne à penser à ajouter le ? opérateur dans la langue d'une manière intéressante.

https://github.com/golang/go/issues/25582

N'est-ce pas fondamentalement juste un piège ?

Voici un spitball :

func NewClient(...) (*Client, error) {
    trap(err error) {
        return nil, err
    }

    listener, err? := net.Listen("tcp4", listenAddr)
    trap(_ error) {
        listener.Close()
    }

    conn, err? := ConnectionManager{}.connect(server, tlsConfig)
    trap(_ error) {
        conn.Close()
    }

    if forwardPort == 0 {
        env, err := environment.GetRuntimeEnvironment()
        if err != nil {
            log.Printf("not forwarding because: %v", err)
        } else {
            forwardPort, err = env.PortToForward()
            if err != nil {
                log.Printf("env couldn't provide forward port: %v", err)
            }
        }
    }
    var forwardOut *forwarding.ForwardOut
    if forwardPort != 0 {
        u, _ := url.Parse(fmt.Sprintf("http://127.0.0.1:%d", forwardPort))
        forwardOut = forwarding.NewOut(u)
    }

    client := &Client{...}

    toServer := communicationProtocol.Wrap(conn)
    toServer.Send(&client.serverConfig)?

    toServer.Send(&stprotocol.ClientProtocolAck{ClientVersion: Version})?

    session, err? := communicationProtocol.FinalProtocol(conn)
    client.session = session

    return client, nil
}

59 lignes → 44

trap signifie "exécuter ce code dans l'ordre de la pile si une variable marquée avec ? du type spécifié n'est pas la valeur zéro". C'est comme defer mais cela peut affecter le flux de contrôle.

J'aime un peu l'idée trap , mais la syntaxe me dérange un peu. Et si c'était une sorte de déclaration ? Par exemple, trap err error {} déclare un trap appelé err de type error qui, lorsqu'il est affecté, exécute le code donné. Le code n'a même pas besoin de revenir ; c'est juste autorisé à le faire. Cela brise également la dépendance au fait que nil soit spécial.

Edit : Développer et donner un exemple maintenant que je ne suis pas au téléphone.

func Example(r io.Reader) error {
  trap err error {
    if err != nil {
      return err
    }
  }

  n, err? := io.Copy(ioutil.Discard, r)
  fmt.Printf("Read %v bytes.\n", n)
}

Essentiellement, un trap fonctionne comme un var , sauf que chaque fois qu'il est affecté à avec l'opérateur ? qui lui est attaché, le bloc de code est exécuté. L'opérateur ? empêche également d'être masqué lorsqu'il est utilisé avec := . Redéclarer un trap dans la même portée, contrairement à un var , est autorisé, mais il doit être du même type que celui existant ; cela vous permet de changer le bloc de code associé. Parce que le bloc en cours d'exécution ne revient pas nécessairement, il vous permet également d'avoir des chemins séparés pour des choses spécifiques, comme vérifier si err == io.EOF .

Ce que j'aime dans cette approche, c'est qu'elle semble similaire à l'exemple errWriter de Errors are Values , mais dans une configuration un peu plus générique qui ne nécessite pas de déclaration d'un nouveau type.

@carlmjohnson à qui
Quoi qu'il en soit, ce concept trap semble être juste une façon différente d'écrire une déclaration defer , non ? Le code, tel qu'il est écrit, serait essentiellement le même si vous aviez panic sur une erreur non nulle, puis utilisiez une fermeture différée pour définir des valeurs de retour nommées et effectuer un nettoyage. Je pense que cela rencontre les mêmes problèmes que ma proposition précédente d'utiliser _! pour paniquer automatiquement, car cela place une méthode de gestion des erreurs sur une autre. FWIW J'ai également senti que le code, tel qu'il était écrit, était beaucoup plus difficile à raisonner que l'original. Ce concept de trap pourrait-il être imité avec go aujourd'hui, même s'il est moins clair que d'avoir une syntaxe pour cela ? J'ai l'impression que cela pourrait et ce serait if err != nil { panic (err) } et defer pour capturer et gérer cela.

Cela semble similaire au concept du bloc collect j'ai suggéré ci-dessus, qui me semble personnellement fournit un moyen plus propre d'exprimer la même idée ("si cette valeur n'est pas nulle, je veux la capturer et faire quelque chose avec ça"). Go aime être linéaire et explicite. trap ressemble à une nouvelle syntaxe pour panic / defer mais avec un flux de contrôle moins clair.

@mccolljr , cela semblait être une réponse pour moi . Je déduis de votre message que vous n'avez pas vu ma proposition (maintenant dans les "éléments cachés", bien que pas si loin là-haut), car j'ai en fait utilisé une déclaration de report dans ma proposition, étendue avec un recover - comme fonction pour la gestion des erreurs.

J'observais également que la réécriture du "trap" a supprimé de nombreuses fonctionnalités de ma proposition (des erreurs _très_ différentes apparaissent), et en outre, je ne sais pas comment éliminer la gestion des erreurs avec les instructions de piège. Une grande partie de cette réduction par rapport à ma proposition se présente sous la forme d'une suppression de l'exactitude de la gestion des erreurs et, je pense, d'un retour au fait de rendre plus facile de simplement renvoyer les erreurs directement que de faire autre chose.

La possibilité de continuer le flux est autorisée par l'exemple modifié trap que j'ai donné ci-dessus . Je l'ai édité plus tard, donc je ne sais pas si vous l'avez vu ou non. C'est très similaire à un collect , mais je pense que cela donne un peu plus de contrôle dessus. Selon la façon dont les règles de cadrage fonctionnaient, cela pourrait être un peu spaghetti, mais je pense qu'il serait possible de trouver un bon équilibre.

@thejerf Ah, c'est plus logique. Je ne savais pas que c'était une réponse à ta proposition. Cependant, je ne sais pas quelle serait la différence entre erroring() et recover() , mis à part le fait que recover répond à panic . Il semble que nous fassions simplement implicitement une forme de panique lorsqu'une erreur doit être renvoyée. Le report est également une opération quelque peu coûteuse, donc je ne sais pas trop ce que je pense de l'utiliser dans toutes les fonctions qui pourraient générer des erreurs.

@DeedleFake Même chose pour trap , car la façon dont je le vois trap est essentiellement une macro qui insère du code lorsque l'opérateur ? est utilisé, ce qui présente son propre ensemble de préoccupations et considérations, ou il est implémenté en tant que goto ... ce qui, que se passe-t-il si l'utilisateur ne revient pas dans le bloc trap , ou s'il s'agit simplement d'un defer syntaxiquement différent. De plus, que se passe-t-il si je déclare plusieurs blocs trap dans une fonction ? Est-ce autorisé? Si oui, lequel est exécuté ? Cela ajoute de la complexité à la mise en œuvre. Go aime être opiniâtre, et j'aime ça à ce sujet. Je pense que collect ou une construction linéaire similaire est plus alignée sur l'idéologie de Go que trap , qui, comme cela m'a été signalé après ma première proposition, semble être un try-catch construire en costume.

que faire si l'utilisateur ne revient pas dans le bloc piège

Si le trap ne renvoie pas ou ne modifie pas le flux de contrôle ( goto , continue , break , etc.), le flux de contrôle retourne là où le code bloc a été "appelé" à partir de. Le bloc lui-même fonctionnerait de la même manière que l'appel d'une fermeture, à l'exception du fait qu'il a accès aux mécanismes de contrôle de flux. Les mécanismes fonctionneraient à l'endroit où le bloc est déclaré, pas à l'endroit d'où il est appelé, donc

for {
  trap err error {
    break
  }

  err? = errors.New("Example")
}

travaillerait.

De plus, que se passe-t-il si je déclare plusieurs blocs trap dans une fonction ? Est-ce autorisé? Si oui, lequel est exécuté ?

Oui, c'est autorisé. Les blocs sont nommés par le piège, il est donc assez simple de déterminer lequel doit être appelé. Par exemple, dans

trap err error {
  // Block 1.
}

trap n int {
  // Block 2.
}

n? = 3

le bloc 2 est appelé. La grande question dans ce cas serait probablement ce qui se passe dans le cas de n?, err? = 3, errors.New("Example") , ce qui nécessiterait probablement que l'ordre des affectations soit spécifié, comme indiqué dans #25609.

Je pense que la collection ou une construction linéaire similaire est plus alignée sur l'idéologie de Go que sur le piège, qui, comme cela m'a été signalé après ma première proposition, semble être une construction d'essai en costume.

Je pense que collect et trap sont essentiellement des try-catch à l'envers. Un try-catch standard est une politique d'échec par défaut qui vous oblige à vérifier ou elle explose. Il s'agit d'un système de réussite par défaut qui vous permet essentiellement de spécifier un chemin d'échec.

Une chose qui complique le tout est le fait que les erreurs ne sont pas intrinsèquement traitées comme un échec, et certaines erreurs, telles que io.EOF , ne spécifient pas vraiment l'échec du tout. Je pense que c'est pourquoi les systèmes qui ne sont pas spécifiquement liés aux erreurs, tels que collect ou trap , sont la voie à suivre.

"Ah, cela a plus de sens. Je ne savais pas que c'était une réponse à votre proposition. Cependant, je ne sais pas quelle serait la différence entre erroring() et recover(), mis à part le fait que recover répond à panique."

Ne pas avoir beaucoup de différence est le point. J'essaie de minimiser le nombre de nouveaux concepts créés tout en en tirant le plus de puissance possible. Je considère que s'appuyer sur des fonctionnalités existantes est une fonctionnalité, pas un bogue.

L'un des points de ma proposition est d'explorer au-delà de « et si nous réparions ce morceau récurrent de trois lignes où nous return err et le remplaçons par un ? » pour « comment cela affecte-t-il le reste du langage ? Quels nouveaux modèles permet-il ? Quelles nouvelles « meilleures pratiques » crée-t-il ? Quelles anciennes « meilleures pratiques » cessent d'être des meilleures pratiques ? » Je ne dis pas que j'ai fini ce travail. Et même s'il est jugé que l'idée a en fait trop de puissance au goût de Go (comme Go n'est pas un langage qui maximise la puissance, et même avec le choix de conception de le limiter à taper error c'est toujours probablement le plus puissant proposition faite dans ce fil, que je veux dire pleinement dans le bon et le mauvais sens de "puissant"), je pense que nous pourrions envisager d'explorer les questions de ce que les nouvelles constructions feront aux programmes dans leur ensemble, plutôt que ce qu'elles fera à sept fonctions d'exemple de ligne, c'est pourquoi j'ai essayé d'amener au moins les exemples jusqu'à ~ 50-100 lignes de la plage "code réel". Tout se ressemble sur 5 lignes, ce qui inclut la gestion des erreurs Go 1.0, ce qui explique peut-être en partie la raison pour laquelle nous savons tous d'après nos propres expériences qu'il y a un vrai problème ici, mais la conversation tourne en quelque sorte en rond si nous parlons de à trop petite échelle jusqu'à ce que certaines personnes commencent à se convaincre qu'il n'y a peut-être pas de problème après tout. (Faites confiance à vos vraies expériences de codage, pas aux exemples de 5 lignes !)

"Il semble que nous fassions simplement implicitement une forme de panique lorsqu'une erreur doit être renvoyée."

Ce n'est pas implicite. C'est explicite. Vous utilisez l'opérateur pop quand il fait ce que vous voulez. Quand il ne fait pas ce que vous voulez, vous ne l'utilisez pas. Ce qu'il fait est assez simple pour être capturé en une seule phrase simple, bien que la spécification prenne probablement un paragraphe complet car c'est ainsi que de telles choses fonctionnent. Il n'y a pas d'implicite. De plus ce n'est pas une panique car cela ne déroule qu'un niveau de la pile, exactement comme un retour ; c'est autant une panique qu'un retour, ce qui n'est pas du tout.

Je m'en fous aussi si vous épelez pop comme ? ou peu importe. Personnellement, je pense qu'un mot ressemble un peu plus à Go car Go n'est actuellement pas un langage riche en symboles, mais je ne peux pas nier qu'un symbole a l'avantage de n'entrer en conflit avec aucun code source existant. Je m'intéresse à la sémantique et à ce que nous pouvons en tirer et quels comportements la nouvelle sémantique offre aux programmeurs nouveaux et expérimentés plus que l'orthographe.

"Le report est également une opération quelque peu coûteuse, donc je ne sais pas ce que je pense de l'utiliser dans toutes les fonctions qui pourraient générer des erreurs."

Je l'ai déjà reconnu. Bien que je suggère qu'en général, ce n'est pas si cher et je ne me sens pas trop mal de dire qu'à des fins d'optimisation, si vous avez une fonction chaude, écrivez-la de la manière actuelle. Ce n'est explicitement pas mon objectif d'essayer de modifier 100% de toutes les fonctions de gestion des erreurs, mais de rendre 80% d'entre elles beaucoup plus simples et plus correctes et de laisser les 20% de cas (probablement plus comme 98/2, honnêtement) rester tels qu'ils sont sont. La grande partie du code Go n'est pas sensible à l'utilisation de defer, ce qui est, après tout, la raison defer laquelle

En fait, vous pouvez modifier trivialement la proposition pour ne pas utiliser defer et utiliser un mot clé comme trap comme déclaration qui n'est exécutée qu'une seule fois, quel que soit l'endroit où elle apparaît, plutôt que la façon dont defer est en fait une déclaration qui pousse un gestionnaire sur la pile de fonctions différées. J'ai délibérément choisi de réutiliser defer pour éviter d'ajouter de nouveaux concepts à la langue... même comprendre les pièges qui pourraient résulter de defers in loops mordant les gens de façon inattendue. Mais c'est toujours le seul concept defer à comprendre.

Juste pour préciser que l'ajout d'un nouveau mot-clé à la langue est un changement décisif.

package main

import (
    "fmt"
)

func return(i int)int{
    return i
}

func main() {
    return(1)
}

résulte en

prog.go:7:6: syntax error: unexpected select, expecting name or (

Ce qui signifie que si nous essayons d'ajouter un try , trap , assert , n'importe quel mot-clé dans le langage, nous courons le risque de casser une tonne de code. Code qui peut être maintenu plus longtemps.

C'est pourquoi j'ai initialement proposé d'ajouter un opérateur spécial ? go qui peut être appliqué aux variables dans le contexte des instructions. Le caractère ? partir de maintenant est désigné comme caractère illégal pour les noms de variables. Ce qui signifie qu'il n'est actuellement utilisé dans aucun code Go actuel et que nous pouvons donc l'introduire sans subir de modifications importantes.

Maintenant, le problème de son utilisation dans le côté gauche d'une affectation est qu'il ne prend pas en compte le fait que Go autorise plusieurs arguments de retour.

Par exemple, considérons cette fonction

func getCoord() x int, y int, z int, err error{
    x, err = getX()
    if err != nil{
        return 
    }

    y, err = getY()
    if err != nil{
        return 
    }

    z, err = getZ()
        if err != nil{
        return 
    }
    return
}

si on utilise ? ou essayez dans les lhs de l'affectation de vous débarrasser des blocs if err != nil, supposons-nous automatiquement que les erreurs signifient que toutes les autres valeurs sont désormais des ordures ? Et si on faisait comme ça

func GetCoord() (x, y, z int, err error) {
    err = try GetX(&x) // or err? = GetX(&x) 
    err = try GetY(&y) // or err? = GetY(&x) 
    err = try GetZ(&z) // or err? = GetZ(&x) 
}

quelles hypothèses faisons-nous ici? Qu'il ne devrait pas être dangereux de simplement supposer qu'il est acceptable de jeter la valeur ? que se passe-t-il si l'erreur est plutôt un avertissement et que la valeur x est correcte ? Et si la seule fonction qui lève l'erreur était l'appel à GetZ() et que les valeurs x, y étaient en fait bonnes ? Avons-nous l'intention de les retourner. Et si nous n'utilisons pas d'arguments de retour nommés ? Que se passe-t-il si les arguments de retour sont des types de référence comme une carte ou un canal, devons-nous supposer qu'il est sûr de renvoyer nil à l'appelant ?

TLDR ; ajouter ? ou try aux affectations dans le but d'éliminer

if err != nil{
    return err
}

introduit beaucoup trop de confusion que d'avantages.

Et ajouter quelque chose comme la suggestion trap introduit la possibilité de casse.

C'est pourquoi dans ma proposition que j'ai faite dans un numéro séparé. J'ai autorisé la possibilité de déclarer un func ?() bool sur n'importe quel type de sorte que lorsque vous appelez, dites

x, err := doSomething; return x, err?    

vous pouvez faire en sorte que cet effet secondaire de piège se produise d'une manière qui s'applique à n'importe quel type.

Et appliquer le ? travailler uniquement sur des instructions comme je l'ai montré permet la programmation des instructions. Dans ma proposition, je suggère d'autoriser une instruction switch spéciale qui permet à quelqu'un de basculer sur les cas qui sont le mot-clé + ?

switch {
    case select?:
    //side effect/trap code specific to select
    case return?:
    //side effect/trap code specific to returns
    case for?: 
    //side effect/trap code specific to for? 

    //etc...
}  

Si nous utilisons ? sur un type qui n'a pas d'explicite ? fonction déclarée ou un type intégré, alors le comportement par défaut de la vérification si var == nil || La valeur zéro {exécuter la déclaration} est l'intention présumée.

Idk, je ne suis pas un expert en conception de langage de programmation, mais n'est-ce pas

Par exemple, la fonction os.Chdir est actuellement

func Chdir(dir string) error {
  if e := syscall.Chdir(dir); e != nil {
      return &PathError{"chdir", dir, e}
  }
  return nil
}

Selon cette proposition, il pourrait être écrit comme

func Chdir(dir string) error {
  syscall.Chdir(dir) || &PathError{"chdir", dir, err}
  return nil
}

essentiellement la même chose que les fonctions fléchées de javascript ou comme Dart le définit "syntaxe grosse flèche"

par exemple

func Chdir(dir string) error {
    syscall.Chdir(dir) => &PathError{"chdir", dir, err}
    return nil
}

du tour de fléchettes .

Pour les fonctions qui ne contiennent qu'une seule expression, vous pouvez utiliser une syntaxe abrégée :

bool isNoble(int atomicNumber) => _nobleGases[atomicNumber] != null;
La syntaxe => expr est un raccourci pour { return expr; }. La notation => est parfois appelée syntaxe de grosse flèche.

@mortdeus , le côté gauche de la flèche Dart est une signature de fonction, tandis que syscall.Chdir(dir) est une expression. Ils semblent plus ou moins indépendants.

@mortdeus J'ai oublié de préciser plus tôt, mais l'idée que j'ai commentée ici n'est guère similaire à la proposition que vous avez taguée. J'aime l'idée de ? comme espace réservé, donc je l'ai copié, mais mon idée mettait l'accent sur la réutilisation d'un seul bloc de code pour gérer les erreurs tout en évitant certains des problèmes connus avec try...catch . J'ai fait très attention à proposer quelque chose dont on n'avait pas encore parlé afin de pouvoir apporter une nouvelle idée.

Que diriez-vous d'une nouvelle instruction conditionnelle return (ou returnIf ) ?

return(bool expression) ...

c'est à dire.

err := syscall.Chdir(dir)
return(err != nil) &PathError{"chdir", dir, e}

a, b, err := Foo()    // signature: func Foo() (string, string, error)
return(err != nil) "", "", err

Ou laissez simplement fmt formater les fonctions de retour uniquement sur une ligne au lieu de trois :

err := syscall.Chdir(dir)
if err != nil { return &PathError{"chdir", dir, e} }

a, b, err := Foo()    // signature: func Foo() (string, string, error)
if err != nil { return "", "", err }

Il me vient à l'esprit que je peux obtenir tout ce que je veux avec juste l'ajout d'un opérateur qui revient tôt si l'erreur la plus à droite n'est pas nulle, si je la combine avec des paramètres nommés :

func NewClient(...) (c *Client, err error) {
    defer annotateError(&err, "client couldn't be created")

    listener := net.Listen("tcp4", listenAddr)?
    defer closeOnErr(&err, listener)
    conn := ConnectionManager{}.connect(server, tlsConfig)?
    defer closeOnErr(&err, conn)

    if forwardPort == 0 {
        env, err := environment.GetRuntimeEnvironment()
        if err != nil {
            log.Printf("not forwarding because: %v", err)
        } else {
            forwardPort, err = env.PortToForward()
            if err != nil {
                log.Printf("env couldn't provide forward port: %v", err)
            }
        }
    }
    var forwardOut *forwarding.ForwardOut
    if forwardPort != 0 {
         forwardOut = forwarding.NewOut(pop url.Parse(fmt.Sprintf("http://127.0.0.1:%d", forwardPort)))
    }

    client := &Client{listener: listener, conn: conn, forward: forwardOut}

    toServer := communicationProtocol.Wrap(conn)
    toServer.Send(&client.serverConfig)?
    toServer.Send(&stprotocol.ClientProtocolAck{ClientVersion: Version})?
    session := communicationProtocol.FinalProtocol(conn)?
    client.session = session

    return client, nil
}

func closeOnErr(err *error, c io.Closer) {
    if *err != nil {
        closeErr := c.Close()
        if err != nil {
            *err = multierror.Append(*err, closeErr)
        }
    }
}

func annotateError(err *error, annotation string) {
    if *err != nil {
        log.Printf("%s: %v", annotation, *err)
        *err = errwrap.Wrapf(annotation +": {{err}}", err)
    }
}

Actuellement, ma compréhension du consensus Go va à l'encontre des paramètres nommés, mais si les possibilités des paramètres nommés changent, le consensus peut également changer. Bien sûr, si le consensus est suffisamment fort contre, il est possible d'inclure des accesseurs.

Cette approche me permet d'obtenir ce que je recherche (faciliter la gestion systématique des erreurs au sommet d'une fonction, possibilité de factoriser un tel code, également réduction du nombre de lignes) avec à peu près toutes les autres propositions ici aussi, y compris l'original . Et même si la communauté Go décide qu'elle ne l'aime pas, je n'ai pas à m'en soucier, car c'est dans mon code fonction par fonction et il n'y a pas de décalage d'impédance dans les deux sens.

Bien que j'exprime une préférence pour une proposition qui permet à une fonction de signature func GetInt() (x int, err error) d'être utilisée dans le code avec OtherFunc(GetInt()?, "...") (ou quel que soit le résultat final) à une qui ne peut pas être composée en une expression. Bien qu'il soit moins gênant pour la clause de gestion des erreurs simples et répétitives, la quantité de mon code qui décompresse une fonction d'arité 2 juste pour qu'elle puisse avoir le premier résultat est toujours ennuyeuse et n'ajoute rien à la clarté du code résultant.

@thejerf , j'ai l'impression qu'il y a beaucoup de comportements étranges ici. Vous appelez net.Listen , qui renvoie une erreur, mais elle n'est pas affectée. Et puis vous différez, en passant err . Chaque nouveau defer remplace-t-il le dernier, de sorte que annotateError ne soit jamais invoqué ? Ou s'empilent-ils, de sorte que si une erreur est renvoyée par, disons, toServer.Send , alors closeOnErr est appelé deux fois, puis annotateError est appelé ? Est-ce que closeOnErr appelé uniquement si l'appel précédent a une signature correspondante ? Et cette affaire ?

conn := ConnectionManager{}.connect(server, tlsConfig)?
fmt.Printf("Attempted to connect to server %#v", server)
defer closeOnErr(&err, conn)

En relisant le code, cela embrouille aussi les choses, comme pourquoi ne puis-je pas simplement dire

client.session = communicationProtocol.FinalProtocol(conn)?

Vraisemblablement, parce que FinalProtocol renvoie une erreur ? Mais cela est caché au lecteur.

Enfin, que se passe-t-il lorsque je souhaite signaler une erreur et la récupérer dans une fonction ? Il semble que votre exemple empêcherait ce cas?

_Addenda_

OK, je pense que lorsque vous voulez récupérer d'une erreur, par votre exemple, vous l'affectez, comme dans cette ligne :

env, err := environment.GetRuntimeEnvironment()

C'est bien parce que l'erreur est ombrée, mais alors si je changeais...

forwardPort, err = env.PortToForward()
if err != nil {
    log.Printf("env couldn't provide forward port: %v", err)
}

pour juste

forwardPort = env.PortToForward()

Ensuite, votre erreur différée gérée ne l'attrapera pas, car vous utilisez le err créé dans le champ d'application. Ou est-ce que j'ai raté quelque chose ?

Je pense qu'un ajout à la syntaxe qui indique qu'une fonction peut échouer est un bon début. Je propose quelque chose du genre :

func (r Reader) Read(b []byte) (n int) fails {
    if somethingFailed {
        fail errors.New("something failed")
    }

    return 0
}

Si une fonction échoue (en utilisant le mot clé fail au lieu de return , elle renvoie la valeur zéro pour chaque paramètre de retour.

func (c EmailClient) SendEmail(to, content string) fails {
    if !c.connected() {
        fail errors.New("could not connect")
    }

    // You can handle it and execution will continue if you don't fail or return
    n := r.Read(b) handle (err) {
        fmt.Printf("failed to read: %s", err)
    }

    // This shouldn't compile and should complain about an unhandled error
    n := r.Read(b)
}

Cette approche aura l'avantage de permettre au code actuel de fonctionner, tout en permettant un nouveau mécanisme pour une meilleure gestion des erreurs (au moins dans la syntaxe).

Commentaires sur les mots-clés :

  • fails n'est peut-être pas la meilleure option, mais c'est la meilleure à laquelle je puisse penser actuellement. J'ai pensé à utiliser err (ou errs ), mais la façon dont ils sont utilisés actuellement pourrait en faire un mauvais choix en raison des attentes actuelles ( err est très probablement un nom de variable, et errs peuvent être supposés être une tranche ou un tableau ou des erreurs).
  • handle pourrait être un peu trompeur. Je voulais utiliser recover , mais c'est utilisé pour panic s...

edit : modification de l'invocation r.Read pour qu'elle corresponde à io.Reader.Read() .

Une partie de la raison de cette suggestion est que l'approche actuelle de Go n'aide pas les outils à comprendre si une error renvoyée indique une fonction défaillante ou si elle renvoie une valeur d'erreur dans le cadre de sa fonction (par exemple github.com/pkg/errors ).

Je pense que permettre aux fonctions d'exprimer explicitement l'échec est la première étape pour améliorer la gestion des erreurs.

@ibracho , en quoi votre exemple est-il différent de ...

func (c EmailClient) SendEmail(to, content string) error {
    // ...

    // You can handle it and execution will continue if you don't fail or return
    _, _, err := r.Read()
        if err != nil {
        fmt.Printf("failed to read: %s", err)
    }

    // This shouldn't compile and should complain about an unhandled error
    _, _, err := r.Read()
}

... si nous donnons des avertissements au compilateur ou des peluches pour les instances non gérées de error ? Aucun changement de langue requis.

Deux choses:

  • Je pense que la syntaxe proposée se lit et semble mieux. ??
  • Ma version nécessite de donner aux fonctions la possibilité d'indiquer qu'elles échouent explicitement. C'est quelque chose qui manque actuellement à Go et qui pourrait permettre aux outils d'en faire plus. Nous pouvons toujours traiter une fonction renvoyant une error comme un échec, mais c'est une hypothèse. Que se passe-t-il si la fonction renvoie 2 valeurs error ?

Ma suggestion avait quelque chose que j'ai supprimé, qui était la propagation automatique :

func (c EmailClient) SendEmail(to, content string) fails {
    n := r.Read(b)

    // Would automaticaly propgate the error, so it will be equivlent to this:
    // n := r.Read(b) handle (err) {
    //  fail err
    // }
}

J'ai supprimé cela car je pense que la gestion des erreurs devrait être explicite.

edit : modification de l'invocation r.Read pour qu'elle corresponde à io.Reader.Read() .

Alors, ce serait une signature ou un prototype valide ?

func (r *MyFileReader) Read(b []byte) (n int, err error) fails

(Étant donné qu'une implémentation io.Reader attribue io.EOF lorsqu'il n'y a plus rien à lire et une autre erreur pour les conditions d'échec.)

Oui. Mais personne ne devrait s'attendre err ce que

Je proposais qu'un échec entraînerait le retour des valeurs de retour sous forme de valeurs nulles. Le Reader.Read actuel fait déjà des promesses qui pourraient ne pas être possibles avec cette nouvelle approche.

Lorsque Read rencontre une erreur ou une condition de fin de fichier après avoir lu avec succès n > 0 octets, il renvoie le nombre d'octets lus. Il peut renvoyer l'erreur (non nulle) du même appel ou renvoyer l'erreur (et n == 0) d'un appel suivant. Un exemple de ce cas général est qu'un lecteur renvoyant un nombre non nul d'octets à la fin du flux d'entrée peut renvoyer soit err == EOF soit err == nil. La prochaine lecture doit retourner 0, EOF.

Les appelants doivent toujours traiter les n > 0 octets renvoyés avant de considérer l'erreur d'erreur. Cela permet de gérer correctement les erreurs d'E/S qui se produisent après la lecture de certains octets, ainsi que les deux comportements EOF autorisés.

Il est déconseillé aux implémentations de Read de renvoyer un nombre d'octets nul avec une erreur nulle, sauf lorsque len(p) == 0. Les appelants doivent considérer un retour de 0 et nil comme indiquant que rien ne s'est passé ; en particulier il n'indique pas EOF.

Tous ces comportements ne sont pas possibles dans l'approche actuellement proposée. D'après le contrat d'interface de lecture actuel, je vois quelques lacunes, comme la façon de gérer les lectures partielles.

En général, comment une fonction doit-elle se comporter lorsqu'elle est partiellement terminée au moment où elle échoue ? Honnêtement, je n'y ai pas encore pensé.

Le cas de io.EOF est simple :

func DoSomething(r io.Reader) fails {
    // I'm using rerr so that I don't shadow the err returned from the function
    n, err := r.Read(b) handle (rerr) {
        if rerr != io.EOF {
            fail err
        }
        // Else do nothing?
    }
}

@thejerf , j'ai l'impression qu'il y a beaucoup de comportements étranges ici. Vous appelez net.Listen, qui renvoie une erreur, mais elle n'est pas attribuée.

J'utilise l'opérateur commun ? proposé par plusieurs personnes pour indiquer le retour de l'erreur avec des valeurs nulles pour les autres valeurs, si l'erreur n'est pas nulle. Je préfère légèrement un mot court à un opérateur car je ne pense pas que Go soit une langue lourde d'opérateurs, mais si ? devait entrer dans la langue, je compterais quand même mes gains plutôt que de taper du pied dans un souffle.

Chaque nouveau report remplace-t-il le dernier, de sorte qu'annotateError ne soit jamais invoqué ? Ou s'empilent-ils, de sorte que si une erreur est renvoyée par, disons, toServer.Send, closeOnErr est appelée deux fois, puis annotateError est appelée ?

Cela fonctionne comme defer maintenant : https://play.golang.org/p/F0xgP4h5Vxf juste être en train de réduire le comportement actuel de Go, mais il n'en a pas eu. Hélas. Comme cet extrait le montre également, l'ombrage n'est pas un problème, ou du moins, n'est pas plus un problème qu'il ne l'est déjà. (Cela ne le résoudrait ni ne l'aggraverait particulièrement.)

Je pense qu'un aspect qui peut être déroutant est qu'il est déjà le cas dans le Go actuel qu'un paramètre nommé va finir par être "tout ce qui est réellement retourné", vous pouvez donc faire ce que j'ai fait et prendre un pointeur dessus et passer cela à une fonction différée et la manipulez, que vous renvoyiez directement ou non une valeur comme return errors.New(...) , qui peut intuitivement ressembler à une "nouvelle variable" qui n'est pas la variable nommée, mais en fait Go finira avec elle affectée à la variable nommée au moment où le report s'exécute. Il est facile d'ignorer ce détail particulier du Go actuel pour le moment. Je soumets que même si cela peut être déroutant maintenant, si vous travailliez même sur une base de code qui utilisait cet idiome (c'est-à-dire que je ne dis pas que cela doit même devenir "Go best practice", quelques expositions suffiraient) vous' d comprendre assez rapidement. Parce que, juste pour le dire encore une fois pour être très clair, c'est déjà ainsi que Go fonctionne, pas un changement proposé.

Voici une proposition qui, je pense, n'a pas été suggérée auparavant. À l'aide d'un exemple :

 r, !handleError := something()

La signification de ceci est la même que celle-ci :

 r, _xyzzy := something()
 if ok, R := handleError(_xyzzy); !ok { return R }

(où _xyzzy est une nouvelle variable dont la portée s'étend uniquement à ces deux lignes de code, et R peut être plusieurs valeurs).

Les avantages de cette proposition ne sont pas spécifiques aux erreurs, ne traitent pas spécialement les valeurs zéro et il est facile de spécifier de manière concise comment encapsuler les erreurs dans un bloc de code particulier. Les changements de syntaxe sont petits. Compte tenu de la traduction simple, il est facile de comprendre comment cette fonctionnalité fonctionne.

Les inconvénients sont qu'il introduit un retour implicite, on ne peut pas écrire un gestionnaire générique qui renvoie simplement l'erreur (puisque ses valeurs de retour doivent être basées sur la fonction d'où il est appelé), et que la valeur qui est passée au gestionnaire est pas disponible dans le code d'appel.

Voici comment vous pouvez l'utiliser :

func Read(filename string) error {
  herr := func(err error) (bool, error) {
      if err != nil { return true, fmt.Errorf("Read failed: %s", err) }
      return false, nil
  }

  f, !herr := OpenFile(filename)
  b, !herr := ReadBytes(f)
  !herr := ProcessBytes(b)
  return nil
}

Ou vous pouvez l'utiliser dans un test pour appeler t.Fatal si votre code d'initialisation échoue :

func TestSomething(t *testing.T) {
  must := func(err error) bool { t.Fatalf("init code failed: %s", err); return true }
  !must := setupTest()
  !must := clearDatabase()
  ...
}

Je suggérerais de changer la signature de la fonction en func(error) error . Cela simplifie le cas majoritaire, et si vous avez besoin d'analyser davantage l'erreur, utilisez simplement le mécanisme actuel.

Question de syntaxe : pouvez-vous définir la fonction en ligne ?

func Read(filename string) error {
    f, !func(err error) error {
        if err != nil { return true, fmt.Errorf("... %s", err) }
        return false, nil
    } := OpenFile(filename)
    /...

Je suis à l'aise avec "ne pas faire ça", mais la syntaxe devrait probablement lui permettre de réduire le nombre de cas spéciaux. Cela permettrait également :

func failed(s string) func(error) error {
    return func(err error) {
       // returns a decorated error with the given string
   }
}

func Read(filename string) error {
  f, !failed("couldn't open file") := OpenFile(filename)
  b, !failed("couldn't read file") := ReadBytes(f)
  !failed("couldn't process file") := ProcessBytes(b)
  return nil
}

Ce qui me semble être l'une des meilleures propositions pour ce genre de choses, du moins en termes d'introduction concise de ces éléments. C'est un autre cas où, à mon humble avis, vous dégagez de meilleures erreurs à ce stade que le code basé sur les exceptions, ce qui ne parvient souvent pas à détailler la nature de l'erreur car il est si facile de laisser les exceptions se propager.

Je suggérerais également, pour des raisons de performances, que les fonctions d'erreur de bang soient définies comme n'étant pas appelées sur des valeurs nulles. Cela maintient leur impact sur les performances assez minime; dans le cas que je viens de montrer, si Read réussit normalement, ce n'est pas plus cher qu'une implémentation de lecture actuelle qui est déjà if à chaque erreur et qui échoue à la clause if . Si nous appelons une fonction sur nil tout le temps, cela va devenir très coûteux chaque fois qu'elle ne peut pas être intégrée, ce qui finira par être une quantité de temps non triviale. (Si une erreur se produit activement, nous pouvons probablement justifier et nous permettre un appel de fonction dans presque toutes les circonstances (si vous ne pouvez pas revenir à la méthode actuelle), mais ne le voulons pas vraiment pour les non-erreurs.) Il signifie également que les fonctions bang peuvent prendre une valeur non nulle dans leur implémentation, ce qui les simplifie également.

@thejerf, le chemin agréable mais heureux est fortement modifié.
Il y a de nombreux messages, il y avait une suggestion d'avoir un peu Ruby comme "ou" sintax - f := OpenFile(filename) or failed("couldn't open file") .

Préoccupation supplémentaire : est-ce pour tout type de paramètres ou uniquement pour les erreurs ? si pour les erreurs uniquement - alors le type erreur doit avoir une signification particulière pour le compilateur.

@thejerf, le chemin agréable mais heureux est fortement modifié.

Je recommanderais de faire la distinction entre le chemin probablement commun de la proposition originale où il ressemble à la suggestion originale de Paulhankin :

func Read(filename string) error {
  herr := func(err error) (bool, error) {
      if err != nil { return true, fmt.Errorf("Read failed: %s", err) }
      return false, nil
  }

  f, !herr := OpenFile(filename)
  b, !herr := ReadBytes(f)
  !herr := ProcessBytes(b)
  return nil
}

peut-être même avec herr pris en compte quelque part, et mes explorations de ce qu'il faudrait pour le spécifier complètement, ce qui est une nécessité pour cette conversation, et mes propres réflexions sur la façon dont je pourrais l'utiliser dans mon code personnel, qui est simplement une exploration de ce qui est permis et offert par une suggestion. J'ai déjà dit qu'incorporer littéralement une fonction est probablement une mauvaise idée après tout, mais la grammaire devrait probablement permettre de garder la grammaire simple. Je peux déjà écrire une fonction Go qui prend trois fonctions et toutes les insérer directement dans l'appel. Cela ne veut pas dire que Go est cassé ou que Go doit faire quelque chose pour empêcher cela ; cela signifie que je ne devrais pas le faire si j'apprécie la clarté du code. J'aime que Go offre la clarté du code, mais il y a toujours un certain degré de responsabilité irréductible sur les développeurs pour garder le code clair.

Si vous voulez me dire que le "chemin heureux" pour

func Read(filename string) error {
  f, !failed("couldn't open file") := OpenFile(filename)
  b, !failed("couldn't read file") := ReadBytes(f)
  !failed("couldn't process file") := ProcessBytes(b)
  return nil
}

est brouillé et difficile à lire, mais le chemin heureux est facile à lire avec

func Read(filename string) error {
  f, err := OpenFile(filename)
  if err != nil {
    return fmt.Errorf("Read failed: %s", err)
  }

  b, err := ReadBytes(f)
  if err != nil {
    return fmt.Errorf("Read failed: %s", err)
  }

  err = ProcessBytes(b)
  if err != nil {
    return fmt.Errorf("Read failed: %s", err)
  }

  return nil
}

alors je soumets que le seul moyen possible pour que le second soit plus facile à lire est que vous soyez déjà habitué à le lire et que vos yeux soient entraînés à parcourir exactement le bon chemin pour voir le chemin heureux. Je peux le deviner, parce que les miens le sont aussi. Mais une fois que vous serez habitué à une syntaxe alternative, vous serez également formé pour cela. Vous devez analyser la syntaxe en fonction de ce que vous ressentirez lorsque vous y serez déjà habitué, et non de ce que vous ressentez maintenant.

Je noterais également que les nouvelles lignes ajoutées dans le deuxième exemple représentent ce qui arrive à mon vrai code. Ce ne sont pas seulement des « lignes » de code que la gestion actuelle des erreurs Go a tendance à ajouter au code, elle ajoute également beaucoup de « paragraphes » à ce qui devrait autrement être une fonction très simple. Je veux ouvrir le fichier, lire quelques octets et les traiter. je ne veux pas

Ouvrez un fichier.

Et puis lisez quelques octets, si cela vous convient.

Et puis traitez-les, si cela vous convient.

J'ai l'impression qu'il y a beaucoup de votes négatifs qui correspondent à "ce n'est pas ce à quoi je suis habitué", plutôt qu'une analyse réelle de la façon dont ceux-ci se joueront dans le code réel, une fois que vous y êtes habitué et que vous les utilisez couramment .

Je n'aime pas l'idée de cacher l'instruction return, je préfère :

f := OpenFile(filename) or return failed("couldn't open file")
....
func failed(msg string, err error) error { ... } 

Dans ce cas, or est un opérateur de transfert conditionnel nul ,
transmettre le dernier retour s'il est différent de zéro.
Il existe une proposition similaire en C# utilisant l'opérateur ?>

f := OpenFile(filename) ?> return failed("couldn't open file")

@thejerf "chemin heureux" dans votre cas, précédé d'un appel à failed (...), ce qui peut être très long. sonne aussi comme Yoda :rofl:

Les caractères mon humble avis - (si vous avez plusieurs dispositions de clavier)

S'il vous plaît, ne rendez pas cette façon plus complexe qu'elle ne l'est maintenant. Vraiment déplacer les mêmes codes sur une ligne (au lieu de 3 ou plus) n'est pas vraiment une solution. Personnellement, je ne vois aucune de ces propositions viable à ce point. Les gars, le calcul est très simple. Adoptez l'idée "Try-catch" ou gardez les choses telles qu'elles sont maintenant, ce qui signifie beaucoup de "si alors sinon" et des bruits de code et n'est pas vraiment adapté à une utilisation dans des modèles OO comme Fluent Interface.

Merci beaucoup pour toutes vos aides et peut-être quelques aides ;-) (je plaisante)

@KamyarM IMO, "utiliser l'alternative la plus connue ou n'apporter aucun changement" n'est pas une déclaration très productive. Il stoppe l'innovation et facilite les arguments circulaires.

@KernelDeimos Je suis d'accord avec vous mais je vois de nombreux commentaires sur ce fil qui préconisait essentiellement l'ancien en déplaçant exactement 4 5 lignes en une seule ligne, ce que je ne vois pas comme vraiment une solution et aussi beaucoup dans la communauté Go rejettent très RELIGIEUSEMENT l'utilisation Try-Catch qui ferme les portes à tout autre avis. Personnellement, je pense que ceux qui ont inventé ce concept de try-catch y ont vraiment réfléchi et bien qu'il puisse avoir quelques défauts, mais ces défauts sont simplement causés par de mauvaises habitudes de programmation et il n'y a aucun moyen de forcer les programmeurs à écrire de bons codes même si vous supprimez ou limitez tous les bons ou certains peuvent dire les mauvaises fonctionnalités qu'un langage de programmation peut avoir.
J'ai proposé quelque chose comme ça auparavant et ce n'est pas exactement java ou C# try-catch et je pense qu'il peut prendre en charge la gestion des erreurs et les bibliothèques actuelles et j'utilise l'un des exemples ci-dessus. Donc, fondamentalement, le compilateur vérifie les erreurs après chaque ligne et passe au bloc catch si la valeur err est définie sur non nil :

try (var err error){ 
    f, err := OpenFile(filename)
    b, err := ReadBytes(f)
    err = ProcessBytes(b)
    return nil
} catch (err error){ //Required
   return err
} finally{ // Optional
    // Do something else like close the file or connection to DB. Not necessary in this example since we  return earlier.
}

@KamyarM
Dans votre exemple, comment savoir (au moment de l'écriture du code) quelle méthode a renvoyé l'erreur ? Comment puis-je remplir la troisième façon de gérer l'erreur ("renvoyer l'erreur avec des informations contextuelles supplémentaires") ?

@urandom
une façon consiste à utiliser le commutateur Go et à trouver le type d'exception dans le catch. Disons que vous avez une exception pathError que vous savez causée par OpenFile() de cette façon. Une autre façon qui n'est pas très différente de la gestion actuelle des erreurs if err!=nil dans GoLang est la suivante :

try (var err error){ 
    f, err := OpenFile(filename)
} catch (err error){ //Required
   return err
}
try (var err error){ 
    b, err := ReadBytes(f)
catch (err error){ //Required
   return err
}
try (var err error){ 
    err = ProcessBytes(b)
catch (err error){ //Required
   return err
}
return nil

Vous avez donc des options de cette façon, mais vous n'êtes pas limité. Si vous voulez vraiment savoir exactement quelle ligne a causé le problème, mettez chaque ligne dans une capture d'essai de la même manière que vous écrivez actuellement beaucoup de if-then-else. Si l'erreur n'est pas importante pour vous et que vous souhaitez la transmettre à la méthode de l'appelant qui, dans les exemples discutés dans ce fil de discussion, concerne en fait cela, je pense que mon code proposé fait simplement le travail.

@KamyarM Je vois d'où vous venez maintenant. Je dirais que s'il y a tant de gens contre try...catch, je vois cela comme une preuve que try...catch n'est pas parfait et a des défauts. C'est une solution facile à un problème, mais si Go2 peut améliorer la gestion des erreurs par rapport à ce que nous avons vu dans d'autres langages, je pense que ce serait vraiment cool.

Je pense qu'il est possible de prendre ce qui est bon dans try...catch sans prendre ce qui est mauvais dans try...catch, ce que j'ai proposé plus tôt. Je suis d'accord que transformer trois lignes en 1 ou 2 ne résout rien.

Le problème fondamental, à mon avis, est que le code de gestion des erreurs dans une fonction se répète si une partie de la logique est "retourner à l'appelant". Si vous souhaitez modifier la logique à tout moment, vous devez modifier chaque instance de if err != nil { return nil } .

Cela dit, j'aime beaucoup l'idée de try...catch tant que les fonctions ne peuvent rien throw implicitement.

Une autre chose que je pense serait utile si la logique dans catch {} nécessitait un mot-clé break pour rompre le flux de contrôle. Parfois, vous souhaitez gérer une erreur sans interrompre le flux de contrôle. (ex : "pour chaque élément de ces données, faites quelque chose, ajoutez une erreur non nulle à une liste et continuez")

@KernelDeimos Je suis entièrement d'accord avec cela. J'ai vu la situation exacte comme ça. Vous devez capturer les erreurs autant que possible avant de casser les codes. Si quelque chose comme Channels pouvait être utilisé dans ces situations dans GoLang, c'était bien. Vous pourriez alors envoyer toutes les erreurs à ce canal que catch attend, puis catch pourrait les gérer une par une.

Je préfère mélanger "ou revenir" avec #19642, #21498 plutôt que d'utiliser try..catch (defer/panic/recover existe déjà ; lancer dans la même fonction revient à avoir plusieurs instructions goto et est devenu compliqué avec un commutateur de type supplémentaire à l'intérieur de catch ; permet d'oublier la gestion des erreurs en ayant try..catch en haut de la pile (ou compliquer considérablement le compilateur si la portée try..catch à l'intérieur d'une fonction unique)

@egorse
Il semble que la syntaxe try-catch suggérée par @KamyarM soit un peu de sucre de syntaxe pour gérer les variables de retour d'erreur, pas une introduction pour les exceptions. Bien que je préfère une syntaxe de type "ou retour" pour diverses raisons, cela semble être une suggestion légitime.

Cela étant dit, @KamyarM , pourquoi le try contient-il une partie de définition de variable ? Vous définissez une variable err , mais elle est masquée par les autres variables err dans le bloc lui-même. Quel est son but?

Je pense que c'est pour lui dire quelle variable surveiller, lui permettant d'être découplée du type error . Cela nécessiterait probablement une modification des règles d'observation, à moins que vous n'ayez simplement besoin que les gens y fassent très attention. Je ne suis pas sûr de la déclaration dans le bloc catch , cependant.

@egorse Exactement ce que @DeedleFake a mentionné, c'est son but. Cela signifie que try block a un œil sur cet objet. De plus, cela limite sa portée. C'est quelque chose de similaire à l'instruction using en C#. En C#, le ou les objets définis à l'aide du mot-clé sont automatiquement supprimés une fois ce bloc exécuté et la portée de ces objets est limitée au bloc « Using ».
https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/using-statement

L'utilisation du catch est nécessaire car nous voulons forcer le programmeur à décider comment gérer l'erreur correctement. En C# et Java, catch est également obligatoire. en C# si vous ne voulez pas gérer une exception, vous n'utilisez pas du tout try-catch dans cette fonction. Lorsqu'une exception se produit, n'importe quelle méthode de la hiérarchie des appels peut gérer l'exception ou même la relancer (ou l'envelopper dans une autre exception) à nouveau. Je ne pense pas que vous puissiez faire la même chose en Java. En Java, une méthode susceptible de lever une exception doit la déclarer dans la signature de la fonction.

Je tiens à souligner que ce bloc try-catch n'est pas l'exact. J'ai utilisé ces mots-clés car cela est similaire dans ce qu'il veut atteindre et c'est aussi ce que de nombreux programmeurs connaissent et apprennent dans la plupart des cours de concept de programmation.

Il pourrait y avoir un _return on error_ affectation, qui ne fonctionne que s'il y a un _named error return parameter_, comme dans :

func process(someInput string) (someOutput string, err error) {
    err ?= otherAction()
    return
}

Si err n'est pas nil alors revenez.

Je pense que cette discussion sur l'ajout d'un sucre try à Rust serait éclairante pour les participants à cette discussion.

FWIW, une vieille pensée sur la simplification de la gestion des erreurs (excuses si cela n'a pas de sens) :

L' identifiant de relance , indiqué par le symbole caret ^ , peut être utilisé comme l'un des opérandes du côté gauche d'une affectation. Pour les besoins de l'affectation, l'identifiant de relance est un alias pour la dernière valeur de retour de la fonction conteneur, que la valeur ait ou non un nom. Une fois l'affectation terminée, la fonction teste la dernière valeur de retour par rapport à la valeur zéro de son type (nil, 0, false, ""). Si elle est considérée comme nulle, la fonction continue de s'exécuter, sinon elle revient.

L'objectif principal de l'identifiant de relance est de propager de manière concise les erreurs des fonctions appelées vers l'appelant dans un contexte donné sans cacher le fait que cela se produit.

À titre d'exemple, considérons le code suivant :

func Alpha() (string, error) {

    b, ^ := beta()
    g, ^ := gamma()
    return b + g, nil
}

Cela équivaut à peu près à :

func Alpha() (ret1 string, ret2 error) {

    b, ret2 := beta()
    if ret2 != nil {
        return
    }

    g, ret2 := gamma()
    if ret2 != nil {
        return
    }

    return b + g, nil
}

Le programme est malformé si :

  • l'identifiant de relance est utilisé plus d'une fois dans une affectation
  • la fonction ne retourne pas de valeur
  • le type de la dernière valeur de retour n'a pas de test significatif et efficace pour zéro

Cette suggestion est similaire à d'autres en ce sens qu'elle n'aborde pas le problème de fournir plus d'informations contextuelles, pour ce que cela vaut.

@gboyle C'est pourquoi la dernière valeur de retour IMO doit être nommée et de type error . Cela a deux conséquences importantes:

1 - d'autres valeurs de retour sont également nommées, d'où
2 - ils ont déjà des valeurs zéro significatives.

@object88 Comme nous le montre l'historique du package context , cela nécessite une action de l'équipe principale, comme définir un type error intégré (juste un Go normal error ) avec quelques attributs communs (message ? pile d'appels ? etc etc.).

AFAIK, il n'y a pas beaucoup de constructions de langage contextuel dans Go. A part go et defer il n'y en a pas d'autres et même ces deux sont très explicites et clairs (bot en syntaxe - et à l'oeil - et sémantique).

Et quelque chose comme ça ?

(j'ai copié du vrai code sur lequel je travaille) :

func (g *Generator) GenerateDevices(w io.Writer) error {
    var err error
    catch err {
        _, err = io.WriteString(w, "package cc\n\nconst (") // if err != nil { goto Caught }
        for _, bd := range g.zwClasses.BasicDevices {
            _, err = w.Write([]byte{'\t'}) // if err != nil { goto Caught }
            _, err = io.WriteString(w, toGoName(bd.Name)) // if err != nil { goto Caught }
            _, err = io.WriteString(w, " BasicDeviceType = ") // if err != nil { goto Caught }
            _, err = io.WriteString(w, bd.Key) // if err != nil { goto Caught }
            _, err = w.Write([]byte{'\n'}) // if err != nil { goto Caught }
        }
        _, err = io.WriteString(w, ")\n\nvar BasicDeviceTypeNames = map[BasicDeviceType]string{\n") // if err != nil { goto Caught }
       // ...snip
    }
    // Caught:
    return err
}

Lorsque err n'est pas nul, il s'arrête à la fin de l'instruction "catch". Vous pouvez utiliser "catch" pour regrouper des appels similaires qui renvoient généralement le même type d'erreur. Même si les appels n'étaient pas liés, vous pouvez vérifier les types d'erreurs par la suite et les envelopper de manière appropriée.

@lukescott a lu ce billet de blog par @robpike https://blog.golang.org/errors-are-values

@davecheney L'idée de capture (sans l'essai) reste dans l'esprit de ce sentiment. Il traite l'erreur comme une valeur. Il s'interrompt simplement (au sein de la même fonction) lorsque la valeur n'est plus nulle. Cela ne fait en aucun cas planter le programme.

@lukescott, vous pouvez utiliser la technique de Rob aujourd'hui, vous n'avez pas besoin de changer la langue.

Il y a une assez grande différence entre les exceptions et les erreurs :

  • des erreurs sont attendues (nous pouvons écrire un test pour elles),
  • les exceptions ne sont pas attendues (d'où l'"exception"),

De nombreuses langues traitent les deux comme des exceptions.

Entre les génériques et une meilleure gestion des erreurs, je choisirais une meilleure gestion des erreurs, car la plupart des encombrements de code dans Go proviennent de la gestion des erreurs. Bien que l'on puisse dire que ce type de verbosité est bon et favorise la simplicité, l'OMI masque également le _chemin heureux_ d'un flux de travail au point d'être ambigu.

J'aimerais m'appuyer un peu sur la proposition de

Tout d'abord, au lieu d'un ! , un opérateur or est introduit, ce décalage est le dernier argument renvoyé par l'appel de fonction sur le côté gauche, et invoque une instruction return sur la droite, dont l'expression est une fonction qui est appelée, si l'argument décalé est différent de zéro (non nul pour les types d'erreur), en lui passant cet argument. C'est bien si les gens pensent que cela devrait être uniquement pour les types d'erreur, bien que je pense que cette construction sera utile pour les fonctions qui renvoient également un booléen comme dernier argument (c'est quelque chose d'ok/pas ok).

La méthode Read ressemblera à ceci :

func Read(filename string) error {
  f := OpenFile(filename) or return errors.Contextf("opening file %s", filename)
  b := ReadBytes(f) or return errors.Contextf("reading file %s", filename)
  ProcessBytes(b) or return errors.Context("processing data")
  return nil
}

Je suppose que le package d'erreurs fournit des fonctions pratiques telles que les suivantes :

func Noop() func(error) error {
   return func(err error) {
       return err   
   }
}


func Context(msg string) func(error) error {
    return func(err error) {
        return fmt.Errorf("%s: %v", msg, err)
    }
}
...

Cela semble parfaitement lisible, tout en couvrant tous les points nécessaires, et cela ne semble pas trop étranger non plus, en raison de la familiarité de l'instruction return.

@urandom Dans cette déclaration f := OpenFile(filename) or return errors.Contextf("opening file %s", filename) comment la raison peut-elle être connue ? Par exemple, est-ce le manque d'autorisation de lecture ou le fichier n'existe pas du tout ?

@dc0d
Eh bien, même dans l'exemple ci-dessus, l'erreur d'origine est incluse, car le message donné par l'utilisateur n'est qu'un contexte ajouté. Comme indiqué, et dérivé de la proposition originale, or return attend une fonction qui reçoit un seul paramètre du type décalé. C'est la clé et permet non seulement des fonctions utilitaires qui conviendront à une foule assez nombreuse, mais vous pouvez à peu près écrire les vôtres si vous avez besoin d'une gestion vraiment personnalisée de valeurs spécifiques.

@urandom IMO ça cache trop.

Mes 2 centimes ici, je voudrais proposer une règle simple :

"paramètre d'erreur de résultat implicite pour les fonctions"

Pour toute fonction, un paramètre d'erreur est implicite à la fin de la liste des paramètres de résultat
s'il n'est pas explicitement défini.

Supposons que nous ayons une fonction définie comme suit pour les besoins de la discussion :

func f() (int) {}
qui est identique à : func f() (int, error) {}
selon notre règle d'erreur de résultat implicite.

pour l'affectation, vous pouvez remonter, ignorer ou détecter l'erreur comme suit :

1) faire des bulles

x := f()

si f renvoie une erreur, la fonction en cours retournera immédiatement avec l'erreur
(ou créer une nouvelle pile d'erreurs ?)
si la fonction en cours est principale, le programme s'arrêtera.

Il équivaut à l'extrait de code suivant :

x, erreur := f()
si erreur != nil {
retour..., euh
}

2) ignorer

x, _ := f()

un identificateur vide à la fin de la liste d'expressions d'affectation pour signaler explicitement l'élimination de l'erreur.

3) attraper

x, erreur := f()

l'erreur doit être traitée comme d'habitude.

Je pense que ce changement de convention de code idiomatique ne devrait nécessiter que des changements minimes dans le compilateur
ou un préprocesseur devrait faire le travail.

@dc0d Pouvez-vous donner un exemple de ce qu'il cache et comment ?

@urandom C'est la raison qui a provoqué la question "où est l'erreur d'origine?", comme je l'ai demandé dans un commentaire précédent. Il transmet l'erreur implicitement et il n'est pas clair (par exemple) où est l'erreur d'origine placée dans cette ligne : f := OpenFile(filename) or return errors.Contextf("opening file %s", filename) . L'erreur d'origine renvoyée par OpenFile() - ce qui peut être quelque chose comme un manque d'autorisation de lecture ou l'absence de fichier et pas seulement "il y a quelque chose qui ne va pas avec le nom de fichier".

@dc0d
Je ne suis pas d'accord. C'est à peu près aussi clair que de traiter avec http.Handlers, où avec ce dernier vous les transmettez à un multiplexeur, et soudain vous obtenez une demande et un rédacteur de réponse. Et les gens sont habitués à ce type de comportement. Comment les gens savent-ils ce que fait l'instruction go ? Ce n'est évidemment pas clair lors de la première rencontre, mais c'est assez omniprésent et c'est dans la langue.

Je ne pense pas que nous devrions être contre toute proposition au motif que c'est nouveau et que personne n'a la moindre idée de comment cela fonctionne, car c'est vrai pour la plupart d'entre eux.

@urandom Maintenant, cela a un peu plus de sens (y compris l'exemple http.Handler ).

Et nous discutons des choses. Je ne parle pas contre ou pour une idée précise. Mais je soutiens la simplicité, le fait d'être explicite et en même temps de transmettre un peu de raison à propos de l'expérience des développeurs.

@dc0d

ce qui peut être quelque chose comme un manque d'autorisation de lecture ou l'absence d'un fichier

Dans ce cas, vous ne feriez pas que relancer l'erreur, mais vérifieriez son contenu réel. Pour moi, ce numéro concerne le cas le plus populaire. C'est-à-dire une erreur de relance avec un contexte ajouté. Ce n'est que dans des cas plus rares que vous convertissez l'erreur en un type concret et vérifiez ce qu'elle dit réellement. Et pour cela, la syntaxe actuelle de gestion des erreurs est parfaitement correcte et n'ira nulle part même si l'une des propositions ici était acceptée.

@creker Les erreurs ne sont pas des exceptions (un de mes commentaires précédents). Les erreurs sont des valeurs, il n'est donc pas possible de les lancer ou de les relancer. Pour les scénarios de type try/catch, Go a panique/récupération.

@dc0d Je ne parle pas d'exceptions. Par relancer, j'entends renvoyer l'erreur à l'appelant. Le or return errors.Contextf("opening file %s", filename) proposé enveloppe et renvoie une erreur.

@creker Merci pour l'explication. Il ajoute également des appels de fonction supplémentaires qui affectent le planificateur qui à son tour peut ne pas produire le comportement souhaité dans certaines situations.

@dc0d c'est un détail d'implémentation et pourrait changer à l'avenir. Et cela pourrait réellement changer, la préemption non coopérative est en cours en ce moment.

@creker
Je pense que vous pouvez couvrir encore plus de cas que de simplement renvoyer une erreur modifiée :

func retryReadErrHandler(filename string, count int) func(error) error {
     return func(err error) error {
          if os.IsTimeout(err) {
               count++
               return Read(filename, count)
          }
          if os.IsPermission(err) {
               log.Fatal("Permission")
          }

          return fmt.Errorf("opening file %s: %v", filename, err)
      }
}

func Read(filename string, count int) error {
  if count > 3 {
    return errors.New("max retries")
  }

  f := OpenFile(filename) or return retryReadErrHandler(filename, count)

  ...
}

@dc0d
Les appels de fonction supplémentaires seront probablement intégrés par le compilateur

@urandom ça a l'air très intéressant. Un peu magique avec un argument implicite mais celui-ci pourrait en fait être assez général et concis pour tout couvrir. Ce n'est que dans de très rares cas que vous auriez à recourir à des if err != nil réguliers

@urandom , je suis confus par votre exemple. Pourquoi retryReadErrHandler renvoie-t-il une fonction ?

@ objet88
C'est l'idée derrière l'opérateur or return . Il attend une fonction qu'il appellera dans le cas d'un dernier argument renvoyé différent de zéro du côté gauche. À cet égard, il agit exactement de la même manière qu'un http.Handler, laissant la logique réelle de la façon de gérer l'argument et son retour (ou la demande et sa réponse, dans le cas d'un gestionnaire), au rappel. Et pour utiliser vos propres données personnalisées dans le rappel, vous créez une fonction wrapper qui reçoit ces données en tant que paramètres et renvoie ce qui est attendu.

Ou en termes plus familiers, cela ressemble à ce que nous faisons habituellement avec les gestionnaires :
``` va
func nodesHandler(repo Repo) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
données, _ := json.Marshal(repo.GetNodes())
w.Write(données)
})
}

@urandom , vous pourriez éviter un peu de magie en laissant le LHS le même qu'aujourd'hui et en changeant or ... return en returnif (cond) :

func Read(filename string) error {
   f, err := OpenFile(filename) returnif(err != nil) errors.Contextf(err, "opening file %s", filename)
   b, err := ReadBytes(f) returnif(err != nil) errors.Contextf(err, "reading file %s", filename)
   err = ProcessBytes(b) returnif(err != nil) errors.Context(err, "processing data")
   return nil
}

Cela améliore la généralité et la transparence des valeurs d'erreur à gauche et de la condition de déclenchement à droite.

Plus je vois ces différentes propositions, plus je suis enclin à vouloir un changement purement gofmt. Le langage a déjà le pouvoir, rendons-le simplement plus numérisable. @billyh , pour ne pas s'en prendre à votre suggestion en particulier mais returnif(cond) ... n'est qu'une façon de réécrire if cond { return ...} . Pourquoi ne pouvons-nous pas simplement écrire ce dernier? Nous savons déjà ce que cela signifie.

x, err := foo()
if err != nil { return fmt.Errorf(..., err) }

ou même

if x, err := foo(); err != nil { return fmt.Errorf(..., err) }

ou

x, err := foo(); if err != nil { return fmt.Errorf(..., err) }

Pas de nouveaux mots-clés, syntaxe ou opérateurs magiques.

(Cela pourrait aider si nous corrigeons également #377 pour ajouter une certaine flexibilité à l'utilisation de := .)

@ randall77 Je suis de plus en plus enclin de cette façon aussi.

@ randall77 À quel moment ce bloc sera-t-il enroulé en ligne ?

La solution ci-dessus est plus agréable par rapport aux alternatives proposées ici, mais je ne suis pas convaincu que ce soit mieux que de ne rien faire. Gofmt doit être aussi déterministe que possible.

@as , je n'y ai pas vraiment réfléchi, mais peut-être que "si le corps d'une instruction if contient une seule instruction return , alors l'instruction if est formatée comme une seule ligne."

Peut-être qu'il doit y avoir une restriction supplémentaire sur la condition if , comme s'il doit s'agir d'une variable booléenne ou d'un opérateur binaire de deux variables ou constantes.

@billyh
Je ne vois pas la nécessité de le rendre plus détaillé, car je ne vois rien de confus dans ce petit peu de magie dans le or . Je suppose que contrairement à @as , beaucoup de gens ne trouvent rien non plus déroutant dans la façon dont nous travaillons avec les gestionnaires http.

@randall77
Ce que vous suggérez est plus conforme à une suggestion de style de code, et c'est là que vont les opinions très arrêtées. Il se peut que cela ne fonctionne pas bien dans la communauté dans son ensemble pour qu'il y ait soudainement 2 styles de formatage des instructions if.

Sans oublier que des doublures comme celles-ci sont beaucoup plus difficiles à lire. if != ; { } est trop même sur plusieurs lignes, d'où cette proposition. Le modèle est fixe pour presque tous les cas et peut être transformé en sucre syntaxique facile à lire et à comprendre.

Le problème que j'ai avec la plupart de ces suggestions est que ce qui se passe n'est pas clair. Dans le message d'ouverture, il est suggéré de réutiliser || pour renvoyer une erreur. Il n'est pas clair pour moi qu'un retour se passe là-bas. Je pense que si une nouvelle syntaxe doit être inventée, elle doit s'aligner sur les attentes de la plupart des gens. Quand je vois || je ne m'attends pas à un retour, ni même à une interruption d'exécution. C'est choquant pour moi.

J'aime le sentiment "les erreurs sont des valeurs" de Go, mais je suis également d'accord pour dire que if err := expression; err != nil { return err } est trop verbeux, principalement parce que presque chaque appel devrait renvoyer une erreur. Cela signifie que vous allez en avoir beaucoup, et il est facile de tout gâcher, selon l'endroit où l'erreur est déclarée (ou masquée). C'est arrivé avec notre code.

Étant donné que Go n'utilise pas try/catch et utilise panic/defer pour des circonstances "exceptionnelles", nous pouvons avoir la possibilité de réutiliser les mots-clés try et/ou catch pour raccourcir la gestion des erreurs sans faire planter le programme.

Voici une réflexion que j'ai eue :

func WriteFooBar(w io.Writer) (err error) {
    _, try err = io.WriteString(w, "foo")
    _, try err = w.Write([]byte{','})
    _, try err = io.WriteString(w, "bar")
    return
}

L'idée est que vous préfixez err sur la LHS avec le mot-clé try . Si err n'est pas nul, un retour se produit immédiatement. Vous n'êtes pas obligé d'utiliser une capture ici, à moins que le retour ne soit pas complètement satisfait. Cela s'aligne davantage sur les attentes des gens concernant "l'exécution d'un essai interrompu", mais au lieu de faire planter le programme, il renvoie simplement.

Si le retour n'est pas complètement satisfait (vérification de l'heure de compilation) ou si nous voulons envelopper l'erreur, nous pouvons utiliser catch comme étiquette spéciale avant uniquement comme ceci :

func WriteFooBar(w io.Writer) (err error) {
    _, try err = io.WriteString(w, "foo")
    _, try err = w.Write([]byte{','})
    _, try err = io.WriteString(w, "bar")
    return
catch:
    return &CustomError{"some detail", err}
}

Cela vous permet également de vérifier et d'ignorer certaines erreurs :

func WriteFooBar(w io.Writer) (err error) {
    _, try err = io.WriteString(w, "foo")
    _, try err = w.Write([]byte{','})
    _, err = io.WriteString(w, "bar")
        if err == io.EOF {
            err = nil
        } else {
            goto catch
        }
    return
catch:
    return &CustomError{"some detail", err}
}

Peut-être pourriez-vous même demander à try spécifier le libellé :

func WriteFooBar(w io.Writer) (err error) {
    _, try(handle1) err = io.WriteString(w, "foo")
    _, try(handle2) err = w.Write([]byte{','})
    _, try(handle3) err = io.WriteString(w, "bar")
    return
handle1:
    return &CustomError1{"...", err}
handle2:
    return &CustomError2{"...", err}
handle3:
    return &CustomError3{"...", err}
}

Je me rends compte que mes exemples de code sont un peu nuls (foo/bar, ack). Mais j'espère avoir illustré ce que je veux dire par aller avec/contre les attentes existantes. Je serais également tout à fait d'accord pour conserver les erreurs telles qu'elles sont dans Go 1. Mais si une nouvelle syntaxe est inventée, il faut réfléchir soigneusement à la façon dont cette syntaxe est déjà perçue, pas seulement dans Go. Il est difficile d'inventer une nouvelle syntaxe sans qu'elle ait déjà un sens, il est donc souvent préférable d'aller avec les attentes existantes plutôt que contre elles.

Peut-être une sorte de chaînage comme comment vous pouvez chaîner des méthodes mais pour les erreurs ? Je ne sais pas vraiment à quoi cela ressemblerait ou si cela fonctionnerait même, juste une idée folle.
Vous pouvez trier la chaîne maintenant pour réduire le nombre de vérifications d'erreurs en maintenant une sorte de valeur d'erreur dans une structure et en l'extrayant à la fin de la chaîne.

C'est une situation très curieuse car même s'il y a un peu de passe-partout, je ne sais pas trop comment le simplifier davantage tout en ayant un sens.

@thejerf « exemple de code de ressemble à ceci avec @lukescott » proposition:

func NewClient(...) (*Client, error) {
    listener, try err := net.Listen("tcp4", listenAddr)
    defer func() {
        if err != nil {
            listener.Close()
        }
    }()

    conn, try err := ConnectionManager{}.connect(server, tlsConfig)
    defer func() {
        if err != nil {
            conn.Close()
        }
    }()

    if forwardPort == 0 {
        env, err := environment.GetRuntimeEnvironment()
        if err != nil {
            log.Printf("not forwarding because: %v", err)
        } else {
            forwardPort, err = env.PortToForward()
            if err != nil {
                log.Printf("env couldn't provide forward port: %v", err)
            }
        }
    }
    var forwardOut *forwarding.ForwardOut
    if forwardPort != 0 {
        u, _ := url.Parse(fmt.Sprintf("http://127.0.0.1:%d", forwardPort))
        forwardOut = forwarding.NewOut(u)
    }

    client := &Client{...}

    toServer := communicationProtocol.Wrap(conn)
    try err = toServer.Send(&client.serverConfig)

    try err = toServer.Send(&stprotocol.ClientProtocolAck{ClientVersion: Version})

    session, try err := communicationProtocol.FinalProtocol(conn)
    client.session = session

    return client, nil

catch:
    return nil, err
}

Il passe de 59 lignes à 47.

C'est la même longueur, mais je pense que c'est un peu plus clair que d'utiliser defer :

func NewClient(...) (*Client, error) {
    var openedListener, openedConn bool
    listener, try err := net.Listen("tcp4", listenAddr)
    openedListener = true

    conn, try err := ConnectionManager{}.connect(server, tlsConfig)
    openedConn = true

    if forwardPort == 0 {
        env, err := environment.GetRuntimeEnvironment()
        if err != nil {
            log.Printf("not forwarding because: %v", err)
        } else {
            forwardPort, err = env.PortToForward()
            if err != nil {
                log.Printf("env couldn't provide forward port: %v", err)
            }
        }
    }
    var forwardOut *forwarding.ForwardOut
    if forwardPort != 0 {
        u, _ := url.Parse(fmt.Sprintf("http://127.0.0.1:%d", forwardPort))
        forwardOut = forwarding.NewOut(u)
    }

    client := &Client{...}

    toServer := communicationProtocol.Wrap(conn)
    try err = toServer.Send(&client.serverConfig)

    try err = toServer.Send(&stprotocol.ClientProtocolAck{ClientVersion: Version})

    session, try err := communicationProtocol.FinalProtocol(conn)
    client.session = session

    return client, nil

catch:
    if openedConn {
        conn.Close()
    }
    if openedListener {
        listener.Close()
    }
    return nil, err
}

Cet exemple serait probablement plus facile à suivre avec un deferifnotnil ou quelque chose.
Mais cela revient en quelque sorte à toute une ligne de question sur laquelle portent bon nombre de ces suggestions.

Après avoir joué un peu avec l'exemple de code, je suis maintenant contre la variante try(label) name . Je pense que si vous avez plusieurs choses de fantaisie à faire, utilisez simplement le système actuel de if err != nil { ... } . Si vous faites essentiellement la même chose, par exemple en définissant un message d'erreur personnalisé, vous pouvez procéder comme suit :

func WriteFooBar(w io.Writer) (err error) {
    msg := "thing1 went wrong"
    _, try err = io.WriteString(w, "foo")
    msg = "thing2 went wrong"
    _, try err = w.Write([]byte{','})
    msg = "thing3 went wrong"
    _, try err = io.WriteString(w, "bar")
    return nil

catch:
    return &CustomError{msg, err}
}

Si quelqu'un a utilisé Ruby, cela ressemble beaucoup à sa syntaxe rescue , qui je pense se lit assez bien.

Une chose qui pourrait être faite est de faire de nil une valeur fausse et d'évaluer les autres valeurs comme vraies, donc vous vous retrouvez avec :

err := doSomething()
if err { return err }

Mais je ne suis pas sûr que cela fonctionnerait vraiment et cela ne fait que raser quelques personnages.
J'ai tapé beaucoup de choses, mais je ne pense pas avoir déjà tapé != nil .

Rendre les interfaces vrai/faux a déjà été mentionné, et j'ai dit que cela rendrait les bogues avec des drapeaux plus fréquents :

verbose := flag.Bool("v", false, "verbose logging")
flag.Parse()
if verbose { ... } // should be *verbose!

@carlmjohnson , dans l'exemple que vous avez fourni juste au-dessus, il y a des messages d'erreur entrecoupés de code happy-path, ce qui m'est un peu étrange. Si vous devez formater ces chaînes, vous faites beaucoup de travail supplémentaire, que quelque chose se passe mal ou non :

func (f *foo) WriteFooBar(w io.Writer) (err error) {
    msg := fmt.Sprintf("While writing %s, thing1 went wrong", f.foo)
    _, try err = io.WriteString(w, f.foo)
    msg = fmt.Sprintf("While writing %s, thing2 went wrong", f.separator)
    _, try err = w.Write(f.separator)
    msg = fmt.Sprintf("While writing %s, thing3 went wrong", f.bar)
    _, try err = io.WriteString(w, f.bar)
    return nil

catch:
    return &CustomError{msg, err}
}

@ object88 , je pense que l'analyse SSA devrait être capable de déterminer si certaines affectations ne sont pas utilisées et de les réorganiser pour qu'elles ne se produisent pas si elles ne sont pas nécessaires (trop optimiste ?). Si c'est vrai, cela devrait être efficace :

func (f *foo) WriteFooBar(w io.Writer) (err error) {
    var format string, args []interface{}

    msg = "While writing %s, thing1 went wrong", 
    args = []interface{f.foo}
    _, try err = io.WriteString(w, f.foo)

    format = "While writing %s, thing2 went wrong"
    args = []interface{f.separator}
    _, try err = w.Write(f.separator)

    format = "While writing %s, thing3 went wrong"
    args = []interface{f.bar}
    _, try err = io.WriteString(w, f.bar)
    return nil

catch:
    msg := fmt.Sprintf(format, args...)
    return &CustomError{msg, err}
}

Serait-ce légal ?

func Foo() error {
catch:
    try _ = doThing()
    return nil
}

Je pense que cela devrait boucler jusqu'à ce que doThing() renvoie zéro, mais je pourrais être convaincu du contraire.

@carlmjohnson

Après avoir joué un peu avec l'exemple de code, je suis maintenant contre la variante du nom try(label).

Oui, je n'étais pas sûr de la syntaxe. Je n'aime pas ça parce que ça fait ressembler try à un appel de fonction. Mais je pouvais voir l'intérêt de spécifier une étiquette différente.

Serait-ce légal ?

Je dirais oui parce que try devrait être avant seulement. Si vous vouliez le faire, je dirais que vous devez le faire comme ceci:

func Foo() error {
tryAgain:
    if err := doThing(); err != nil {
        goto tryAgain
    }
    return nil
}

Ou comme ça :

func Foo() error {
    for doThing() != nil {}
    return nil
}

@Azareal

Une chose qui pourrait être faite est de faire de nil une valeur fausse et d'évaluer les autres valeurs comme vraies, donc vous vous retrouvez avec : err := doSomething() if err { return err }

Je pense qu'il est utile de le raccourcir. Cependant, je ne pense pas que cela devrait s'appliquer à zéro dans toutes les situations. Peut-être qu'il pourrait y avoir une nouvelle interface comme celle-ci :

interface Truthy {
  True() bool
}

Ensuite, toute valeur qui implémente cette interface peut être utilisée comme vous l'avez proposé.

Cela fonctionnerait tant que l'erreur implémentait l'interface :

err := doSomething()
if err { return err }

Mais cela ne fonctionnerait pas :

err := doSomething()
if err == true { return err } // err is not true

Je suis vraiment nouveau dans le golang, mais que pensez-vous de l'introduction du délégateur conditionnel comme ci-dessous ?

func someFunc() error {

    errorHandler := delegator(arg1 Arg1, err error) error if err != nil {
        // ...
        return err // or modifiedErr
    }

    ret, err := doSomething()
    delegate errorHandler(ret, err)

    ret, err := doAnotherThing()
    delegate errorHandler(ret, err)

    return nil
}

le délégant fonctionne comme des trucs mais

  • Son return signifie return from its caller context . (le type de retour doit être le même que l'appelant)
  • Il faut éventuellement if avant { , dans l'exemple ci-dessus c'est if err != nil .
  • Il doit être délégué par l'appelant avec le mot-clé delegate

Il peut être possible d'omettre delegate pour déléguer, mais je pense que cela rend difficile la lecture du flux de la fonction.

Et peut-être que c'est utile non seulement pour la gestion des erreurs, mais je ne suis pas sûr maintenant.

C'est beau d'ajouter check , mais tu peux aller plus loin avant de revenir :

result, err := openFile(f);
if err != nil {
        log.Println(..., err)
    return 0, err 
}

devient

result, err := openFile(f);
check err

``` Allez
résultat, err := openFile(f);
vérifier erreur {
log.Println(..., err)
}

```Go
reslt, _ := check openFile(f)
// If err is not nil direct return, does not execute the next step.

``` Allez
résultat, err := vérifier openFile(f) {
log.Println(..., err)
}

It also attempts simplifying the error handling (#26712):
```Go
result, err := openFile(f);
check !err {
    // err is an interface with value nil or holds a nil pointer
    // it is unusable
    result.C...()
}

Il tente également de simplifier la gestion des erreurs (considérée par certains comme fastidieuse) (#21161). Il deviendrait :

result, err := openFile(f);
check err {
   // handle error and return
    log.Println(..., err)
}

Bien sûr, vous pouvez utiliser un try et d'autres mots-clés à la place du check , si c'est plus clair.

reslt, _ := try openFile(f)
// If err is not nil direct return, does not execute the next step.

``` Allez
résultat, err := openFile(f);
essayez err {
// gérer l'erreur et retourner
log.Println(..., err)
}

Reference:

A plain idea, with support for error decoration, but requiring a more drastic language change (obviously not for go1.10) is the introduction of a new check keyword.

It would have two forms: check A and check A, B.

Both A and B need to be error. The second form would only be used when error-decorating; people that do not need or wish to decorate their errors will use the simpler form.

1st form (check A)
check A evaluates A. If nil, it does nothing. If not nil, check acts like a return {<zero>}*, A.

Examples

If a function just returns an error, it can be used inline with check, so
```Go
err := UpdateDB()    // signature: func UpdateDb() error
if err != nil {
    return err
}

devient

check UpdateDB()

Pour une fonction avec plusieurs valeurs de retour, vous devrez attribuer, comme nous le faisons maintenant.

a, b, err := Foo()    // signature: func Foo() (string, string, error)
if err != nil {
    return "", "", err
}

// use a and b

devient

a, b, err := Foo()
check err

// use a and b

2ème forme (cocher A, B)
vérifier A, B évalue A. Si nul, il ne fait rien. S'il n'est pas nul, le chèque agit comme un retour {}*, B.

C'est pour les besoins de décoration d'erreur. Nous vérifions toujours A, mais c'est B qui est utilisé dans le retour implicite.

Exemple

a, err := Bar()    // signature: func Bar() (string, error)
if err != nil {
    return "", &BarError{"Bar", err}
}

devient

a, err := Foo()
check err, &BarError{"Bar", err}

Remarques
C'est une erreur de compilation pour

utiliser l'instruction de contrôle sur les choses qui ne s'évaluent pas en erreur
utilisez check dans une fonction avec des valeurs de retour qui ne sont pas sous la forme { type }*, erreur
Le contrôle de forme à deux expressions A, B est court-circuité. B n'est pas évalué si A est nul.

Remarques sur la praticité
Il existe une prise en charge des erreurs de décoration, mais vous ne payez pour la syntaxe de contrôle A, B plus lourde que lorsque vous avez réellement besoin de décorer les erreurs.

Pour le if err != nil { return nil, nil, err } partout (ce qui est très courant), l'erreur de vérification est aussi brève que possible sans sacrifier la clarté (voir la note sur la syntaxe ci-dessous).

Remarques sur la syntaxe
Je dirais que ce type de syntaxe (vérifier .., au début de la ligne, similaire à un retour) est un bon moyen d'éliminer les erreurs de vérification sans masquer la perturbation du flux de contrôle que les retours implicites introduisent.

Un inconvénient d'idées comme le||etpriseci-dessus, ou le a, b = foo()? proposé dans un autre fil, c'est qu'ils masquent la modification du flux de contrôle d'une manière qui rend le flux plus difficile à suivre ; le premier avec ||machines ajoutées à la fin d'une ligne par ailleurs simple, cette dernière avec un petit symbole qui peut apparaître partout, y compris au milieu et à la fin d'une ligne de code simple, éventuellement plusieurs fois.

Une instruction de contrôle sera toujours de niveau supérieur dans le bloc actuel, ayant la même importance que d'autres instructions qui modifient le flux de contrôle (par exemple, un retour anticipé).

Voici une autre pensée.

Imaginez une instruction again qui définit une macro avec une étiquette. L'instruction qu'elle étiquette peut à nouveau être développée par substitution textuelle plus tard dans la fonction (rappelant à const/iota, avec des nuances de goto :-] ).

Par example:

func(foo int) (int, error) {
    err := f(foo)
again check:
    if err != nil {
        return 0, errors.Wrap(err)
    }
    err = g(foo)
    check
    x, err := h()
    check
    return x, nil
}

équivaudrait exactement à :

func(foo int) (int, error) {
    err := f(foo)
    if err != nil {
        return 0, errors.Wrap(err)
    }
    err = g(foo)
    if err != nil {
        return 0, errors.Wrap(err)
    }
    x, err := h()
    if err != nil {
        return 0, errors.Wrap(err)
    }
    return x, nil
}

Notez que l'expansion de la macro n'a pas d'arguments - cela signifie qu'il devrait y avoir moins de confusion sur le fait qu'il s'agit d'une macro, car le compilateur n'aime pas les symboles seuls .

Comme l'instruction goto, la portée de l'étiquette se trouve dans la fonction actuelle.

Idée intéressante. J'ai aimé l'idée de l'étiquette de capture, mais je ne pense pas qu'elle convenait bien aux portées Go (avec le Go actuel, vous ne pouvez pas goto une étiquette avec de nouvelles variables définies dans sa portée). L'idée again résout ce problème car l'étiquette vient avant l'introduction de nouvelles portées.

Voici à nouveau le méga-exemple :

func NewClient(...) (*Client, error) {
    var (
        err      error
        listener net.Listener
        conn     net.Conn
    )
    catch {
        if conn != nil {
            conn.Close()
        }
        if listener != nil {
            listener.Close()
        }
        return nil, err
    }

    listener, try err = net.Listen("tcp4", listenAddr)

    conn, try err = ConnectionManager{}.connect(server, tlsConfig)

    if forwardPort == 0 {
        env, err := environment.GetRuntimeEnvironment()
        if err != nil {
            log.Printf("not forwarding because: %v", err)
        } else {
            forwardPort, err = env.PortToForward()
            if err != nil {
                log.Printf("env couldn't provide forward port: %v", err)
            }
        }
    }
    var forwardOut *forwarding.ForwardOut
    if forwardPort != 0 {
        u, _ := url.Parse(fmt.Sprintf("http://127.0.0.1:%d", forwardPort))
        forwardOut = forwarding.NewOut(u)
    }

    client := &Client{...}

    toServer := communicationProtocol.Wrap(conn)
    try err = toServer.Send(&client.serverConfig)

    try err = toServer.Send(&stprotocol.ClientProtocolAck{ClientVersion: Version})

    session, try err := communicationProtocol.FinalProtocol(conn)
    client.session = session

    return client, nil
}

Voici une version plus proche de la proposition de Rog (je l'aime moins) :

func NewClient(...) (*Client, error) {
    var (
        err      error
        listener net.Listener
        conn     net.Conn
    )
again:
    if err != nil {
        if conn != nil {
            conn.Close()
        }
        if listener != nil {
            listener.Close()
        }
        return nil, err
    }

    listener, err = net.Listen("tcp4", listenAddr)
    check

    conn, err = ConnectionManager{}.connect(server, tlsConfig)
    check

    if forwardPort == 0 {
        env, err := environment.GetRuntimeEnvironment()
        if err != nil {
            log.Printf("not forwarding because: %v", err)
        } else {
            forwardPort, err = env.PortToForward()
            if err != nil {
                log.Printf("env couldn't provide forward port: %v", err)
            }
        }
    }
    var forwardOut *forwarding.ForwardOut
    if forwardPort != 0 {
        u, _ := url.Parse(fmt.Sprintf("http://127.0.0.1:%d", forwardPort))
        forwardOut = forwarding.NewOut(u)
    }

    client := &Client{...}

    toServer := communicationProtocol.Wrap(conn)
    err = toServer.Send(&client.serverConfig)
    check

    err = toServer.Send(&stprotocol.ClientProtocolAck{ClientVersion: Version})
    check

    session, err := communicationProtocol.FinalProtocol(conn)
    check
    client.session = session

    return client, nil
}

@carlmjohnson Pour mémoire, ce n'est pas tout à fait ce que je suggère. L'identifiant "check" n'est pas spécial - il doit être déclaré en le plaçant après le mot clé "again".

De plus, je suggérerais que l'exemple ci-dessus n'illustre pas très bien son utilisation - il n'y a rien dans l'instruction à nouveau étiquetée ci-dessus qui ne pourrait tout aussi bien être fait dans une instruction defer. Dans l'exemple try/catch, ce code ne peut pas (par exemple) envelopper l'erreur avec des informations sur l'emplacement source du retour d'erreur. Cela ne fonctionnera pas non plus AFAICS si vous ajoutez un "try" dans l'une de ces instructions if (par exemple pour vérifier l'erreur renvoyée par GetRuntimeEnvironment), car l'"err" référencé par l'instruction catch est dans une portée différente de celle déclaré à l'intérieur du bloc.

Je pense que mon seul problème avec un mot-clé check est que toutes les sorties vers une fonction doivent être un return (ou au moins avoir une sorte de connotation "Je vais quitter la fonction"). Nous _pourrions_ obtenir become (pour le coût total de possession), au moins become a une sorte de "Nous devenons une fonction différente"... mais le mot "vérifier" ne ressemble vraiment pas à ça va être une sortie pour la fonction.

Le point de sortie d'une fonction est extrêmement important, et je ne sais pas si check vraiment cette sensation de "point de sortie". En dehors de cela, j'aime beaucoup l'idée de ce que fait check , cela permet une gestion des erreurs beaucoup plus compacte, mais permet toujours de gérer chaque erreur différemment, ou d'envelopper l'erreur comme vous le souhaitez.

Puis-je également ajouter une suggestion?
Et quelque chose comme ça :

func Open(filename string) os.File onerror (string, error) {
       f, e := os.Open(filename)
       if e != nil { 
              fail "some reason", e // instead of return keyword to go on the onerror 
       }
      return f
}

f := Open(somefile) onerror reason, e {
      log.Prinln(reason)
      // try to recover from error and reasign 'f' on success
      nf = os.Create(somefile) onerror err {
             panic(err)
      }
      return nf // return here must return whatever Open returns
}

L'affectation d'erreur peut avoir n'importe quelle forme, même être quelque chose de stupide comme

f := Open(name) =: e

Ou renvoyez un ensemble de valeurs différent en cas d'erreur, pas seulement d'erreurs, et un bloc try catch serait également bien.

try {
    f := Open("file1") // can fail here
    defer f.Close()
    f1 := Open("file2") // can fail here
    defer f1.Close()
    // do something with the files
} onerror err {
     log.Println(err)
}

@cthackers Je pense personnellement que c'est très bien pour les erreurs de Go de ne pas avoir de traitement spécial. Ce sont simplement des valeurs, et je pense qu'elles devraient le rester.

De plus, try-catch (et des constructions similaires) est juste tout autour d'une mauvaise construction qui encourage les mauvaises pratiques. Chaque erreur doit être gérée séparément, et non par un gestionnaire d'erreurs "attrape-tout".

https://go.googlesource.com/proposal/+/master/design/go2draft-error-handling-overview.md
c'est trop compliqué.

mon idée : |err| signifie vérifier l'erreur : if err!=nil {}

// common util func
func parseInt(s string) (i int64, err error){
    return strconv.ParseInt(s, 10, 64)
}

// expression check err 1 : check and use err variable
func parseAndSum(a string ,b string) (int64,error) {
    sum := parseInt(a) + parseInt(b)  |err| return 0,err
    return sum,nil
} 

// expression check err 2 : unuse variable 
func parseAndSum(a string , b string) (int64,error) {
    a,err := parseInt(a) |_| return 0, fmt.Errorf("parseInt error: %s", a)
    b,err := parseInt(b) |_| { println(b); return 0,fmt.Errorf("parseInt error: %s", b);}
    return a+b,nil
} 

// block check err 
func parseAndSum(a string , b string) (  int64,  error) {
    {
      a := parseInt(a)  
      b := parseInt(b)  
      return a+b,nil
    }|err| return 0,err
} 

@chen56 et tous les futurs commentateurs : voir https://go.googlesource.com/proposal/+/master/design/go2draft.md .

Je soupçonne que cela rend ce fil obsolète maintenant et il est inutile de continuer ici. La page de commentaires Wiki est l'endroit où les choses devraient probablement aller à l'avenir.

Le méga exemple utilisant la proposition Go 2:

func NewClient(...) (*Client, error) {
    var (
        listener net.Listener
        conn     net.Conn
    )
    handle err {
        if conn != nil {
            conn.Close()
        }
        if listener != nil {
            listener.Close()
        }
        return nil, err
    }

    listener = check net.Listen("tcp4", listenAddr)

    conn = check ConnectionManager{}.connect(server, tlsConfig)

    if forwardPort == 0 {
        env, err := environment.GetRuntimeEnvironment()
        if err != nil {
            log.Printf("not forwarding because: %v", err)
        } else {
            forwardPort, err = env.PortToForward()
            if err != nil {
                log.Printf("env couldn't provide forward port: %v", err)
            }
        }
    }
    var forwardOut *forwarding.ForwardOut
    if forwardPort != 0 {
        u, _ := url.Parse(fmt.Sprintf("http://127.0.0.1:%d", forwardPort))
        forwardOut = forwarding.NewOut(u)
    }

    client := &Client{...}

    toServer := communicationProtocol.Wrap(conn)
    check toServer.Send(&client.serverConfig)

    check toServer.Send(&stprotocol.ClientProtocolAck{ClientVersion: Version})

    session := check communicationProtocol.FinalProtocol(conn)
    client.session = session

    return client, nil
}

Je pense que c'est à peu près aussi propre que nous pouvons l'espérer. Le bloc handle a les bonnes qualités de l'étiquette again ou du mot-clé rescue Ruby. Les seules questions qui me restent à l'esprit sont de savoir s'il faut utiliser la ponctuation ou un mot-clé (je pense mot-clé) et s'il faut autoriser l'obtention de l'erreur sans la renvoyer.

J'essaie de comprendre la proposition - il semble qu'il n'y ait qu'un seul bloc de poignée par fonction, plutôt que la possibilité de créer différentes réponses à différentes erreurs tout au long des processus d'exécution de la fonction. Cela semble être une vraie faiblesse.

Je me demande également si nous oublions le besoin critique de développer des harnais de test dans nos systèmes également. Considérer comment nous allons exercer les chemins d'erreur pendant les tests devrait faire partie de la discussion, mais je ne le vois pas non plus,

@sdwarwick Je ne pense pas que ce soit le meilleur endroit pour discuter du projet de conception décrit sur https://go.googlesource.com/proposal/+/master/design/go2draft-error-handling.md . Une meilleure approche consiste à ajouter un lien vers un article sur la page wiki à l' adresse https://github.com/golang/go/wiki/Go2ErrorHandlingFeedback .

Cela dit, ce projet de conception autorise plusieurs blocs de poignée dans une fonction.

Cette question a commencé comme une proposition spécifique. Nous n'allons pas adopter cette proposition. Il y a eu beaucoup de grandes discussions sur cette question, et j'espère que les gens tireront les bonnes idées dans des propositions séparées et dans des discussions sur le récent projet de conception. Je vais clore ce sujet. Merci pour toutes les discussions.

Si parler dans l'ensemble de ces exemples :

r, err := os.Open(src)
    if err != nil {
        return err
    }

Que je voudrais écrire en une ligne environ ainsi :

r, err := os.Open(src) try ("blah-blah: %v", err)

Au lieu de "essayer", mettez n'importe quel mot beau et approprié.

Avec une telle syntaxe, l'erreur serait renvoyée et le reste serait des valeurs par défaut selon le type. Si je dois revenir avec une erreur et quelque chose d'autre de spécifique, plutôt que par défaut, alors personne n'annule l'option classique plus multiligne.

Encore plus brièvement (sans ajouter une sorte de gestion des erreurs) :

r, err := os.Open(src) try

)
PS Excusez-moi pour mon anglais))

Ma variante :

func CopyFile(src, dst string) string, error {
    r := check os.Open(src) // return nil, error
    defer r.Close()

    // if error: run 1 defer and retun error message
    w := check os.Create(dst) // return nil, error
    defer w.Close()

    // if error: run 2, 1 defer and retun error message
    if check io.Copy(w, r) // return nil, error

}

func StartCopyFile() error {
  res := check CopyFile("1.txt", "2.txt")

  return nil
}

func main() {
  err := StartCopyFile()
  if err!= nil{
    fmt.printLn(err)
  }
}

Bonjour,

J'ai une idée simple, qui est vaguement basée sur le fonctionnement de la gestion des erreurs dans le shell, tout comme l'était la proposition initiale. Dans le shell, les erreurs sont communiquées par des valeurs de retour différentes de zéro. La valeur de retour de la dernière commande/appel est stockée dans $? dans la coquille. En plus du nom de variable donné par l'utilisateur, nous pourrions automatiquement stocker la valeur d'erreur du dernier appel dans une variable prédéfinie et la faire vérifier par une syntaxe prédéfinie. J'ai choisi ? comme syntaxe pour référencer la dernière valeur d'erreur, qui a été renvoyée à partir d'un appel de fonction dans la portée actuelle. J'ai choisi ! comme raccourci pour si ? != néant {}. Le choix pour ? est influencée par la coque, mais aussi parce qu'elle semble avoir du sens. Si une erreur se produit, vous êtes naturellement intéressé par ce qui s'est passé. Cela soulève une question. ? est le signe commun pour une question posée et nous l'utilisons donc pour référencer la dernière valeur d'erreur qui a été générée dans la même portée.
! est utilisé comme raccourci pour si ? != nil, car cela signifie qu'il faut faire attention en cas de problème. ! signifie : si quelque chose ne va pas, faites-le. ? référence la dernière valeur d'erreur. Comme d'habitude la valeur de ? est égal à zéro s'il n'y a pas eu d'erreur.

val, err := someFunc(param)
! { return &SpecialError("someFunc", param, ?) }

Pour rendre la syntaxe plus attrayante, je permettrais de placer le ! ligne directement derrière l'appel ainsi qu'en omettant les accolades.
Avec cette proposition, vous pouvez également gérer les erreurs sans utiliser d'identifiant défini par le programmeur.

Cela serait autorisé :

val, _ := someFunc(param)
! return &SpecialError("someFunc", param, ?)

Cela serait autorisé

val, _ := someFunc(param) ! return &SpecialError("someFunc", param, ?)

Dans cette proposition, vous n'avez pas à revenir de la fonction lorsqu'une erreur se produit
et vous pouvez à la place essayer de récupérer de l'erreur.

val, _ := someFunc(param)
! {
val, _ := someFunc(paramAlternative)
  !{ return &SpecialError("someFunc alternative try failed too", paramAlternative, ?) }}

Sous cette proposition, vous pourriez utiliser ! dans une boucle for pour plusieurs tentatives comme celle-ci.

val, _ := someFunc(param)
for i :=0; ! && i <5; i++ {
  // Sleep or make a change or both
  val, _ := someFunc(param)
} ! { return &SpecialError("someFunc", param, ? }

J'en suis conscient ! est principalement utilisé pour la négation d'expressions, de sorte que la syntaxe proposée pourrait causer de la confusion chez les non-initiés. L'idée c'est ça ! par lui-même s'étend à ? != nil lorsqu'il est utilisé dans une expression conditionnelle dans un cas comme le montre l'exemple supérieur, où il n'est attaché à aucune expression spécifique. La ligne supérieure for ne peut pas être compilée avec le go actuel, car elle n'a aucun sens sans contexte. Sous cette proposition ! en soi est vrai, lorsqu'une erreur s'est produite dans l'appel de fonction le plus récent, cela peut renvoyer une erreur.

L'instruction return pour renvoyer l'erreur est conservée, car comme d'autres l'ont commenté ici, il est souhaitable de voir en un coup d'œil où votre fonction retourne. Vous pouvez utiliser cette syntaxe dans un scénario où une erreur ne vous oblige pas à quitter la fonction.

Cette proposition est plus simple que d'autres, car il n'y a aucun effort pour créer une variante du bloc try and catch comme la syntaxe connue dans d'autres langages. Il maintient la philosophie actuelle de go consistant à gérer les erreurs directement là où elles se produisent et le rend plus succinct.

@tobimensch s'il vous plaît poster de nouvelles suggestions à un point essentiel, et lier cela dans le wiki de retour d'information sur la gestion des erreurs Go 2 . Les publications sur ce problème clos peuvent être ignorées.

Si vous ne l'avez pas vu, vous pouvez lire le Go 2 Error Handling Draft Design .

Et vous pourriez être intéressé par les exigences à prendre en compte pour la gestion des erreurs Go 2 .

Il est peut-être un peu trop tard pour le souligner, mais tout ce qui ressemble à de la magie javascript me dérange. Je parle de l'opérateur || qui devrait fonctionner comme par magie avec une interface error . Je n'aime pas ça.

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