Quelles sont les principales difficultés que vous avez rencontrées dans sa réalisation ? Y a-t-il des points qui vous ont particulièrement posé problème ?
La difficulté la plus visible, c'est que le sujet évolue pendant qu'on écrit. Go 1.26 a activé par défaut Green Tea, une nouvelle façon de conduire la phase de marquage du garbage collector : les petits objets sont traités par page plutôt qu'un par un, pour améliorer la localité mémoire. Le chapitre sur le GC était déjà écrit, et il a fallu le reprendre pour intégrer ce changement.
Vient ensuite la mesure. Un benchmark mal écrit mesure autre chose que ce qu'on croit : le compilateur supprime du code dont le résultat n'est pas utilisé, la machine a du bruit, et le résultat change d'un processeur à l'autre. Je privilégie donc toujours les comparaisons : deux variantes mesurées sur la même machine, où c'est l'écart qui compte et non la valeur absolue. C'est ce qui a permis de changer de machine en cours de projet, d'un Mac à processeur Intel à un Mac équipé d'une puce Apple M4 Pro, sans que cela remette en cause les mesures déjà faites.
Restait à savoir où s'arrêter, et donc quoi inclure. Ma liste de sujets initiale était bien plus longue que le sommaire final. En voyant le livre grossir, j'ai dû trier et renoncer à des sujets entiers. Le critère était l'objectif du livre : il ne s'agit pas de former des développeurs du compilateur, mais de donner les éléments pour prendre des décisions.
Comment avez-vous vérifié vos affirmations ? Sur quoi se fondent vos préconisations ?
Sur les sources primaires. Les descriptions du runtime et du compilateur s'appuient sur la documentation de go.dev et sur le code source du dépôt github.com/golang/go. Quand je décris une structure interne, elle vient du code, pas d'un article de seconde main.
L'outillage fourni avec Go permet d'observer directement les choix du compilateur : ce qui est alloué sur le heap, ce qui est inliné. Quand un comportement peut être mesuré ou mis en évidence, un benchmark ou une démonstration l'accompagne, avec un code en annexe que le lecteur peut rejouer sur sa machine et sa version de Go.
« Désassembler pour voir
ce qui est réellement exécuté. »
J'ai fait six ans d'études en électronique, et l'assembleur a été mon premier langage. Chez HAProxy Technologies, chaque modification du cœur de HAProxy donnait lieu à un désassemblage pour vérifier que l'assembleur généré était optimal : au million de requêtes par seconde, chaque instruction compte. J'en ai retenu un réflexe que j'applique à Go : désassembler pour voir ce qui est réellement exécuté.
Les préconisations découlent de ces mécanismes, mais aussi de mon expérience professionnelle. Je privilégie le pragmatisme : une solution simple qui répond au besoin plutôt qu'une construction pensée pour des cas qui n'arriveront pas. Quand un choix dépend du contexte, je le dis plutôt que d'en faire une règle. Quand un choix de conception de Go me paraît discutable, je le dis aussi, comme c'est le cas de la variable déclarée dans l'init d'une boucle for. La syntaxe reste ambiguë, et plutôt que de lever cette ambiguïté, les concepteurs ont changé le comportement en Go 1.22 pour adopter ce que la majorité comprenait. Cela supprime beaucoup de bugs, mais un langage qui s'aligne sur la lecture majoritaire laisse les autres se tromper.
Go est généralement apprécié pour sa simplicité et ses performances. Pourquoi encourager ses utilisateurs à s'intéresser au runtime ?
Parce qu'un langage simple à écrire cache généralement une complexité à l'exécution, et Go n'échappe pas à la règle. Son compilateur et son runtime prennent des décisions à la place du développeur : si une variable va sur la stack ou sur le heap, combien de threads utiliser, quand lancer le garbage collector, quand préempter une goroutine. Tant que ces décisions conviennent, on n'a pas besoin de les connaître.
Quand elles ne conviennent plus, les symptômes sont indirects : une latence qui augmente sous charge, une mémoire qui ne redescend pas après un pic, du CPU consommé par le GC. Sans modèle du runtime, on essaie des réglages au hasard. Avec, on sait quelle métrique regarder et quels leviers existent.
Plus simplement, le runtime est le matériau du développeur Go, et connaître son matériau permet de travailler mieux et plus efficacement.
« Le runtime est le matériau
du développeur Go »
Avez-vous un exemple personnel d'un bug ou d'un problème de performance que vous avez résolu grâce à cette compréhension du runtime ?
Je me rappelle d'un service dont la consommation mémoire montait trop. J'ai commencé par régler le GC pour qu'il se déclenche plus tôt : cela contenait suffisamment la mémoire pour éviter l'OOM kill, au prix d'un peu de CPU, le temps de comprendre la cause. La cause, c'était quelques structures à la durée de vie éphémère, mais très manipulées au cœur du logiciel, qui vivaient sur le heap et circulaient par pointeur. Les repasser sur la stack, avec des copies, a supprimé le problème : une copie coûte moins cher qu'une allocation sur le heap suivie du travail du GC. Le réglage du GC est alors redevenu inutile.
C'est un cas où les deux niveaux servent : le réglage pour tenir en attendant, et l'escape analysis pour traiter la cause.
On pourrait penser que cette connaissance ne sert qu'aux systèmes critiques ou très sollicités. Est-ce le cas, et si non, pouvez-vous donner un exemple du quotidien où elle fait une vraie différence ?
En fait, non. Quand on connaît le runtime et le comportement du compilateur, on se méfie plus facilement de certaines formes de code. On reconnaît des schémas, on connaît les limites de certaines techniques, et cela vaut pour n'importe quel programme.
Une concaténation de chaînes dans une boucle a par exemple une limite d'efficacité, du fait des contraintes du type string. Une variable capturée par une closure pour être modifiée, et qui survit à la fonction appelante, passe de la pile au tas et est accédée comme un pointeur, sans que la syntaxe l'indique. Une slice passée à une fonction ne protège pas les données : c'est lié au fonctionnement fondamental des slices. Un switch à dix cas peut être plus rapide qu'un switch à cinq cas : c'est lié à la façon dont le compilateur les traduit.
Au quotidien, la différence se fait sur la vitesse de compréhension : quand on sait ce qui se passe en dessous, on reconnaît plus vite les schémas de bugs. Sur la durée de vie du projet, la différence se fait sur les choix faits dès l'écriture.
Bien sûr, tout ceci n'est qu'une brique dans la conception générale. Pour avoir une vision complète, le développeur doit aussi connaître d'autres ordres de grandeur : le coût d'évaluation d'une expression régulière, le parsing d'une requête HTTP, le temps d'accès à une base de données. C'est aussi ce qui permet de trancher : savoir dans quels cas Go convient, et dans quels cas il vaut mieux l'éviter.
Les assistants IA qui génèrent du code Go tiennent-ils compte de ces subtilités d'implémentation ? Et cela change-t-il, selon vous, l'intérêt de creuser soi-même la connaissance d'un langage ?
Ils produisent en général du code Go correct et idiomatique pour les cas courants. Mais ils reproduisent ce qu'ils ont vu : si un motif a été répandu, ils le proposent, qu'il soit encore utile ou non. C'est paradoxal : l'assistant peut restituer le fonctionnement du runtime, mais tant que le développeur ne sollicite pas cette connaissance, c'est le code le plus répandu qui prime. Par ailleurs, si on ne le leur précise pas, ils ne connaissent pas le contexte de déploiement : la charge, les limites mémoire et CPU, la latence acceptable, qui déterminent souvent le bon choix.
« Les assistants renforcent l'intérêt de connaître le langage,
plutôt qu'ils ne le réduisent. »
Une part croissante du code est déjà produite par des assistants, et le rôle du développeur se déplace vers la spécification : décrire le besoin, les contraintes, et les formes à privilégier ou à éviter. Une bonne spécification ne s'arrête pas à la fonctionnalité. Elle intègre les contraintes, comme le volume de données, la latence attendue ou la mémoire disponible, et elle liste les pièges à éviter dans les fonctions très sollicitées. Un assistant n'aura pas forcément ces réflexes. Pour les lui signaler, il faut savoir que ces pièges existent. Les assistants renforcent donc l'intérêt de connaître le langage, plutôt qu'ils ne le réduisent.
Connaître le runtime du langage qu'on utilise, c'est un premier pas pour passer de programmeur à développeur. C'est probablement là que se situera la valeur ajoutée d'un humain face aux assistants.
Le livre s'appuie sur des mécanismes très récents du runtime, certains liés à Go 1.26. Comment avez-vous pensé sa pérennité face à un langage qui continue d'évoluer ?
Les principes généraux changent peu : le modèle G-M-P du scheduler, le GC concurrent à trois couleurs, l'escape analysis, le modèle mémoire et ses relations happens-before, la représentation des slices, des strings et des interfaces. Une grande partie du livre porte sur ces principes, et ils restent valables d'une version à l'autre. Ce sont les implémentations qui évoluent à chaque version.
Le livre décrit donc une version de référence, Go 1.23, celle qui était encore couramment utilisée en production quand j'ai commencé à écrire. Quand un mécanisme appartient à une version plus récente, le texte le précise. C'est le cas, par exemple, de Green Tea, le nouveau mode de marquage du GC, activé par défaut dans Go 1.26.
Le livre montre aussi comment observer le runtime soi-même : GODEBUG, pprof, sortie du compilateur, assembleur. Ces outils restent quand la version change. Le lecteur qui les a pratiqués au fil du livre est capable de comprendre par lui-même pourquoi un comportement diffère de ce qui est écrit.
Les évolutions d'implémentation seront intégrées dans une future édition, basée sur une version de référence plus récente.
Comment lire votre livre ? Du début à la fin ?
Pas nécessairement. Les deux premières parties, Fondements du runtime et Types et structures de données, se lisent indépendamment l'une de l'autre et chacune est utile seule. Les parties sur les mécanismes avancés du langage et sur la concurrence s'appuient sur elles : il vaut mieux connaître le scheduler avant d'aborder les channels, et la représentation des types avant les optimisations du compilateur.
Les annexes sont facultatives. Elles contiennent le code des démonstrations, les benchmarks et le détail de quelques algorithmes utilisés par le runtime, comme les tas binaires ou la division par une constante.
Après une première lecture, le livre peut servir de référence : on revient au chapitre sur le GC quand un service consomme trop de mémoire, ou aux primitives de synchronisation quand un profil montre de la contention.
« Les principes généraux changent peu.
Ce sont les implémentations qui évoluent à chaque version. »