Le hasard est une chose trop sérieuse pour être laissée au hasard
\n\nQuiconque a déjà écrit du code en Go a été confronté à la génération de nombres aléatoires. Le générateur de nombres aléatoires (RNG) est une chose perfide. En apparence, tout est simple : on appelle rand.Intn() et on obtient un nombre. Mais si on ne prête pas attention à l'initialisation correcte (seed), votre « hasard » se transforme en une séquence prévisible qui cassera les tests, décevra en cryptographie ou surprendra simplement par des résultats identiques à chaque exécution.
Sur Stack Overflow, la question « How to properly seed random number generator » a recueilli des milliers de vues. Les gens se perdent avec time.Now().UnixNano(), utilisent crypto/rand là où ce n'est pas nécessaire, et souffrent de faibles performances. Aujourd'hui, nous allons analyser ce sujet avec Go — sans blabla, avec de vrais benchmarks et des solutions prêtes à l'emploi.
Pourquoi le seed n'est-il pas une simple formalité ?
\n\nLe seed (germe) est l'état initial du générateur. Un même germe donne une même séquence de nombres. C'est pratique pour le débogage, mais désastreux en production si vous attendez de l'imprévisibilité.
\n\nEn Go, le package standard math/rand utilise un algorithme déterministe. Si vous oubliez d'appeler rand.Seed(), alors... Avant Go 1.20, le générateur par défaut utilisait un germe fixe (généralement 1). Depuis Go 1.20, math/rand est automatiquement initialisé avec une valeur aléatoire au démarrage du programme. Cependant, se fier à cela dans des endroits critiques est une mauvaise pratique.
Le principal problème des débutants est de tenter d'initialiser le générateur pour chaque nombre aléatoire :
\n\n// ❌ MAUVAIS : on recrée le générateur à chaque fois\nfunc getRandom() int {\n r := rand.New(rand.NewSource(time.Now().UnixNano()))\n return r.Intn(100)\n}\n\nPourquoi est-ce mauvais ? time.Now().UnixNano() donne des nanosecondes, mais en réalité la précision dépend du système d'exploitation. Sous Windows, par exemple, elle peut être d'environ 15 ms. Si vous appelez la fonction deux fois de suite, vous obtiendrez des nombres identiques. De plus, chaque appel à rand.NewSource crée un nouvel objet, ce qui sollicite le ramasse-miettes.
Top 3 des approches pour l'initialisation en Go
\n\n1. Méthode classique : un seed global une seule fois
\n\nL'option la plus simple et la plus correcte pour la plupart des applications est de définir le seed une seule fois au démarrage du programme.
\n\npackage main\n\nimport (\n "fmt"\n "math/rand"\n "time"\n)\n\nfunc main() {\n // ✅ BON : on initialise une seule fois\n rand.Seed(time.Now().UnixNano())\n\n for i := 0; i < 5; i++ {\n fmt.Println(rand.Intn(100))\n }\n}\n\nImportant : time.Now().UnixNano() donne une valeur unique, mais si le programme est exécuté plusieurs fois dans la même nanoseconde (peu probable, mais théoriquement possible), le seed sera identique. Pour 99 % des applications, cela suffit.
2. Générateur local pour l'isolation
\n\nLorsque vous avez plusieurs goroutines ou que vous ne voulez pas influencer l'état global, créez votre propre instance de rand.Rand.
func NewSeededGenerator() *rand.Rand {\n seed := time.Now().UnixNano()\n return rand.New(rand.NewSource(seed))\n}\n\n// Utilisation :\nrng := NewSeededGenerator()\nfor i := 0; i < 3; i++ {\n fmt.Println(rng.Intn(100))\n}\n\nCette approche offre un contrôle total : différentes goroutines peuvent avoir leurs propres générateurs, et elles ne se bloquent pas mutuellement (le rand global utilise un mutex).
3. Seed cryptographique pour les tâches critiques
\n\nSi vous avez besoin d'un vrai hasard (jetons, mots de passe, cryptographie) — oubliez math/rand. Utilisez crypto/rand :
import (\n "crypto/rand"\n "encoding/binary"\n "math/rand"\n)\n\nfunc CryptoSeed() int64 {\n var buf [8]byte\n _, err := rand.Read(buf[:])\n if err != nil {\n