- La verbosité de la gestion des erreurs en Go figure depuis longtemps parmi les principales sources d’insatisfaction des utilisateurs
- Diverses propositions d’amélioration syntaxique (par ex.
check/handle, try, l’opérateur ?, etc.) ont été discutées et testées, mais toutes ont été rejetées faute d’un consensus suffisant dans la communauté
- L’impact très large d’un changement de langage sur le code, les outils, la documentation, etc., ainsi que le principe propre à Go de préserver la simplicité, constituent des considérations majeures
- En raison de la clarté de l’approche actuelle, de sa facilité pour le débogage et de la préférence de certains utilisateurs, les arguments en faveur d’un changement syntaxique restent limités
- Aucun changement de syntaxe pour la gestion des erreurs n’est prévu dans un avenir prévisible, et les propositions associées seront toutes closes sans étude supplémentaire
Mise en avant du problème de verbosité de la gestion des erreurs en Go
- L’une des plaintes historiques concernant Go est que la syntaxe de gestion des erreurs est excessivement verbeuse
- En particulier, des motifs comme
if err != nil se répètent partout dans le code
- Plus un programme nécessite d’appels d’API, plus cette syntaxe devient visible, au point que le code de gestion des erreurs peut dépasser la logique métier elle-même
- Dans les enquêtes annuelles auprès des utilisateurs, cette plainte revient constamment parmi les premières
Concertation avec la communauté et premières propositions
- L’équipe Go accorde de l’importance aux retours de la communauté et a poursuivi ses recherches sur des améliorations de la gestion des erreurs
- Lors des discussions du projet Go 2 en 2018, Russ Cox a formalisé les points clés du problème de la gestion des erreurs
- La proposition de mécanisme
check et handle de Marcel van Lohuizen y figurait
- Elle incluait une analyse comparative avec des langages similaires et l’examen de diverses alternatives
- Cette approche rendait effectivement le code plus concis, mais n’a pas été adoptée en raison de la complexité supplémentaire qu’elle introduisait
La proposition try et la suite
- En 2019, une proposition bien plus simple de fonction intégrée
try a été formulée
- Elle n’implémentait en code que la fonctionnalité de
check, en omettant handle
- La proposition a été critiquée pour le fait qu’elle masquait le flux de contrôle, puis abandonnée face à la réaction négative de la communauté
- Cette expérience a mis en évidence le risque des propositions trop abouties sans retour suffisant en amont
- Elle a confirmé que, pour des changements d’ampleur, il est crucial de recueillir des avis plus larges dès les premières étapes de conception
Tentatives supplémentaires et diversité des propositions
- De nombreuses variantes et approches alternatives de gestion des erreurs ont continué à apparaître dans la communauté
- Ian Lance Taylor a récapitulé la situation via une umbrella issue, tandis que le Go Wiki, des blogs et d’autres sources ont continué à collecter des exemples
- En 2024, une proposition visant à appliquer l’opérateur
? emprunté à Rust a été avancée
- De petits tests d’utilisabilité ont suggéré qu’il était intuitif, mais là encore, aucun consensus n’a émergé malgré la diversité des avis
Blocage des discussions et conclusion
- Malgré plus de trois propositions officielles ou semi-officielles et des centaines de propositions communautaires, toutes ont été rejetées faute d’un accord ou d’un consensus suffisant
- Même le groupe des architectes internes de Go ne partage pas de position commune sur l’orientation à suivre
- Il a donc été décidé de suspendre toute tentative de modification de la syntaxe de gestion des erreurs jusqu’à un éventuel changement de contexte ou à l’émergence d’un consensus particulier
Principaux arguments en faveur du maintien de l’approche actuelle
- Si un sucre syntaxique avait été introduit dès la conception initiale du langage, la controverse n’aurait peut-être pas eu lieu, mais aujourd’hui l’écosystème est habitué à une approche utilisée depuis 15 ans
- L’introduction d’une nouvelle syntaxe ferait inévitablement craindre un écart de style entre anciens et nouveaux utilisateurs ainsi qu’une perte de cohérence
- Cela correspond aussi à la philosophie de conception de Go (ne pas faire la même chose de plusieurs façons) et à son attachement à la simplicité et à la cohérence
- L’autorisation de redéclaration avec la déclaration courte de variable (
:=) est elle aussi un changement secondaire né des besoins liés à la gestion des erreurs
- Une syntaxe explicite de gestion des erreurs (via
if) présente des avantages intuitifs pour la lecture du code, le débogage et le placement de points d’arrêt
- Un changement de langage représente également une charge importante en raison de son ampleur et de son coût réels (code, documentation, outils, etc.)
Améliorations alternatives et orientation future
- Le renforcement de la bibliothèque standard (par ex. l’introduction de
cmp.Or) peut réduire une partie du code répétitif
- Les IDE et outils de développement, avec le pliage de code, l’autocomplétion, l’usage des LLM, etc., permettent en pratique d’atténuer en partie cette verbosité
- Parmi les principaux groupes d’utilisateurs de Go (par ex. les participants à Google Cloud Next), l’opinion dominante est défavorable à un changement du langage
- Plus on utilise Go, moins ce problème de verbosité est ressenti dans la pratique
Arguments en faveur de la nécessité d’une amélioration syntaxique
- D’après les retours des utilisateurs, une demande d’amélioration de la syntaxe de gestion des erreurs subsiste bel et bien
- Une syntaxe de gestion des erreurs qui n’apporte pas seulement moins de caractères, mais aussi plus de clarté, pourrait contribuer à améliorer la qualité et la sûreté du code
- Des recherches plus fines sont nécessaires sur la gestion des erreurs qui joue un rôle réel, au-delà de la simple vérification d’erreur
Conclusion finale et politique à venir
- Constatant l’absence persistante de consensus ou de changement concret à ce jour, il est déclaré que toutes les discussions et propositions de changements syntaxiques du langage pour la gestion des erreurs sont interrompues pour un avenir prévisible
- Les discussions et recherches menées jusqu’ici ont indirectement contribué à améliorer l’écosystème Go et ses processus
- Si, à l’avenir, une définition plus claire du problème et un consensus plus net devaient émerger, les discussions pourraient reprendre
- Pour le moment, la priorité restera de préserver la robustesse et la simplicité de Go plutôt que de tenter de nouvelles approches
1 commentaires
Commentaire sur Hacker News
Si vous voulez facilement suggérer que l’équipe Go aurait pu choisir d’autres alternatives, j’aimerais vraiment que vous consultiez d’abord le wiki Go2ErrorHandlingFeedback ou la recherche d’issues GitHub. Presque toutes les idées proposées ont déjà été discutées sérieusement, et en tant qu’utilisateur reconnaissant de l’approche transparente de l’équipe Go, j’apprécie chaque jour d’utiliser Go
Le brouillon de conception mentionne C++, Rust et Swift, mais j’ai du mal à y trouver des choses comme la do-notation / for-comprehensions / monadic-let des langages fonctionnels tels que Haskell, Scala ou OCaml. L’équipe Go donne l’impression d’être passée maître dans la conception de langages, mais se retrouve en réalité bloquée sur le problème de la gestion des erreurs à cause des limites d’un typage statique sans polymorphisme paramétrique, comme en Java. À mon avis, c’est un problème qui vient de la conception fondamentale du langage
Bien que le document ait été rédigé par des personnes intelligentes et expérimentées, je trouve très surprenant qu’aucune solution comme les monades Maybe/Either de Haskell et l’opérateur bind (do-notation) ne soit mentionnée nulle part. En pratique, ce n’est ni difficile ni pédant, et c’est une manière très élégante et éprouvée de propager les erreurs en toute sécurité. Je ne comprends pas pourquoi la communauté Go n’a pas essayé d’intégrer cela. Je suis reconnaissant que cette page existe, mais ignorer une solution aussi connue est difficile à comprendre
Presque tous les langages proposent diverses approches meilleures, alors je me demande pourquoi le problème ressort autant uniquement avec Go. Est-ce simplement qu’aucun consensus ne se dégage, ou bien y a-t-il une caractéristique propre à Go qui fait que les solutions des autres langages ne conviennent pas ?
On voit souvent, dans les critiques de Go, une tendance de personnes relativement peu expertes à supposer que les développeurs Go comprennent moins bien les langages qu’eux. En réalité, dans la plupart des cas, les développeurs Go ont au contraire bien plus d’expérience et en savent bien davantage. Les non-spécialistes ont tendance à penser qu’un langage avec plus de fonctionnalités est forcément meilleur, en oubliant que ce qui compte vraiment, c’est l’équilibre global
Je pense que les utilisateurs bénéficient du conservatisme de Go quand il s’agit d’ajouter de nouvelles fonctionnalités au langage. Dans le cas de Swift, les changements de fonctionnalités sont si nombreux qu’il est difficile à apprendre, et même sur un Mac récent il arrive souvent qu’un projet pourtant simple ne compile pas. Comme les mots-clés continuent d’augmenter et de changer, Swift perd en continuité d’usage, tandis que Go a pour force sa constance
Une fois, dans une fonction Go, je me suis trouvé dans une situation exceptionnelle où une fonction interne était censée produire une erreur, et si elle n’en produisait pas, la fonction appelante devait au contraire considérer cela comme une erreur. Dans cette structure peu courante, il fallait brancher sur
if err == nil, et par habitude j’ai écritif err != nil, ce qui m’a fait perdre beaucoup de temps avant de repérer l’erreur tant j’étais conditionné par le schéma habituel. Je me suis dit qu’un support syntaxique au niveau du langage pour distinguer le fréquentif err != nildu rareif err == nilaurait peut-être permis de réduire ce genre d’erreurif err == nil, j’ajoute un commentaire// invertedpour mettre en évidence le motif. Ce serait bien que le langage s’en charge automatiquement, mais pour l’instant cela permet au moins de rendre la différence plus visibleif err == nil { return ... }pourrait au contraire paraître plus maladroit dans le code. Beaucoup de gens préfèrent la manière actuelle de gérer les erreurs en Go parce qu’elle est claire et facile à lireif fruit != "Apple", donc au fond ce n’est pas uniquement un problème propre à la gestion des erreurs, mais plus généralement un problème de branchement sur l’état. Une erreur est finalement traitée comme n’importe quelle autre valeur d’étatif err != nilcomme un symbole spécial pour qu’il se fonde naturellement dans l’arrière-plan et attire moins l’attention, et de faire ressortir uniquement le motif différentif err == nil, afin de réduire les erreurs au niveau de l’éditeurif err … {J’aime bien la gestion explicite des erreurs de Go. Je comprends simplement qu’une fonction réussit toujours (minimal error) ou peut échouer. Une fonction susceptible d’échouer doit impérativement être traitée avant de passer à l’étape suivante. Dans beaucoup de langages, les erreurs sont levées sous forme d’exceptions et remontent la pile jusqu’à être capturées, ce qui, selon moi, indique seulement où l’erreur s’est produite sans donner d’indice réellement utile. En Go, on dispose clairement des options suivantes : 1) ignorer l’erreur 2) retourner immédiatement en cas d’erreur 3) envelopper l’erreur pour ajouter des informations utiles 4) interpréter une erreur précise pour effectuer un traitement conditionnel (par exemple la convertir en 404). Dans Go2, j’aimerais essayer d’ajouter un type
Result<Value, Failure>ou des types d’erreurs plus spécifiques et énumérables. Je pense qu’il serait plus approprié de l’introduire dans Go 2 pour préserver la compatibilité avec Go 1Au début, je n’aimais pas beaucoup la manière dont Go gère les erreurs, mais après avoir lu le billet de blog errors-are-values et commencé à utiliser
panic(err)aux endroits appropriés, j’en suis au contraire venu à en être très satisfait. Pour les états anormaux que le code parent ne doit pas traiter directement, l’utilisation de panic m’a permis de réduire fortement toutes les branches de gestion d’erreur parasites. Cette manière de gérer les erreurs m’aide beaucoup dans le travail réel-epour gérer les erreurstry/catch/finallyen C#, je l’ai trouvé original, mais aujourd’hui je préfère au contraire une logique simple comme celle de Go. Même avec un volume de code plus élevé (Loc), je considère que la clarté du flux de code est un avantage réelÀ l’affirmation selon laquelle, dès qu’on traite réellement les erreurs, la verbosité disparaît vite, on peut répondre de façon amusée en se demandant si générer manuellement une stack trace constitue vraiment un « traitement ». Selon cette définition de Go, une exception ne serait-elle pas elle aussi une forme de traitement ?
Je n’aime pas que cet article réduise le problème de la gestion des erreurs en Go à la simple idée que « la syntaxe est verbeuse ». À mon avis, les vrais problèmes sont plutôt : 1) les erreurs peuvent facilement être omises silencieusement ou ignorées par inadvertance 2) on ne peut pas facilement transmettre ou stocker le résultat d’une fonction comme une valeur 3) des erreurs imbriquées comme avec
errors.Iss’articulent maladroitement avec le système de types 4) il est difficile de faire du switching sur les erreurs 5) les sentinel values sont très présentes dans la bibliothèque standard 6) l’articulation avec les génériques est mauvaise, au point de créer un besoin de packages dédiésEn Elixir (et Erlang), une fonction renvoie généralement un tuple
{:ok, result}ou{:error, description}. Grâce à la syntaxewithd’Elixir, on peut regrouper la gestion des erreurs en bas du bloc, ce qui améliore beaucoup la lisibilité. Si Go adoptait quelque chose de proche d’unwith, on pourrait n’enchaîner l’exécution que lorsque l’erreur estnil, avec un bloc de gestion placé tout en bas, ce qui rendrait le code plus lisibleJe ne comprends pas pourquoi ils ne reprennent pas simplement le style de Rust. Surtout maintenant qu’il y a les génériques, quelque chose de proche pourrait être implémenté rapidement. Je ne trouve pas convaincant l’argument selon lequel l’opérateur
?de Rust serait pratique mais encouragerait à ignorer les erreurs. En pratique, Go laisse très souvent passer des valeurs de retour d’erreur ignorées sans produire d’erreur de compilation. Pour vraiment éviter les erreurs humaines, il faudrait imposer un retour de type Result comme en Rust. Si cela devient controversé au nom de la commodité, alors il faudrait aussi interdire panic, non ?Resultparce qu’il n’a pas de sum types et qu’il repose sur une conception particulière où tous les types doivent avoir une zero value?» conduirait à « ne plus utiliser d’erreurs enrichies », d’autres répondent qu’au contraire on peut concevoir une telle fonctionnalité de manière à encourager le wrappingJe pense qu’il ne faut pas discuter de l’adoption des fonctionnalités comme on coche des cases à la manière de Rust ; un langage doit être conçu dans une cohérence d’ensemble. Le fait d’avoir coché toute une liste de fonctionnalités ne signifie pas automatiquement qu’elles correspondent à la nature profonde du langage