Go n’est toujours pas bon
(blog.habets.se)- Plusieurs choix de conception du langage Go ont été faits de manière inutile ou en ignorant l’expérience accumulée auparavant
- Le problème de gestion de la portée des variables d’erreur rend la lisibilité du code et la recherche de bugs plus difficiles
- La double nature de
nil, l’utilisation mémoire, la portabilité du code et d’autres aspects révèlent une conception peu intuitive et déconnectée de la réalité - Les limites de l’instruction
deferainsi que la manière dont la bibliothèque standard gère les situations exceptionnelles compliquent la garantie de la sûreté face aux exceptions - Des problèmes accumulés comme la gestion mémoire et le traitement insuffisant de l’UTF-8 nuisent à long terme à la qualité des bases de code Go
Critique de long terme du langage Go
- Comme je l’ai expliqué dans de précédents billets (Why Go is not my favourite language, Go programs are not portable), je souligne depuis plus de dix ans plusieurs problèmes du langage Go
- En particulier, les choix de conception inutiles qui ignorent des bonnes pratiques déjà bien connues paraissent de plus en plus regrettables
Le manque d’intuition dans la portée des variables d’erreur
- La syntaxe de Go élargit inutilement la portée de la variable d’erreur (
err), ce qui augmente le risque d’erreurs- Dans le code d’exemple, la variable
errreste vivante pendant toute la fonction et est réutilisée, ce qui nuit à la lisibilité et à la maintenabilité du code - Même les développeurs expérimentés peuvent perdre du temps et être induits en erreur lors de la recherche de bugs à cause de ces questions de portée
- La syntaxe n’autorise pas de moyen approprié de limiter localement cette variable
- Dans le code d’exemple, la variable
Les deux formes de nil
- Dans Go, il existe une confusion car
nilne se comporte pas de la même manière selon qu’il s’agit d’un typeinterfaceou d’un type pointeur- Comme dans l’exemple ci-dessous, même si
s(pointeur) eti(interface) reçoivent tous deuxnil,s==iest évalué différemment, ce qui montre un comportement incohérent - C’est précisément le genre de problème que l’on cherche généralement à éviter dans la gestion de
null, et cela donne l’impression d’une conception insuffisamment réfléchie
- Comme dans l’exemple ci-dessous, même si
Les limites de la portabilité du code
- L’utilisation de commentaires pour la compilation conditionnelle est nettement inefficace du point de vue de la maintenance et de la portabilité
- Quiconque a déjà développé un logiciel réellement portable sait qu’une telle approche est lourde et source d’erreurs
- L’expérience accumulée historiquement en matière de portabilité du code et de cas pratiques a été ignorée
- Voir Go programs are not portable pour plus de détails
Le manque de clarté sur la propriété avec append
- La relation de propriété entre la fonction
appendet les slices n’est pas claire, ce qui rend le code difficile à prévoir- L’exemple montre qu’il est difficile de savoir à l’avance quel effet un
appendeffectué dans une fonctionfooaura réellement sur l’original - Le langage accumule ainsi des « quirks » qu’il faut connaître, ce qui favorise les erreurs
- L’exemple montre qu’il est difficile de savoir à l’avance quel effet un
Une conception inaboutie de defer
- Go ne fournit pas de support clair pour la libération des ressources, contrairement au principe RAII (Resource Acquisition Is Initialization)
- Par rapport aux structures de gestion des ressources de Java et Python, Go ne permet pas de savoir clairement quelles ressources doivent être libérées avec
defer - Comme le montre l’exemple des fichiers, il faut même gérer soi-même les problèmes de double fermeture, et l’ordre ainsi que la méthode corrects de libération restent flous
- Par rapport aux structures de gestion des ressources de Java et Python, Go ne permet pas de savoir clairement quelles ressources doivent être libérées avec
La gestion des exceptions dans la bibliothèque standard
- Go ne prend pas en charge les exceptions explicites, mais des situations exceptionnelles comme
panicexistent malgré tout- Dans certains cas,
panicne provoque pas un arrêt complet du programme et peut au contraire être absorbé - La bibliothèque standard (
fmt.Print, serveur HTTP, etc.) contient des schémas où les exceptions sont ignorées, ce qui rend impossible toute véritable garantie de sûreté face aux exceptions - En pratique, écrire du code sûr face aux exceptions reste indispensable, sans qu’il soit possible d’utiliser directement des exceptions
- Dans certains cas,
Le traitement de l’UTF-8 et des chaînes
- Même si l’on place des données binaires arbitraires dans le type
string, Go fonctionne sans validation particulière- Des noms de fichiers créés avant l’ère de l’encodage UTF-8 peuvent ainsi être silencieusement omis
- Lors de sauvegardes, cela peut entraîner la perte de données importantes, ce qui reflète une approche simpliste peu alignée avec la réalité du terrain
Les limites de la gestion mémoire
- Il est difficile de contrôler directement l’utilisation de la RAM, et la fiabilité du GC (garbage collector) a aussi ses limites
- La consommation mémoire de Go augmente, ce qui finit par créer des problèmes de coût et de performance
- Dans des environnements à multiples instances ou conteneurs, des problèmes réels de coût et de passage à l’échelle apparaissent
Conclusion : il existait de meilleures voies
- Alors qu’il existait déjà des conceptions de langage ayant fait leurs preuves, Go les a ignorées sur de nombreux points
- Contrairement aux problèmes des premières versions de Java, de meilleures approches existaient déjà au moment de la sortie de Go
Références
- Uber: Data race patterns in Go
- FasterThanLime: Lies we tell ourselves to keep using Golang
- FasterThanLime: I want off Mr Golang’s wild ride
1 commentaires
Opinion sur Hacker News
J’utilise Go depuis l’époque pré-1.0 dans presque tous mes emplois à temps plein. C’est simple pour apprendre les bases en équipe, et ça fonctionne de manière globalement stable. Quand on met à jour vers une nouvelle version de Go, il y a rarement de quoi s’inquiéter, et la plupart des fonctionnalités utiles sont incluses par défaut. La vitesse de compilation est un vrai atout. Le parallélisme est un peu délicat, mais quand on y consacre du temps, ça devient une bonne manière d’exprimer le flux de données. Le système de types est généralement pratique, même s’il peut parfois être verbeux. Dans l’ensemble, c’est un outil fiable. Mais je me reconnais aussi dans plusieurs critiques mentionnées dans l’article. Il est clair que Go a des endroits où des développeurs d’une autre génération se sont trop accrochés aux principes en oubliant des commodités pratiques. Bien sûr, c’est mon ressenti, et je pense aussi que si tous les défauts avaient été corrigés, le langage serait peut-être encore moins bon qu’aujourd’hui. Je veux aussi souligner qu’au cours des dernières années, l’ambiance semble plus ouverte à la correction de certaines bizarreries. Je n’aurais jamais imaginé qu’on verrait arriver les generics ou des itérateurs personnalisés. Les critiques sur la RAM et la portabilité me paraissent en partie relever de préférences personnelles. Ce serait bien que ça s’améliore, mais le GC provoque très rarement de vrais problèmes dans la majorité des programmes, et le débogage n’est pas particulièrement difficile. Et Go prend en charge quasiment toutes les plateformes importantes. En revanche, la gestion des erreurs et de
nilcontinue de me gêner. Des syntaxes commeResult[Ok, Err]ouOptional[T]me manquent souventMoi, je dirais plutôt que Go ne s’est pas entêté sur des principes, mais sur la commodité de résoudre rapidement le problème qu’il avait juste sous les yeux. On n’a pas analysé le problème à la racine pour le résoudre correctement ; on a plutôt l’impression d’avoir laissé tomber l’esprit du « Not Invented Here » pour bricoler quelque chose sur le moment. L’API de système de fichiers de Go en est un bon exemple. Il faut une fonction pour ouvrir un fichier ? On fait simplement
func Open(name string) (*File, error)et c’est réglé. Mais si le nom du fichier n’est pas en UTF-8 ? Comme le problème ne se pose pas pendant cinq ans, on l’ignoreJ’ai souvent l’impression que les principes de conception de Go sont trop focalisés sur l’objectif « rendre le compilateur facile à écrire et la compilation rapide ». La structure semble pensée d’abord pour le compilateur et la compilation eux-mêmes, plus que pour le confort du développeur
Après 20 ans, c’est dans un nouveau poste que j’ai vraiment utilisé Go pour la première fois comme langage compilé. C’est peut-être une question de goût personnel, mais honnêtement, son utilisation me provoque même parfois une forme de déplaisir. Pas de valeurs d’arguments par défaut, une gestion des erreurs qui ne me plaît pas, pas de vraie stack trace en production. La syntaxe orientée objet n’est pas élégante à lire, puisqu’il faut accrocher des références un peu maladroites à chaque fonction. Les pointeurs sont aussi une contrainte. Au final, j’ai l’impression de revenir à de vieilles techniques de C/C++. On retrouve exactement l’ambiance de programmation que j’ai connue à l’université vers 1999
Sur le plan du parallélisme, Go est, d’après mon expérience, le seul système où le langage lui-même traite naturellement le parallèle sur des CPU multicœurs. Grâce au formalisme CSP avec les goroutines et les channels, la logique de parallélisme s’exprime de manière intuitive. Python est pénible avec le GIL et ses bibliothèques async ésotériques. C, C++, Java, etc. ont besoin de bibliothèques externes, ce qui rend le raisonnement sur le parallélisme au niveau du langage moins évident. C’est pour ça que je trouve que go est parfaitement adapté aux serveurs HTTP et aux services. À mon expérience, il n’y a pas vraiment d’alternative à ce niveau
Du point de vue du développeur, en matière d’ergonomie — donc de standardisation et de cohérence — j’ai trouvé ça quasiment parfait. Même avec plusieurs bases de code de microservices, on n’a pas à se demander si les styles vont diverger, et il n’y a aucun débat sur le formatage. En revanche, quand Go choisit sa manière standard de faire les choses, on dirait qu’il s’accroche parfois un peu trop à un style ancien. Les développeurs d’aujourd’hui s’attendent davantage à des méthodes fonctionnelles comme
map/filter, alors que Go ne propose que des boucles avec le risque d’erreurs d’index. Le système de types n’est pas non plus aussi intelligent que celui de TypeScript. La gestion des erreurs est pénible. Je comprends l’idée que l’ajout de telles fonctionnalités pourrait multiplier les usages « créatifs mais mauvais », mais je constate aussi qu’il est difficile de convaincre la génération JavaScript d’adopter goCela fait plus de cinq ans que je travaille presque exclusivement sur un gros projet Golang, et dès qu’on doit construire des composants où l’usage mémoire doit être minimisé, on se heurte souvent aux parties fragiles de Go. Le GC ne nettoie pas assez vite ou la fragmentation du heap devient sérieuse (un problème lié au fait que Go n’a pas de ramasse-miettes compactant). Du coup, on essaie d’éviter totalement les allocations, mais ça favorise les bugs. Le débogage est aussi extrêmement difficile. Même avec un profil du heap, on ne voit que les objets survivants ; on ne voit ni les déchets réellement accumulés ni les détails de fragmentation, donc on est obligé de deviner. Par exemple, la fonction X peut sembler n’allouer que 1 KB sur le heap, mais si elle est appelée en boucle, elle peut générer des dizaines de MB de déchets. On finit donc par préallouer des buffers statiques et les réutiliser, mais ça complique les questions de propriété et ouvre des pièges avec
append. Il arrive aussi qu’on doive réimplémenter directement des morceaux de la bibliothèque standard. Je sais bien que notre cas n’est pas représentatif de tout le monde, mais c’est vraiment frustrant d’avoir l’impression de se battre contre le langageDans ce genre de cas, il peut même être moins douloureux de sortir la mémoire du heap. Bien sûr, ce n’est pas simple dans un langage à GC, mais plutôt que de forcer en Go un code trop typé C++/Rust, autant réécrire directement cette partie dans l’un de ces langages
Je pense que dans une situation pareille, le vrai problème, c’est d’avoir choisi go. C/C++/Rust/Zig me semblent plus adaptés
Il y a des nouvelles selon lesquelles le nouveau ramasse-miettes « Green Tea » pourrait aider. Ce n’est pas centré uniquement sur la mémoire, mais c’est un algorithme de marquage parallèle qui gère mieux les objets proches en mémoire. Plus d’infos ici
Une expérimentation autour des
arenaétait en cours, mais elle est actuellement arrêtée. Cela reste néanmoins un sujet intéressantDésolé, ce n’est pas très utile comme remarque, mais vu la situation actuelle, je pense que le choix du langage était totalement mauvais. Je suppose que vous utilisez go à cause d’une politique de langage officielle dans l’entreprise. C’est fréquent dans les grandes boîtes : la production n’est approuvée que pour des langages largement adoptés
Je ne comprends toujours pas pourquoi le
deferde Go ne fonctionne qu’au niveau de la portée de fonction et pas au niveau de la portée lexicale. J’ai découvert ça en traitant des fichiers dans une boucle : quand la liste est devenue trop grande,deferne fermait pas les handles avant la fin de la fonction, ce qui a fini par provoquer un crash. Les développeurs Go autour de moi m’ont dit d’envelopper le corps de la boucle dans une fonction anonyme. À part quelques petits points de ce genre, Go me paraît agréable : la syntaxe est efficace et ça évite aussi une certaine culture inutile du « regardez comme je suis malin ». J’ai réécrit à grande échelle un projet C# en Go ; alors que les fonctionnalités n’étaient qu’un dixième de celles d’origine, le code était pourtant plus court. On n’est pas forcé vers des allocations GC, on est plutôt incité à prendre par défaut des choix performants, et les capacités intégrées de génération de code sont pratiques pour des tâches comme la sérialisation. Contrairement à la syntaxe C# qui tend à vouloir tout remplacer par du langage, en Go l’idée est plutôt d’utiliser SQL comme du SQL, et gRPC via des spécifications protobufParfois, on a besoin d’un
deferà portée lexicale, et parfois à portée de fonction. Par exemple, si dans une boucle on veut ouvrir plusieurs fichiers et les garder tous ouverts jusqu’à la fin de la fonction, la portée de fonction est nécessaire. Aujourd’hui, on a la portée de fonction, mais quand on a besoin d’une portée lexicale, il suffit d’encapsuler dans unefunc. Si on ne supportait que la portée lexicale et qu’on avait besoin de la portée de fonction, on ne saurait pas vraiment comment faireLe fait de supprimer un niveau d’indentation sans fonction d’enrobage, le fait que le comportement soit lié à la pile d’appels ou à son déroulement, et le fait que ça paraisse naturel si l’on vient du style
goto failen C sont des avantages. Bien sûr, utiliserdeferdans une boucle oblige à envelopper dans une fonction séparée, donc ce n’est pas idéalJ’ai déjà utilisé des langages avec
deferau niveau du bloc et au niveau de la fonction, et il m’arrive de souhaiter pouvoir utiliser undeferde niveau fonction même dans un simpleifJe ne pense pas qu’il y ait une raison particulièrement profonde derrière ça, et je me demande même si c’est vraiment important
En C#, on peut aussi travailler avec SQL ou des spécifications protobuf. La différence, c’est simplement qu’il existe d’autres options aussi
Go a beaucoup de défauts, mais dans la catégorie des langages server-side, je n’ai pas l’impression qu’il existe un langage aussi bien équilibré. C’est plus rapide que Node ou Python, et je trouve aussi son système de types meilleur. La barrière d’entrée est plus basse que pour Rust, et la bibliothèque standard comme le tooling sont excellents. J’aime aussi la syntaxe simple et le fait qu’il impose une seule manière de faire. La gestion des erreurs a des défauts, mais c’est toujours mieux que Node où n’importe quelle erreur peut arriver dans un
catch. Je me demande s’il existe un langage encore meilleur qui remplisse tous ces critères. Je ne suis pas un fanatique de Go : j’ai surtout fait du backend en Node pendant ma carrière, mais j’expérimente Go en ce momentEn réalité, on pourrait probablement dire exactement la même chose de Java ou de C#
Ça me gêne un peu d’appeler « Node » un langage de programmation. Node est un runtime JavaScript, et aujourd’hui une grande partie des projets qui tournent sur Node sont écrits en TypeScript. Autrement dit, dire Node ne précise même pas clairement le langage utilisé. Si on prend TypeScript comme référence, je le trouve plutôt plus productif que le système de types de Go. On pourrait faire la même remarque face à Rust
La plupart des langages ont leurs propres irritations. Go offre de très bonnes performances, une excellente portabilité, ainsi qu’un runtime et un écosystème solides. En contrepartie, il y a aussi des défauts comme les pointeurs
nil, les zero values, l’absence de destructeurs, l’absence de macros (ce qui pousse à abuser de la génération de code pour contourner le problème). Il existe de meilleurs langages (par exemple Rust), mais ils deviennent aussi bien plus complexes que Go. C’est la conséquence du fait que les créateurs de Go ont placé la simplicité au-dessus de toutVu les progrès récents du système de types de Python, je le trouve bien plus avancé que celui de Go. Rien que sur le structural typing, Python est plus impressionnant
Je trouve que le système de types de Go est très insuffisant
J’ai déjà étendu un static site generator écrit en Go : le code était très clair et très lisible, mais les limites du langage rendaient l’extensibilité faible. Même des changements simples obligeaient à retoucher difficilement plusieurs endroits du code. Il est difficile d’obtenir différents niveaux d’encapsulation et d’abstraction, et l’abstraction est sacrifiée au nom de la « simplicité ». Or l’abstraction est le moyen le plus important pour écrire du code facile à étendre, donc Go choisit la simplicité au détriment de l’extensibilité. Au final, les programmes Go donnent souvent l’impression d’une « simplicité sans extensibilité ». Les gens répètent que Go est fait comme ça, mais mon expérience ne me convainc pas. Cela dit, au moins, l’« expérience développeur » n’est pas mauvaise
Les conversations sur Go me semblent toujours un peu étranges. Quand on le critique, la réponse est souvent « c’est comme ça que ce langage est fait », comme s’il fallait juste l’accepter. On présente la simplicité comme une force, mais je me demande en quoi devoir écrire soi-même une boucle juste pour extraire la liste des clés d’une map est vraiment plus simple
Est-ce qu’on peut vraiment formuler ce genre de critique après avoir seulement utilisé Go brièvement ? J’ai travaillé depuis 2015 sur de nombreuses grandes bases de code Go, avec des millions de lignes, dans plusieurs équipes. Je ne trouve pas que Go soit particulièrement moins extensible que C, C# ou Java. Go privilégie clairement la lisibilité à l’expressivité. Du coup, on a moins de couches d’abstraction et on prend l’habitude d’écrire de façon plus concrète et explicite. Mais je ne pense pas que cela mène à l’impossibilité d’étendre. Une conception modulaire et extensible relève plus de ce que les développeurs apprennent à faire que des capacités du langage. Le code que tu as vu était sans doute mal conçu ; ce n’est pas une limite intrinsèque de Go
J’ai utilisé Go pendant quelques années. On peut construire rapidement de petites choses, mais plus le projet grossit, plus on souffre d’une multitude de petites frustrations. Le débogage est particulièrement cauchemardesque : s’il y a un X inutilisé (ce qui arrive tout le temps quand on commente temporairement une partie du code pour déboguer), la compilation échoue carrément. Il y a aussi beaucoup de formalisme inutile, de noms de fichiers spéciaux, de noms de champs réservés, tout cela est pénible. Les
paniccachés dans la bibliothèque standard et les copies imprévues sur le heap sont aussi lentes qu’agaçantes. La plupart des aspects « magiques » de Go viennent de tentatives de réemploi forcé de mécanismes existants (noms de fichiers spéciaux, majuscules/minuscules, etc.), avec des effets secondaires pénibles. S’il fallait vraiment un mot-clé d’exposition, ils auraient pu écrirepub, mais il y a une forme d’entêtement étrange. Aujourd’hui, comme l’IA s’est beaucoup améliorée, quand j’ai un problème de types ou de borrow checker en Rust, je demande directement à l’IA et je le résous vite, ce qui est bien plus agréable. Je n’ai plus besoin de perdre du temps dans la doc ou sur Stack Overflow comme avantJe n’ai pas fait beaucoup de Rust récemment, mais quand j’en ai essayé un peu en décembre dernier, j’ai été surpris de voir à quel point l’IA s’en sortait bien avec Rust. Avec sa syntaxe détaillée et ses informations de type explicites, l’IA le résout presque mieux qu’un humain
Quand on se plaint des erreurs de compilation pendant le débogage en Go, le camp Go répond souvent en te reprochant de ne pas utiliser correctement les outils. Les principes y sont appliqués de manière trop extrême, au détriment du confort
J’ai déjà parlé de cette gêne de débogage à l’un des créateurs de Go, et même lui ne semblait pas comprendre le problème. Ça m’a paru incroyablement amateur, et ça m’a déçu. À l’inverse, l’IA n’est pas particulièrement bonne avec Go. Bien que le langage soit relativement simple, ChatGPT aide mieux en Java, C# ou Python
Personnellement, je n’aime pas Go et j’y vois beaucoup de défauts décisifs, mais il est clair pourquoi sa popularité reste forte. Go est relativement rapide et, grâce aux goroutines, il permet d’écrire facilement des services fortement concurrents, stables et fiables, sans avoir à gérer explicitement du multithreading. Quand Google a lancé Go, il n’existait presque aucun langage compilé, statique et grand public comparable. Même aujourd’hui, le concurrent le plus proche dans cette position est Java (désormais avec le support des virtual threads). Les langages avec async/await promettent quelque chose de similaire, mais en pratique ils introduisent beaucoup de complexité : éviter le blocage dans les tâches asynchrones, function coloring, etc. Erlang est encore dans une autre catégorie. Au final, malgré ses nombreux défauts, Go reste populaire grâce aux goroutines et au poids du nom Google derrière le projet
Petit à petit, la JVM réduit l’écart avec Go. Avec des projets comme virtual threads, zgc, lilliput, Leyden ou Valhalla, ça progresse constamment. Le passage de Java 8 à Java 25 est immense. À l’avenir, ce sera probablement encore plus confortable
L’explicitation et la simplicité de Go conviennent très bien à la programmation assistée par LLM. Même de vieux codes Go 1.x tournent encore très bien sur les versions actuelles
En pratique, à l’intérieur de Google, on utilise Java avec virtual threads bien plus souvent que Go
Je serais curieux de savoir quel est selon toi le « langage moderne » le plus adapté aux nouveaux projets
J’aimais Go avant même la sortie de la 1.0, mais je ne suis pas du tout d’accord avec l’idée qu’« ils n’ont toujours pas réussi à le faire ». Bien sûr, il y a des défauts et des frustrations, mais je pense aussi que quand les fondateurs quittent le projet, il devient difficile de maintenir une vision centrale, et le langage risque de se dégrader. Le fait qu’il soit positionné uniquement comme « langage serveur » finira aussi, à mon avis, par pousser les gens vers Rust ou Python. À une époque, on se moquait aussi de Visual Basic, mais ceux qui en avaient besoin s’en servaient très bien malgré tout
Quand on examine de près les billets critiques sur les défauts de Go, la plupart des points évoqués ne sont pas de si gros problèmes. C’est souvent techniquement exact, mais mineur. En revanche, les vrais problèmes graves de conception du langage sont plutôt les zero values, l’absence de constructeurs, la mauvaise gestion de
null, la mutabilité par défaut, un système de types qui n’a pas été pensé pour les generics, desintsans précision arbitraire, ou encore desslicedont les questions de propriété restent floues (issue liée 1, issue liée 2). L’absence de sum types ou de prise en charge de l’interpolation de chaînes est aussi un défautJe suis peut-être biaisé au point d’avoir écrit un livre sur Go, mais après plus de dix ans d’utilisation, je me souviens qu’au début le langage m’avait vraiment paru rafraîchissant. Il y avait moins de boilerplate qu’en Java, c’était facile à apprendre, et les performances étaient tout à fait correctes. Il n’existe pas de langage parfait, et il y a toujours un meilleur choix selon les usages, mais pour les tâches backend classiques, c’est un choix qu’on regrette rarement