davidvella/go-style

Écrire, réviser ou refactoriser du code Go lisible et rapide, en gardant les fonctions petites avec une faible complexité cyclomatique et cognitive, en plaçant la performance dans la conception des API et l'organisation des données plutôt que dans le code astucieux, et en prouvant les changements avec des benchmarks, des comptes d'allocation et l'analyse d'échappement.

Qu'est-ce que go-style ?

Cette compétence guide l'écriture, la révision ou la refactorisation de code Go en privilégiant à la fois la lisibilité et la performance. Elle repose sur le principe que les performances doivent être conçues dans l'API et la disposition des données dès le départ, plutôt que d'être ajoutées après coup via des micro-optimisations ou des astuces de code. La compétence s'appuie sur les enseignements de Dave Cheney ("API Design as a Performance Primitive") et de John Ousterhout, et fournit des directives concrètes pour séparer les chemins chaud et froid, concevoir des API qui minimisent les allocations et les copies, et maintenir des fonctions petites et plates avec une faible complexité cyclomatique (≤10) et cognitive (≤10). Elle détaille comment retourner des vues plutôt que des copies, laisser l'appelant posséder la mémoire réutilisable, définir les erreurs en dehors de la boucle chaude, et compter le travail par unité d'entrée. Enfin, elle insiste sur la nécessité de prouver toute affirmation de performance avec des benchmarks (`benchstat`), des tests d'allocation (`testing.AllocsPerRun`), et l'analyse du compilateur (`go build -gcflags=-m`), et de ne recourir aux micro-optimisations qu'après avoir optimisé la forme de l'API et la complexité algorithmique.

Compatible avec~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/davidvella/skills/tree/HEAD/skills/go-style

Installed? Explore more Développement et programmation skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

Demander à votre IA préférée

Ouvre une nouvelle conversation avec cette compétence d'agent déjà préchargée.

Documentation

Style Go : lisible et rapide

L'objectif est d'écrire du code qu'un mainteneur peut lire de haut en bas, avec des performances qui viennent de la forme de l'API et des données plutôt que d'astuces disséminées dans le code. Lisibilité et rapidité sont rarement en conflit une fois la conception bien faite ; lorsqu'elles le sont, n'abandonnez la lisibilité que sur un chemin chaud mesuré, et mentionnez-le dans un commentaire.

Ceci suit la philosophie de Dave Cheney "API Design as a Performance Primitive" : au-delà d'un certain point, la performance ne peut pas être ajoutée après coup, alors concevez-la dès le départ, puis gardez tout le reste simple.

Sources : talk de Cheney (https://dave.cheney.net/paste/gophercon-uk-2026.html), avec le code dans github.com/pkg/json et github.com/davecheney/bytereader ; John Ousterhout, "Define errors out of existence".

Ordre des priorités

  1. Correction. Ne la sacrifiez jamais pour la vitesse ou la concision.
  2. Forme de l'API et complexité algorithmique. Qui possède la mémoire, ce qui est retourné, combien de travail chaque appel effectue. Cela décide de la performance plus que tout réglage ultérieur.
  3. Allocations sur le chemin chaud. Chaque allocation coûte deux fois : une fois pour allouer, une fois pour le ramasse-miettes. Le ramasse-miettes peut être rapide, "mais ne pas allouer sera toujours plus rapide."
  4. Travail par unité d'entrée. Touchez chaque octet une seule fois et effectuez des appels par jeton ou enregistrement, pas par octet.
  5. Micro-optimisations (inlining manuel, astuces binaires, éviter les vérifications de bornes), uniquement sur un chemin qu'un profil montre comme chaud, et seulement avec un benchmark prouvant le gain.

Si un changement n'est pas justifié au niveau supérieur, ne le faites pas.

Séparer les chemins chauds et froids

Décidez quel code s'exécute par événement ou par requête (chaud) et lequel s'exécute lors de la configuration, de la construction, de la mise à jour ou du traitement par lots (froid).

  • Chemin froid : écrivez le code le plus simple. Clonez plutôt que partagez, utilisez des maps et fmt, allouez librement. N'utilisez pas d'idiomes du chemin chaud ici ; ils coûtent en lisibilité et n'apportent rien.
  • Chemin chaud : peut utiliser des tampons réutilisables, des compteurs de génération, des tableaux plats et des techniques similaires. Donnez à chaque technique un nom (un petit type ou une fonction d'aide) et un commentaire expliquant pourquoi elle existe, afin qu'un lecteur n'ait pas à la rétro-ingénierer.

Concevoir l'API pour la performance

L'API décide combien un appelant peut économiser. "Vous faites un benchmark du ramasse-miettes" lorsque l'API force chaque résultat sur le tas. Façonnez-la pour que le chemin rapide soit le naturel ; un gain de 2 à 10x est souvent disponible "si vous êtes prêt à accepter une API différente".

  • Ne retournez pas de valeurs qui doivent forcément s'échapper. Retourner un type primitif via un interface{}/any, une nouvelle chaîne, une map ou un pointeur vers une nouvelle structure par élément force une allocation par appel (encoding/json.Decoder.Token est l'exemple classique d'"usine à déchets"). Retournez un type concret, ou une vue comme []byte, et laissez l'appelant ne convertir que ce dont il a besoin.
  • Retournez des vues, pas des copies, lorsque l'appelant a seulement besoin de lire. Documentez combien de temps la vue est valide ("valide jusqu'au prochain appel", comme bufio.Scanner.Bytes). Une vue de l'entrée brute (les octets du jeton, dont le premier octet donne son type) est moins coûteuse qu'une valeur décodée que l'appelant pourrait ignorer.
  • Laissez l'appelant posséder la mémoire réutilisable. Acceptez un argument de travail ou de tampon, ou conservez-le dans une structure que l'appelant garde, au lieu d'allouer à chaque appel. Laissez les appelants fournir le tampon initial, afin qu'il puisse vivre sur la pile ou provenir d'un pool : var buf [8 << 10]byte; d := NewDecoderBuffer(r, buf[:]).
  • Cachez la gestion des tampons derrière quelques méthodes. io.Reader copie dans un tampon géré par l'appelant et laisse le re-remplissage à chaque appelant. Une petite API fenêtrée conserve les données en place : Window() []byte pour regarder, Release(n) pour consommer, Extend() int pour lire plus, avec "la fenêtre est invalidée par Release ou Extend" dans le commentaire de documentation.
  • Définissez les erreurs en dehors de la boucle chaude. Utilisez une itération qui retourne un booléen ou un résultat vide et signale l'erreur après coup : for s.Next() { ... } puis s.Err(). Utilisez des résultats sentinelles sur le chemin lent (Extend retourne 0, puis l'appelant demande l'erreur) afin que la boucle interne n'ait pas de branches d'erreur. Traitez io.EOF comme toute autre condition terminale.
  • Comptez le travail par unité d'entrée. Traitez chaque octet une seule fois, effectuez des appels de fonction par jeton ou enregistrement plutôt que par octet, et supprimez les copies intermédiaires. Passer d'appels par octet à des appels par jeton est souvent le deuxième plus grand gain après la suppression des allocations.
  • Superposez l'API. Proposez un bloc de construction de bas niveau, sans allocation (jetons bruts, vues, itérateurs propriétaires de leur tampon) et construisez l'API pratique par-dessus. Ne faites pas de l'API pratique et allouante la seule disponible.
  • Écrivez la propriété et la durée de vie dans les commentaires de documentation : ce qui est emprunté, ce qui est conservé, ce qui est sûr pour une utilisation concurrente, ce que signifie la valeur zéro.
  • Rendez la valeur zéro utile là où c'est naturel.
  • Préférez les valeurs partagées immuables et publiez de nouvelles instantanés, plutôt que des verrous autour d'un état mutable partagé, lorsque les lecteurs dominent.

Mesurez l'API, pas seulement l'implémentation : comparez allocs/op et débit par rapport à l'API que vous remplacez. references/examples.md montre ces formes côte à côte.

Accorder une boucle chaude, en dernier

Seulement après que l'API et la complexité algorithmique soient correctes, et qu'un profil montre que la boucle est chaude :

  • Un switch sur un état se compile en une chaîne de comparaisons. De petites méthodes par état, choisies via un champ func (d.state = (*Decoder).stateValue), gardent chaque état lisible et peuvent supprimer cette répartition.
  • Déplacez une vérification dans la fonction qui utilise la valeur afin que le compilateur puisse prouver que les vérifications de bornes sont inutiles, et gardez le point d'entrée sous le budget d'inlining (80 ; un appel coûte environ 52). Vérifiez avec go build -gcflags=-m=2.
  • Fusionner deux fonctions pour éviter un appel peut gagner 5 à 15 % sur un chemin par jeton, au prix d'un imbrication plus profonde. Ne le faites qu'avec un benchmark, et expliquez pourquoi dans un commentaire.

Comme le dit Cheney : ne recourez pas à ces astuces "sans avoir traité les effets de complexité algorithmique dans votre API. Cela ne fera pas bouger l'aiguille."

Garder les fonctions petites et plates

Visez à ce que chaque fonction fasse une seule chose, avec au plus une ou deux branches. Comme guide, gardez la complexité cyclomatique à 10 ou moins et la complexité cognitive à 10 ou moins (gocyclo, gocognit). Un switch de type plat qui confie chaque cas à un appel est acceptable au-dessus ; l'imbrication ne l'est pas.

  • Nommez les conditions à plusieurs termes. Remplacez if len(a)*len(b) > maxN || size(a)*len(b)+size(b)*len(a) > maxM par if !fitsProduct(a, b). Le nom documente l'intention et la fonction peut porter l'explication.
  • Répartissez, puis déléguez. Un switch sur les types de nœuds ou de messages doit appeler une méthode par cas, et non contenir la logique de chaque cas.
  • Retournez tôt. Les clauses de garde d'abord, le chemin principal non indenté.
  • Ne répétez pas un cas. Si deux branches ne diffèrent que par l'ordre des opérandes, écrivez une seule fonction d'aide et appelez-la deux fois.
  • Séparez les préoccupations mélangées. "Trouver", "construire la nouvelle valeur" et "nettoyer" sont trois fonctions, pas une.
  • Évitez les paramètres booléens. Si l'un est l'option la plus claire, donnez-lui un nom significatif et expliquez-le au site d'appel.
  • Utilisez des tables de recherche (map[string]Kind, [256]bool) au lieu de chaînes de comparaisons de chaînes ou d'octets.

Façonner les données pour le lecteur et le processeur

  • Regroupez les tableaux parallèles en un tableau de structures. Quatre tableaux indexés par le même identifiant sont une seule structure. Cela se lit mieux et améliore généralement la localité.
  • Donnez à un mécanisme répété son propre petit type. Si la même logique de compteur, de bitmap ou de tampon apparaît deux fois, c'est un type avec deux ou trois méthodes.
  • Préférez les champs explicites au bit-packing sauf si le packing est mesuré comme important. []segment{name, each} bat un uint64 de drapeaux avec des décalages de bits.
  • Gardez les représentations peu nombreuses. Moins d'états ou de types signifient moins de switch en double. Fusionnez un cas spécial dans le cas général lorsque cela ne coûte rien de mesurable.

Prouver les affirmations de performance

Ne prétendez jamais qu'un changement est plus rapide, plus lent ou sans allocation sans le mesurer. Avant de modifier du code sensible à la performance :

  1. Enregistrez une référence benchstat des benchmarks pertinents.
  2. Effectuez le changement, puis comparez. Rapportez les différences significatives avec des chiffres.
  3. Diffusez la sortie de go build -gcflags=-m par rapport à la base pour détecter de nouvelles échappées de tas.
  4. Conservez ou ajoutez des tests testing.AllocsPerRun pour les chemins qui promettent zéro allocation.
  5. Profilez avant de deviner la cause d'une régression.

Si la simplification coûte du temps mesurable sur le chemin chaud et que vous ne pouvez pas le récupérer proprement, arrêtez-vous et signalez le compromis plutôt que d'ajouter silencieusement de la complexité ou d'accepter silencieusement la régression.

Lisez references/verify.md pour les commandes exactes, y compris comment mesurer la complexité et les pièges qui causent couramment des régressions. Lisez references/examples.md pour des refactorisations avant-après qui montrent le style.

Suivre les conventions Go

  • gofmt, go vet et le linter du projet doivent passer. Corrigez le code, n'affaiblissez ni n'excluez les règles du linter ; ne supprimez une seule ligne qu'avec une règle nommée et une raison.
  • Noms de récepteur courts, cohérents sur les méthodes d'un type ; ne mélangez pas les récepteurs pointeur et valeur sur un même type sans raison.
  • Les commentaires de documentation commencent par le nom de l'identifiant et énoncent le contrat.
  • Commentez le pourquoi, pas le quoi. Adaptez-vous à la densité de commentaires environnante.
  • Bibliothèque standard d'abord (slices, maps, strings, math/bits) avant les fonctions d'aide écrites à la main.
  • Ne modifiez jamais manuellement du code généré.

Lors de la révision ou de la refactorisation

  1. Identifiez le chemin chaud et ses benchmarks avant de toucher à quoi que ce soit.
  2. Mesurez la complexité pour trouver les pires fonctions ; corrigez-les d'abord.
  3. Refactorisez par petites étapes, en exécutant des tests ciblés après chacune.
  4. Gardez le comportement identique : mêmes résultats, mêmes budgets et solutions de repli, mêmes garanties d'allocation. Ne modifiez le comportement qu'en tant que décision distincte et déclarée.
  5. Signalez ce qui a changé, les chiffres de complexité avant/après et les résultats des benchmarks, et tout ce qui n'a pas été vérifié.

Skills associés