Go : ce que j'apprécie

On m'a posé la question. Voici ma réponse.

« Dis-moi ce que t’aimes bien à propos de Go »

Il y a quelques semaines, on m’a demandé ce que j’aimais bien à propos de Go. C’était pendant un entretien d’embauche et j’ai réalisé sur le moment que je n’avais jamais vraiment songé aux raisons, et cela même si j’utilise Go pour presque tous mes projets depuis un bon bout de temps.

Donc j’y ai réfléchi, et voilà que je l’écris à présent. Je partage mon expérience à partir de deux perspectives :

  • le côté développeur : sur le langage Go en lui-même, le tooling et l’écosystème qui va autour, et
  • le côté "Ops" : sur le déploiement et la gestion des programmes écrits en Go (peut-être l’aspect qui m’importe le plus).

La perspective Ops

Performance

Pour mes usages et le genre de service que j’écris, Go offre des performances plus que décentes qui me permettent de faire tourner mes programmes sur des petites instances peu coûteuses et des environments serverless contraints (comme AWS Lambda ou Scaleway Serverless Jobs). Pour le serverless, les performances sont importantes car la tarification se fait en fonction du temps d’exécution.

Go n’est peut-être pas aussi performant que Rust ou C++, mais venant d’une expérience Python, Go me semble juste stratosphérique.

Cross-compilation facile

La facilité de déploiement sur une flotte hétérogène présentant plusieurs architectures est importante. J’écris des services qui vont tourner sur le Cloud (en serverless ou en IaaS), sur des serveurs physiques, sur mon laptop ou même sur mon Raspberry Pi 4. Avec l’avènement des serveurs Arm64 (peut-être RISC-V 64 dans le futur ?), j’ai besoin de cross-compiler, que ce soit sur ma machine ou dans ma CI, et ce sans tumutle.

Binaires statiques et conteneurisation

Comme évoqué, certains de mes projets tournent sur des environments hétérogènes. C’est donc tout naturellement que je privilégie les conteneurs pour déployer mes services. Conteneuriser des services en Go est plutôt simple, et comme le code en Go se compile rapidement, les images multi-stages se construisent rapidement également. Avoir des binaires statiques permet de choisir des images de base très légères, comme Alpine Linux, sans se soucier des dépendances à la libc, etc. Mes images sont ainsi légères, faisant quelques dizaines de mégaoctets.

Temps de démarrage

Quand je travaille avec des orchestrateurs comme Kubernetes ou AWS ECS, j’ai besoin de conteneurs qui démarrent vite. Ainsi, roller une nouvelle version du service prend moins de temps, tout comme le scaling out (démarrer plus de conteneurs). Pour les service serverless fonctionnant avec des conteneurs, un temps de démarrage moindre signifie aussi un temps d’exécution réduit, et donc moins de frais.

La perspective développeur

Équilibre bas-niveau / haut-niveau

Avant d’apprendre Go, j’ai fait beaucoup de Python (que j’utilise encore), qui est considéré comme un langage de haut niveau. En comparaison, Go est plutôt bas niveau, mais certainement pas aussi bas niveau qu’un C. J’aime assez cet équilibre que Go a. Pas trop bas niveau pour ne pas se perdre dans des détails qui n’ont pas d’importance dans ce que je fais ; mais pas non plus trop haut niveau, me laissant ainsi utiliser des primitives telles que les instructions atomiques, ou me laisser contrôler la disposition d’un struct.

Système de types

Celui-là est très subjectif. J’aime bien les types statiques, je fais souvent des erreurs idiotes évidentes qui sont facilement repérables à la phase de compilation. Go pousse aussi le typage structurel, ce qui m’est plus intuitif à l’usage que le nominal.

Programmation concurrente ergonomique

Cela ne signifie en rien que Go rend la programmation concurrente facile, car la programmation concurrente n’est jamais facile en premier lieu. Mais Go la rend au moins plus agréable, notamment grâce aux goroutines (évitant ainsi le problème des « fonctions colorées » avec l'async) et les channels (parfois seulement).

Tooling standardisé

Comme pas mal d’autres langages de sa génération, Go vient avec son outil standard pour gérer les packages, lancer les tests, compiler les programmes, formater le code et même microbenchmarquer le code. C’est confortable.

Pas de code pompeux difficile à lire

Go propose essentiellement que des constructions simples. Cela décourage le code golfing et je pense que c’est une bonne chose.


Évidemment Go n’est pas parfait et j’aurais bien des choses à redire à son égard. Par ailleurs, je suis bien conscient que Go n’a pas l’exclusivité sur tous les points que je mentionne. Mais ce billet répond à la question qui m’avait été posé initialement : ce que j’appréciais à propos de Go ;)

J’aimerais terminer sur une observation : je juge assez peu Go-le-langage en tant que tel pour parler beaucoup plus des implications opérationnelles d’un programme écrit en Go. J’aime aussi Go-le-langage, bien-sûr, mais au point où j’en suis aujourd’hui, je remarque que l’expérience Ops demeure l’aspect déterminant dans mon choix de techno, peut-être plus que l’expérience de développement et les qualités intrinsèques du langage.

Crédits

Image du billet : logo officiel de Go, utilisée selon les Brand and Trademark Usage Guidelines.

8 commentaires

Merci pour le retour d’expérience.

Outre la documentation, as-tu des ressources intéressantes et de bonne qualité pour obtenir une première expérience en Go ? Je me souviens avoir vécu plus de frustrations qu’autre chose quand je me suis essayé à Go pour un projet perso mais je ne m’arrête jamais à une première impression, surtout quand je sais que, dans mon entourage, on en dit le plus grand bien.

Merci pour le retour d’expérience.

Outre la documentation, as-tu des ressources intéressantes et de bonne qualité pour obtenir une première expérience en Go ? Je me souviens avoir vécu plus de frustrations qu’autre chose quand je me suis essayé à Go pour un projet perso mais je ne m’arrête jamais à une première impression, surtout quand je sais que, dans mon entourage, on en dit le plus grand bien.

Ge0

J’avais mis le pied à l’étrier avec ce livre en ligne : https://quii.gitbook.io/learn-go-with-tests/ Ça a l’air d’être un livre qui se focalise sur les tests, mais en réalité c’est un livre qui enseigne Go en tant que tel en plus des tests en Go. Depuis le temps, j’ai vu que le livre a pas mal évolué (en bien) et il a l’air de couvrir tout ce qui est important pour avoir une base solide avec ce langage (à l’époque c’était un peu moins fourni).

Par la suite, un livre que j’ai beaucoup apprécié : 100 Go Mistakes and How to Avoid Them, édité chez Manning. C’est une excellente resource quand on a déjà un peu codé en Go, et qu’on veut consolider. Il couvre Go 1.18, ce que je considère encore à jour en 2024 (les generics y sont déjà).

De mon côté je serais aussi curieux d’avoir l’autre côté de la pièce : y a-t-il des éléments que tu n’as pas apprécié dans Go, ou des cas d’usage pour lesquels il ne te semble pas adapté ?

Ouaip ! En vrac : Il y a des trucs qui me gonflent vite avec Go, par exemple quand je veux écrire un script rapidement qui va consommer une API et que je dois me farcir des structs ad hoc pour gérer la déserialisation JSON. Sur un "vrai" projet c’est moins un souci car on a en général des types correctement définis et annotés, pour peu que l’API soit à peu près correcte. Mais pour un script one-shot, on a vite fait de juste utiliser Python à la place. Avec des API qui offrent des retours JSON peu cohérents sur la structure les uns avec les autres, ça peut vite devenir pénible, aussi (ex. : un endpoint qui peut retourner au choix {"data": {"msg": "lala", "success": true}} ou {"success": false, "err": "lalala"}). J’ai l’impression qu’en général c’est vite pénible avec des langages statiques de toute façon.

Un autre aspect que je n’aime pas, c’est la gestion de l’intégration du code assembleur : de un, Go utilise en fait son propre "méta-assembleur" (ou plutôt celui de Plan 9, en réalité) en renommant les registres (et plus encore) ; et de deux, à ce jour une fonction définie en assembleur ne peut être inlinée. C’est dommage, beaucoup de fonctions écrites en assembleur ne sont intéressantes que sans l’overhead de l’appel de la fonction. Ça revient assez souvent dans les discussions des contributeurs : une personne tente d’écrire une version SIMD d’un calcul en quelques lignes d’asm, et ça ne performe pas mieux que le code Go compilé non optimisé, mais qui lui sera inliné. Je passe déjà un mauvais moment quand j’essaie d’utiliser l’assembleur de Go, et tout ça sans savoir si le jeu en vaut la chandelle.

Beaucoup d’avantages que j’ai évoqués s’amoindrissent nettement dès qu’on a besoin de s’interfacer avec du code extérieur (CGO_ENABLED=1), typiquement en interface C. Go reste une île isolée, tout se passe bien tant qu’on y reste. C’est pour cela qu’on privilégie en général une lib Go pure plutôt qu’une lib binding. Une fois j’avais un service de transcodage : c’était plus simple de juster appeler ffmpeg via un subprocess que de s’interfacer avec la libav (en C). Je gardais ainsi tous les avantages de compilation statique et cross-platform, et que mon container soit AMD64 ou ARM64, j’avais de toute façon le bon binaire ffmpeg des dépôts Alpine suivant l’arch.

Enfin, Go n’est pas toujours memory-safe et resource-safe, disons-le. Il y a quand même pas mal de gotchas. Parfois un simple go vet suffit à les débusquer, et pour les problèmes de concurrence un test avec le flag -race aussi. Mais pensons aussi aux types pas toujours bien initialisés qui pètent au run time (pointeur égal à nil qu’on a oublié d’initialiser dans un struct), ou une gestion des ressources hasardeuses. Par exemple, admet-on j’ouvre un fichier en début de fonction et j’ai le file descriptor fd associé. Dans le cas très courant, je peux m’assurer de toujours fermer ce fd avec un defer fd.Close(). Maintenant, il se passe quoi si j’ai passé ce fd à une autre goroutine ? (que ce soit via un channel ou autre biais). On peut certes architecturer son programme pour gérer ce cas si on sait qu’on a à le gérer. Mais parfois c’est juste une erreur qui se glisse sans qu’on y prenne garde.

Les langages comme Rust apportent ici une réponse convaincante à ce genre de problèmes. Mais Go, pas toujours.

+3 -0

Tu dis beaucoup de choses de Go qui à mon sens en fait un langage moderne.
Effectivement, toute la perspective Ops de l’environement de ce langage (et tous ces outils, tu dis le tooling) a été pensé dès la conception de cette technologie.
Un des problèmes à résoudre chez Google (ceux qui ont pensé le langage) était justement les nouvelles problématiques du cloud qui ce sont démocratisé avec son utilisation de plus en plus importante.

L’autre point que tu met en avant est du coté du développeur.
C’est quelque chose qui a aussi été pensé dès la conception de l’outil. Un des but était de crée une technologie qui soit facile à apprendre pour n’importe quel développeur.
Une des incidences de cette manière de procédé est l’intégration des génériques environ 10 ans après la première version stable (la version 1.18).
C’est aussi pour cette raison que la programmation concurente est si simple et facile à utiliser. Encore une fois, le langage a été conçu avec cette idée en tête.


Quelque chose à prendre en compte quand on parle de Go est qu’il est récent (2012 pour sa première version stable). Il a donc été pensé pour résoudre les problématiques d’aujourd’hui contrairement à d’autres outils comme par exemple le C ou le Java qui ont été pensé et conçu pour résoudre des problématiques de leurs temps.

+1 -0

Une des incidences de cette manière de procédé est l’intégration des génériques environ 10 ans après la première version stable (la version 1.18).

pyoroalb

Je n’ai pas compris quelle était la manière de procéder en question, le fait d’être facile à apprendre ? Dans ce cas j’ai un peu de mal à voir l’implication.

Quelque chose à prendre en compte quand on parle de Go est qu’il est récent (2012 pour sa première version stable). Il a donc été pensé pour résoudre les problématiques d’aujourd’hui contrairement à d’autres outils comme par exemple le C ou le Java qui ont été pensé et conçu pour résoudre des problématiques de leurs temps.

pyoroalb

Attention quand même avec ce genre d’argument, car on pourrait alors dire que les problématiques de 2024 ne sont plus les mêmes que celles de 2012 et donc que Go est lui-même dépassé.

12 ans c’est plus que le temps qui sépare la première version de Go de la première de C# par exemple (qu’on peut considérer dans la même veine que Java). Et entre Java et Go c’est 17 ans.

Je n’ai pas compris quelle était la manière de procéder en question, le fait d’être facile à apprendre ? Dans ce cas j’ai un peu de mal à voir l’implication.

entwanne

Cette ajout tardif de cette fonctionnalité est du à trouver le meilleure compromis entre 1. une compilation toujours rapide 2. exprimer les idées de manière simple et élégante 3. qu’ils puissent être facile a apprendre (du à sa simplicité notamment)


12 ans c’est plus que le temps qui sépare la première version de Go de la première de C# par exemple (qu’on peut considérer dans la même veine que Java). Et entre Java et Go c’est 17 ans.

entwanne

Ici, si l’on prend l’exemple de Java, son but étais de pouvoir être exécuté absolument partout. D’où le principe d’être exécuté dans une VM.
Aujourd’hui, à l’heure où la conteneurisation se démocratise de plus en plus, on peut se demander quel est l’intéret d’avoir une VM dans un conteneur.
On peut aussi prendre l’exemple de C. Aujourd’hui avec nos ordinateurs toujours plus puissant, on peut se permettre d’utiliser des Garbage Collector afin de ne plus avoir à gérer la mémoire à la main et rendre le développement plus facile.

Ici, quand je parle de C et de Java, et que je remet en question certains de leurs fonctionnement, je parle de «la plupart du temps». Il y a des cas où ces fonctionnalitées de ces outils les rendent indispensable pour résoudre le problem actuel.


Attention quand même avec ce genre d’argument, car on pourrait alors dire que les problématiques de 2024 ne sont plus les mêmes que celles de 2012 et donc que Go est lui-même dépassé.

entwanne

Effectivement, chaque outils qu’on utilise répond à une problématique passé. Car il faut le temps:

  1. d’identifier le nouveau problem
  2. se rendre compte que les outils à disposition ne suffisent pas
  3. conçevoir le nouvel outil
  4. tester que l’outil est viable
  5. démocratiser l’usage de cet outils

Cependant, bien que les outils récent soit toujours en retard sur les problématiques qu’ils résolvent ils permettent toujours d’apporter de nouvelles innovations.

+1 -0

Je partage globalement l’avis de @sgble (y compris la recommandation de Learn Go with tests). J’étais moi-même en train de réfléchir aux pires "gotchas" du langage qui ont été un jour ou l’autre source de frustration pour moi.

En tête, arrive le comportement des variables d’itération quand on les capture dans la closure d’une goroutine :

for _, elem := range elements {
    go func(){
        // do something with elem
    }()
}

Jusqu’à Go 1.22, ce code avait un comportement très surprenant et contre-intuitif, parce que la variable capturée est partagée et peut être modifiée avant que la goroutine ne démarre.

Jusqu’à la dernière version, donc, il fallait le savoir et corriger en conséquence, pour capturer la bonne valeur dans la closure :

for _, e := range elements {
    go func(elem string){
        // do something with elem
    }(e)
}

D’autres gotchas sont plus de l’ordre de l’alignement "convention vs. règles de compilation". Je pense en particulier à toutes les problématiques d’ownership, et en particulier d’ownership des channels. Ça se traduit dans la pratique par un petit ensemble de règles tacites et de patterns qu’il faut avoir vu et appliqué au moins une fois, comme "c’est la fonction qui a créé le channel qui est responsable de le fermer".

Forcément cela fait beaucoup penser à Rust dont l’accent est mis sur l’obtention d’un code correct, et qui est beaucoup plus directif/prescriptif/prohibitif au niveau de la gestion des durées de vie des ressources. C’est-à-dire que face à ces éléments auxquels Go répond par la voie des idiomatismes et de mécanismes diaboliquement simples comme l’instruction defer, Rust y répond par un système de typage clever qui modélise des lifetimes et des erreurs très descriptives qui pètent à la compilation. C’est une toute autre approche, et à ce titre je pense que faire un peu de Rust et commencer à réfléchir comme son compilo aide à écrire du meilleur code concurrent en Go. Ça, et ne pas hésiter à abuser du race detector qui s’active d’un simple flag dans la commande go test : il trouve toujours quelque chose, même quand on se croit malin.

Un autre élément que l’on peut déplorer : il n’existe pas solution vraiment convaincante de bout en bout pour faire des applications GUI en Go. Des outils super sexy pour faire du TUI comme Bubbletea, oui, des bibliothèques et des moteurs de jeu qui permettent de faire de l’OpenGL comme ebitengine, aussi, mais pour des GUI, on peut s’en sortir avec une lib ou une autre en bricolant un peu, mais on sent bien que ce n’est pas le domaine le mieux servi.

Attention quand même avec ce genre d’argument, car on pourrait alors dire que les problématiques de 2024 ne sont plus les mêmes que celles de 2012 et donc que Go est lui-même dépassé.

Il suffit de parler en techniques et en tendances plutôt qu’en années : Go est cloud native, il a été pensé, au passage, pour être ultra-commode à déployer dans des environnements conteneurisés. En ce sens il est d’une génération postérieure à Java/Python/C#, dont l’approche était de fournir une VM pour faire tourner les programmes dedans, et qui, de ce fait, sont très inefficaces du point de vue de la conteneurisation (il faut embarquer un OS, la VM, la lib standard, les dépendances, les images pèsent des tonnes et prennent des plombes à charger et démarrer). En Go, une image FROM scratch dans laquelle on place le binaire et un fichier de conf par défaut est largement suffisante, et parfois cette frugalité est même nécessaire : j’ai brièvement bossé sur un projet où "les conteneurs sont construits à partir de rien et embarquent le minimum vital" était un requirement du point de vue de la sécurité.

On pourrait aussi adopter l’argument un peu mou qui consiste à rappeler que les problématiques sur lesquelles ils planchaient en interne chez Google en 2009, ce sont celles que l’on rencontrait en masse en 2019 : ils n’ont pas 10 ans d’avance sur la question parce qu’ils sont malins ou plus intelligents que les autres, ils ont 10 ans d’avance sur la question parce qu’ils cherchaient des solutions pratiquables à ces échelles (applis data intensive à portée mondiale qui tournent dans le cloud) qu’ils étaient parmi les seuls à traiter en 2009 (et ce depuis plus de 10 ans), et qui sont devenues très banales 10 ans plus tard, notamment grâce aux solutions auxquelles ils ont abouti : Go et Kubernetes (qui est en réalité la troisième itération de leur orchestrateur interne), et au reste du mouvement "cloud native" qui a construit tout un écosystème moderne et standardisé par-dessus ces technos.

+4 -0

Bonjour à tous,

Pour avoir acheté nombre de bouquins sur le langage Go, le meilleur selon moi pour bien débuter, si on a déjà une pratique d’un ou plusieurs autres langages, est sans doute "Learning Go" de Jon Bodner, qui vient de sortir en 2nde édition chez O’Reilly. Je recommande également "100 Go mistakes and how to avoid them" chez Manning précédemment cité…

Et en complément éventuel, "Go in practice" chez Manning toujours, et dont la 2nde édition est en préparation, proposait dans sa 1ère des recommandations pertinentes sur l’écosystème du langage, à commencer par sa bibliothèque standard.

Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte