Proposition: essayer - fonction intégrée de vérification des erreurs

Résumé


Une nouvelle construction try est proposée, spécialement conçue pour éliminer les expressions -f couramment associées à la gestion des erreurs dans Go. C'est le seul changement de langue. Les auteurs prennent en charge l'utilisation des fonctions de bibliothèque defer et standard pour enrichir ou envelopper les erreurs. Cette petite extension convient à la plupart des scénarios, pratiquement sans compliquer la langue.


La construction try est facile à expliquer, facile à implémenter, cette fonctionnalité est orthogonale à d'autres constructions de langage et est entièrement rétrocompatible. Il est également extensible si nous le voulons à l'avenir.


Le reste de ce document est organisé comme suit: après une brève introduction, nous donnons une définition de la fonction intégrée et expliquons son utilisation dans la pratique. La section de discussion passe en revue les suggestions alternatives et la conception actuelle. À la fin, les conclusions et le plan de mise en œuvre avec des exemples et une section de questions et réponses seront fournis.


Présentation


Lors de la dernière conférence Gophercon à Denver, les membres de l'équipe Go (Russ Cox, Marcel van Lohuizen) ont présenté de nouvelles idées sur la façon de réduire la fastidieuse gestion manuelle des erreurs dans Go ( projet de conception ). Depuis lors, nous avons reçu une énorme quantité de commentaires.


Comme Russ Cox l'a expliqué dans son examen du problème , notre objectif est de rendre la gestion des erreurs plus légère en réduisant la quantité de code consacrée spécifiquement à la vérification des erreurs. Nous voulons également rendre l'écriture du code de gestion des erreurs plus pratique, augmentant la probabilité que les développeurs consacrent toujours du temps à la correction de la gestion des erreurs. Dans le même temps, nous voulons laisser le code de gestion des erreurs clairement visible dans le code du programme.


Les idées discutées dans l'avant-projet se concentrent autour du nouvel opérateur de check unaire, ce qui simplifie la vérification explicite de la valeur d'erreur obtenue à partir d'une expression (généralement un appel de fonction), ainsi que la déclaration des gestionnaires d'erreur ( handle ) et un ensemble de règles reliant ces deux nouvelles constructions de langage.


La plupart des commentaires que nous avons reçus portaient sur les détails et la complexité de la conception de la handle , et l'idée d'un opérateur de check s'est avérée plus attrayante. En fait, plusieurs membres de la communauté ont pris l'idée d'un opérateur de check et l'ont élargie. Voici quelques articles les plus similaires à notre offre:


  • La première proposition écrite (connue de nous) d'utiliser la construction de check place de l'opérateur a été proposée par PeterRK dans son article Éléments clés de la gestion des erreurs
  • Il n'y a pas si longtemps, Markus a proposé deux nouveaux mots clés, guard and must ainsi que l'utilisation de la fonction de defer pour envelopper les erreurs dans # 31442.
  • Pjebs a également suggéré une construction must dans # 32219

La proposition actuelle, bien que différente en détail, était fondée sur ces trois éléments et, en général, sur les commentaires reçus sur le projet de conception proposé l'année dernière.


Pour compléter l'image, nous voulons noter que vous trouverez encore plus de suggestions de gestion des erreurs sur cette page wiki . Il convient également de noter que Liam Breck est venu avec un ensemble complet d' exigences pour le mécanisme de gestion des erreurs.


Enfin, après la publication de cette proposition, nous avons appris que Ryan Hileman a implémenté try il y a cinq ans à l'aide de l' outil de réécriture og et l'a utilisé avec succès dans des projets réels. Voir ( https://news.ycombinator.com/item?id=20101417 ).


Fonction d'essai intégrée


Offrir


Nous suggérons d'ajouter un nouvel élément de langage de type fonction appelé try et appelé avec une signature


 func try(expr) (T1, T2, ... Tn) 

expr signifie une expression d'un paramètre d'entrée (généralement un appel de fonction) qui renvoie n + 1 valeurs de types T1, T2, ... Tn et error pour la dernière valeur. Si expr est une valeur unique (n = 0), cette valeur doit être de type error et try ne renvoie pas de résultat. L'appel de try avec une expression qui ne renvoie pas la dernière valeur de type error entraîne une erreur de compilation.


La construction try ne peut être utilisée que dans une fonction qui renvoie au moins une valeur et dont la dernière valeur de retour est de type error . L'appel de try dans d'autres contextes entraîne une erreur de compilation.


Appelez la fonction try avec f() comme dans l'exemple


 x1, x2, … xn = try(f()) 

conduit au code suivant:


 t1, … tn, te := f() // t1, … tn,  ()   if te != nil { err = te //  te    error return //     } x1, … xn = t1, … tn //     //     

En d'autres termes, si le dernier type d' error renvoyé par expr est nil , try simplement de renvoyer les n premières valeurs, en supprimant le nil final.


Si la dernière valeur renvoyée par expr n'est pas nil , alors:


  • La valeur de retour d' error de la fonction englobante (dans le pseudocode ci-dessus nommé err , bien qu'il puisse s'agir de n'importe quel identifiant ou valeur de retour sans nom) reçoit la valeur d'erreur retournée par expr
  • il y a une sortie de la fonction enveloppante
  • si la fonction englobante a des paramètres de retour supplémentaires, ces paramètres conservent les valeurs qui y étaient contenues avant l'appel try .
  • si la fonction englobante a des paramètres de retour supplémentaires sans nom, les valeurs zéro correspondantes sont renvoyées pour eux (ce qui est identique à l'enregistrement de leurs valeurs zéro d'origine avec lesquelles ils sont initialisés).

Si try utilisé dans plusieurs affectations, comme dans l'exemple ci-dessus, et qu'une erreur non nulle (ci-après non nulle - environ Per.) Est détectée, l'affectation (par les variables utilisateur) n'est pas effectuée et aucune des variables sur le côté gauche de l'affectation ne change. Autrement dit, try se comporte comme un appel de fonction: ses résultats ne sont disponibles que si try renvoie le contrôle à l'appelant (contrairement au cas avec un retour de la fonction englobante). Par conséquent, si les variables sur le côté gauche de l'affectation sont des paramètres de retour, l'utilisation de try entraînera un comportement différent du code typique rencontré actuellement. Par exemple, si a,b, err sont nommés paramètres de retour d'une fonction englobante, voici ce code:


 a, b, err = f() if err != nil { return } 

affectera toujours des valeurs aux variables a, b et err , que l'appel à f() renvoyé une erreur ou non. Défi contraire


 a, b = try(f()) 

en cas d'erreur, laissez a et b inchangés. Malgré le fait qu'il s'agit d'une nuance subtile, nous pensons que de tels cas sont assez rares. Si un comportement d'affectation inconditionnel est requis, vous devez continuer à utiliser les expressions if .


Utiliser


La définition de try vous indique explicitement comment l'utiliser: de nombreuses expressions if qui vérifient les retours d'erreur peuvent être remplacées par try . Par exemple:


 f, err := os.Open(filename) if err != nil { return …, err //       } 

peut être simplifié en


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

Si la fonction appelante ne renvoie pas d'erreur, try ne peut pas être utilisé (voir la section Discussion). Dans ce cas, l'erreur doit dans tous les cas être traitée localement (puisqu'il n'y a pas de retour d'erreur), et dans ce cas, if reste le mécanisme approprié pour vérifier les erreurs.


De manière générale, notre objectif n'est pas de remplacer toutes les vérifications d'erreur possibles par une try . Le code qui nécessite une sémantique différente peut et doit continuer à utiliser des expressions if et des variables explicites avec des valeurs d'erreur.


Tester et essayer


Dans l'une de nos tentatives précédentes pour écrire une spécification (voir la section d'itération de conception ci-dessous), try été conçu pour paniquer lorsqu'une erreur se produit lorsqu'il est utilisé à l'intérieur d'une fonction sans erreur de retour. Cela a permis d'utiliser des tests unitaires d'essai basés sur le package de testing de la bibliothèque standard.


Comme l'une des options, il est possible d'utiliser des fonctions de test avec des signatures dans le package de testing


 func TestXxx(*testing.T) error func BenchmarkXxx(*testing.B) error 

afin de permettre l'utilisation de try dans les tests. Une fonction de test qui renvoie une erreur non nulle appellera implicitement t.Fatal(err) ou b.Fatal(err) . Il s'agit d'un petit changement de bibliothèque qui évite d'avoir à try différents comportements (retour ou panique), selon le contexte.


L'un des inconvénients de cette approche est que t.Fatal et b.Fatal ne pourront pas retourner le numéro de ligne sur lequel le test est tombé. Un autre inconvénient est que nous devons également modifier les sous-tests. La solution à ce problème est une question ouverte; nous ne proposons pas de modifications spécifiques au package de testing dans ce document.


Voir aussi # 21111 , qui suggère d'autoriser les exemples de fonctions à renvoyer une erreur.


Gestion des erreurs


Le projet de conception original concernait en grande partie la prise en charge linguistique des erreurs d'emballage ou d'augmentation. Le projet proposait un nouveau descripteur de mot clé et une nouvelle façon de déclarer les gestionnaires d'erreurs . Cette nouvelle construction de langage a attiré des problèmes comme les mouches en raison d'une sémantique non triviale, en particulier lorsque l'on considère son effet sur le flux d'exécution. En particulier, la fonctionnalité de la handle lamentablement croisée avec la fonction de defer , ce qui a rendu le nouveau langage non orthogonal à tout le reste.


Cette proposition réduit le projet de conception original à son essence. Si un enrichissement ou un habillage d'erreur est requis, il existe deux approches: attacher à if err != nil { return err} , ou "déclarer" un gestionnaire d'erreur dans l'expression defer :


 defer func() { if err != nil { //      -   err = … // /  } }() 

Dans cet exemple, err est le nom du paramètre de retour de type error fonction englobante.


Dans la pratique, nous imaginons des fonctions d'aide telles que


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

ou quelque chose de similaire. Le paquet fmt peut devenir un endroit naturel pour de tels assistants (il fournit déjà fmt.Errorf ). À l'aide d'aides, la définition d'un gestionnaire d'erreurs sera dans de nombreux cas réduite à une seule ligne. Par exemple, pour enrichir l'erreur de la fonction "copier", vous pouvez écrire


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

if fmt.HandleErrorf ajoute implicitement des informations d'erreur. Une telle construction est assez facile à lire et a l'avantage de pouvoir être implémentée sans ajouter de nouveaux éléments de la syntaxe du langage.


Le principal inconvénient de cette approche est que le paramètre d'erreur renvoyé doit être nommé, ce qui conduit potentiellement à une API moins précise (voir la FAQ sur ce sujet). Nous pensons que nous nous y habituerons lorsque le style approprié d'écriture du code sera établi.


Différer l'efficacité


Une considération importante lors de l'utilisation de defer comme gestionnaire d'erreurs est l'efficacité. L'expression de defer est considérée comme lente . Nous ne voulons pas choisir entre un code efficace et une bonne gestion des erreurs. Indépendamment de cette proposition, les équipes d'exécution et de compilation de Go ont discuté de méthodes d'implémentation alternatives, et nous pensons que nous pouvons trouver des moyens typiques d'utiliser le report pour gérer les erreurs d'une efficacité comparable au code "manuel" existant. Nous espérons ajouter une implémentation plus rapide du defer dans Go 1.14 (voir également le ticket CL 171158 , qui est la première étape dans cette direction).


Cas particuliers go try(f), defer try(f)


La construction try ressemble à une fonction, et à cause de cela, il est prévu qu'elle puisse être utilisée partout où un appel de fonction est acceptable. Cependant, si l'appel try est utilisé dans l'instruction go , les choses se compliquent:


 go try(f()) 

Ici, f() est exécutée lorsque l'expression go est exécutée dans le goroutine actuel, les résultats de l'appel de f sont passés comme arguments à try , qui commence dans le nouveau goroutine. Si f renvoie une erreur non nulle, try devrait revenir de la fonction englobante; cependant, il n'y a pas de fonction (et il n'y a pas de paramètre de retour de type error ), car le code est exécuté dans un goroutine séparé. Pour cette raison, nous proposons de désactiver try dans une expression go .


Situation avec


 defer try(f()) 

semble similaire, mais ici la sémantique de defer signifie que l'exécution de try sera retardée jusqu'à ce qu'elle revienne de la fonction englobante. Comme précédemment, f() évalué lorsque le defer et ses résultats sont transmis à l' try différé.


try vérifie l'erreur f() renvoyée uniquement au tout dernier moment avant de revenir de la fonction englobante. Sans modifier le comportement d' try , une telle erreur peut remplacer une autre valeur d'erreur que la fonction englobante tente de renvoyer. Cela au mieux confond, au pire, cela provoque des erreurs. Pour cette raison, nous vous proposons d'interdire également d'appeler try dans l'instruction defer . Nous pouvons toujours reconsidérer cette décision s'il existe une application raisonnable d'une telle sémantique.


Enfin, comme le reste des constructions intégrées, try ne peut être utilisé qu'en tant qu'appel; il ne peut pas être utilisé comme fonction de valeur ou dans une expression d'affectation de variable comme dans f := try (tout comme f := print et f := new sont interdits).


La discussion


Itérations de conception


Ce qui suit est une brève discussion des conceptions antérieures qui ont conduit à la proposition minimale actuelle. Nous espérons que cela éclairera certaines décisions de conception.


Notre première itération de cette phrase a été inspirée par deux idées de l'article «Parties clés de la gestion des erreurs», à savoir l'utilisation de la fonction intégrée au lieu de l'opérateur et de la fonction Go habituelle pour gérer les erreurs au lieu de la nouvelle construction de langage. Contrairement à cette publication, notre gestionnaire d'erreurs avait une func(error) error signature fixe pour simplifier les choses. Un gestionnaire d'erreur serait appelé par la fonction try s'il y avait une erreur avant que try ne quitte la fonction englobante. Voici un exemple:


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

Bien que cette approche ait permis de définir des gestionnaires d'erreurs définis par l'utilisateur efficaces, elle a également soulevé de nombreuses questions qui n'avaient manifestement pas les bonnes réponses: que devrait-il se passer si rien est transmis au gestionnaire? Devriez-vous try paniquer ou de considérer cela comme un manque de gestionnaire? Que faire si le gestionnaire est appelé avec une erreur non nulle et renvoie ensuite un résultat nul? Est-ce à dire que l'erreur est "annulée"? Ou une fonction englobante doit-elle renvoyer une erreur vide? Il y avait également des doutes que le transfert facultatif d'un gestionnaire d'erreurs encouragerait les développeurs à ignorer les erreurs au lieu de les corriger. Il serait également facile de faire la gestion des erreurs correcte partout, mais ignorez une utilisation de try . Et similaires.


Dans l'itération suivante, la possibilité de passer un gestionnaire d'erreurs personnalisé a été supprimée au profit de l'utilisation de la fonction de defer pour encapsuler les erreurs. Cela semblait être une meilleure approche car cela rendait les gestionnaires d'erreurs beaucoup plus visibles dans le code source. Cette étape a également éliminé tous les problèmes concernant le transfert facultatif des fonctions de gestionnaire, mais a exigé que les paramètres renvoyés avec le type d' error soient nommés si l'accès était requis (nous avons décidé que c'était normal). De plus, pour essayer de rendre try utile non seulement dans les fonctions qui renvoient des erreurs, il était nécessaire de rendre le comportement de try sensible au contexte: si try utilisé au niveau du package, ou s'il a été appelé dans une fonction qui ne renvoie pas d'erreur, try automatiquement de paniquer lorsqu'une erreur a été détectée. (Et comme effet secondaire, en raison de cette propriété, la construction du langage a été appelée must au lieu de try dans cette phrase.) Le comportement contextuel de try (ou must ) semblait naturel et également très utile: il éliminerait de nombreuses fonctions définies par l'utilisateur utilisées dans les expressions initialisation des variables de package. Il a également ouvert la possibilité d'utiliser des tests unitaires d'essai avec le package de testing .


Cependant, le comportement contextuel de try était lourd d'erreurs: par exemple, le comportement d'une fonction utilisant try pouvait changer discrètement (panique ou non) lors de l'ajout ou de la suppression d'une erreur de retour à la signature de la fonction. Cela semblait une propriété trop dangereuse. La solution évidente était de diviser la fonctionnalité try en deux fonctions must et try distinctes, (très similaires à la manière suggérée dans # 31442 ). Cependant, cela nécessiterait deux fonctions intégrées, tandis que seul l' try directement lié à une meilleure prise en charge de la gestion des erreurs.


Par conséquent, dans l'itération actuelle, au lieu d'inclure la deuxième fonction intégrée, nous avons décidé de supprimer la double sémantique de try et, par conséquent, d'autoriser son utilisation uniquement dans les fonctions qui renvoient une erreur.


Caractéristiques de la conception proposée


Cette suggestion est assez courte et peut sembler un pas en arrière par rapport au projet de l'an dernier. Nous pensons que les solutions sélectionnées sont justifiées:


  • Tout d'abord, try a exactement la même sémantique de l'instruction de check proposée dans l'original sans handle . Cela confirme la fidélité du projet original dans l'un des aspects importants.


  • Le choix d'une fonction intégrée au lieu d'opérateurs présente plusieurs avantages. Il ne nécessite pas de nouveau mot clé comme check , ce qui rendrait la conception incompatible avec les analyseurs existants. Il n'est pas non plus nécessaire d'étendre la syntaxe des expressions avec un nouvel opérateur. L'ajout d'une nouvelle fonction intégrée est relativement trivial et complètement orthogonal aux autres caractéristiques du langage.


  • L'utilisation d'une fonction en ligne au lieu d'un opérateur nécessite l'utilisation de parenthèses. Nous devrions écrire try(f()) au lieu de try f() . C'est le (petit) prix que nous devons payer pour la rétrocompatibilité avec les analyseurs existants. Cependant, cela rend également la conception compatible avec les versions futures: si nous décidons en cours de route que passer sous une forme ou une autre une fonction de gestion des erreurs ou ajouter un paramètre supplémentaire à try à cette fin est une bonne idée, ajouter un argument supplémentaire à l'appel try sera trivial.


  • Il s'est avéré que la nécessité d'écrire des parenthèses a ses avantages. Dans les expressions plus complexes avec plusieurs appels d' try , les parenthèses améliorent la lisibilité en éliminant la nécessité de gérer la priorité des opérateurs, comme dans les exemples suivants:



 info := try(try(os.Open(file)).Stat()) //   try info := try (try os.Open(file)).Stat() //  try   info := try (try (os.Open(file)).Stat()) //  try   

try , : try , .. try (receiver) .Stat ( os.Open ).


try , : os.Open(file) .. try ( , try os , , try try ).


, .. .


  • . , . , , , .

Conclusions


. , . defer , .


Go - , . , Go append . append , . , . , try .


, , Go : panic recover . error try .


, try , , — — , . Go:


  • , try
  • -

, , . if -.


Implémentation


:


  • Go.
  • try . , . .
  • go/types try . .
  • gccgo . ( , ).
  • .

- , . , . .


Robert Griesemer go/types , () cmd/compile . , Go 1.14, 1 2019.


, Ian Lance Taylor gccgo , .


"Go 2, !" , .


1 , , , Go 1.14 .



CopyFile :


 func CopyFile(src, dst string) (err error) { defer func() { if err != nil { err = fmt.Errorf("copy %s %s: %v", src, dst, err) } }() r := try(os.Open(src)) defer r.Close() w := try(os.Create(dst)) defer func() { w.Close() if err != nil { os.Remove(dst) //    “try”    } }() try(io.Copy(w, r)) try(w.Close()) return nil } 

, " ", defer :


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

( defer -), defer , .


printSum


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

:


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

main :


 func localMain() error { hex := try(ioutil.ReadAll(os.Stdin)) data := try(parseHexdump(string(hex))) try(os.Stdout.Write(data)) return nil } func main() { if err := localMain(); err != nil { log.Fatal(err) } } 

- try , :


 n, err := src.Read(buf) if err == io.EOF { break } try(err) 

Q & A


, .


: ?


: check handle , . , handle defer , handle .


: try ?


: try Go . - , . , . , " ". try , .. .


: try try?


: , check , must do . try , . try check (, ), - . . must ; try — . , Rust Swift try ( ). .


: ? Rust?


: Go ; , Go ( ; - ). , ? , . , , , (package, interface, if, append, recover, ...), , (struct, var, func, int, len, image, ..). Rust ? try — Go, , ( ) . , ? . , , (, ..) . . , .


: ( error) , defer , go doc. ?


: go doc , - ( _ ) , . , func f() (_ A, _ B, err error) go doc func f() (A, B, error) . , , , . , , . , , , -, (deferred) . Jonathan Geddes try() .


: defer ?


: defer . , , defer "" . . CL 171758 , defer 30%.


: ?


: , . , ( , ), . defer , . defer - https://golang.org/issue/29934 ( Go 2), .


: , try, error. , ?


: error ( ) , , nil . try . ( , . - ).


: Go , try ?


: try , try . super return -, try Go . try . .


: try , . ?


: try ; , . try ( ), . , if .


: , . try, defer . ?


: , . .


: try ( catch )?


: try — ("") , , ( ) . try ; . . "" . , . , try — . , , throw try-catch Go. , (, ), ( ) , . "" try-catch , . , , . Go . panic , .

Source: https://habr.com/ru/post/fr472758/


All Articles