Introduction : Pourquoi votre service Go a besoin d'expériences
\n\nImaginez : vous avez réécrit la page de paiement sur une nouvelle stack. Tout fonctionne, le code Go est rapide, les tests sont verts. Mais le taux de conversion a chuté de 15 %. Quels que soient vos efforts, il est impossible de prédire le comportement des utilisateurs en conditions réelles. C'est ici qu'intervient le test A/B en Go — le seul moyen de prendre des décisions basées sur les données, et non sur l'intuition.
\n\nDans cet article, nous allons voir comment intégrer le test fractionné Golang dans votre application. Vous apprendrez à implémenter un système de buckets, à éviter la malédiction des comparaisons multiples et à ne pas casser la production. C'est parti !
\n\n1. Architecture de base d'un test A/B en Go
\n\nTout test A/B Golang repose sur trois piliers : le répartiteur de trafic, le système de journalisation et le moteur statistique. Commençons par le premier.
\n\n1.1. Répartition déterministe : l'algorithme des buckets
\n\nRègle clé : un même utilisateur doit toujours tomber dans le même groupe. Utilisons un hachage cohérent basé sur l'ID utilisateur :
\n\npackage abtest\n\nimport (\n "crypto/sha256"\n "encoding/binary"\n "fmt"\n)\n\n// Experiment définit la structure de l'expérience\ntype Experiment struct {\n Name string\n Buckets []Bucket\n}\n\ntype Bucket struct {\n Name string\n Ratio float64 // 0.0 - 1.0\n}\n\n// AssignGroup retourne le nom du groupe pour userID\nfunc (e *Experiment) AssignGroup(userID string) string {\n hash := sha256.Sum256([]byte(fmt.Sprintf("%s:%s", e.Name, userID)))\n value := binary.BigEndian.Uint64(hash[:8]) % 1000\n threshold := uint64(0)\n \n for _, b := range e.Buckets {\n threshold += uint64(b.Ratio * 1000)\n if value < threshold {\n return b.Name\n }\n }\n return "control" // fallback\n}\n\n\nPourquoi sha256 ? Il offre une répartition uniforme et résiste aux collisions. Le bucket testing Go avec cette approche garantit qu'un utilisateur ne sautera pas entre les groupes lors d'un redémarrage du serveur.
1.2. Middleware pour serveur HTTP
\n\nIntégrons le test A/B Go dans votre routeur. Exemple avec net/http :
func ABMiddleware(next http.Handler) http.Handler {\n return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n userID := r.Header.Get("X-User-ID") // ou depuis un cookie\n if userID == "" {\n next.ServeHTTP(w, r)\n return\n }\n \n exp := &Experiment{\n Name: "checkout_v2",\n Buckets: []Bucket{\n {Name: "control", Ratio: 0.5},\n {Name: "treatment", Ratio: 0.5},\n },\n }\n \n group := exp.AssignGroup(userID)\n ctx := context.WithValue(r.Context(), "ab_group", group)\n next.ServeHTTP(w, r.WithContext(ctx))\n })\n}\n\n\nMaintenant, dans vos handlers, vous pouvez lire le groupe via r.Context().Value("ab_group"). Simple et efficace.
2. Techniques avancées : Feature Flags et bandits manchots
\n\nUn simple A/B c'est bien, mais dans les expériences en Golang réelles, la flexibilité est souvent nécessaire.
\n\n2.1. Feature Flags avec activation dynamique
\n\nParfois, il faut activer une fonctionnalité pour 10 % des utilisateurs, puis augmenter progressivement jusqu'à 100 %. Implémentons cela avec Redis :
\n\ntype DynamicExperiment struct {\n Client *redis.Client\n Name string\n}\n\nfunc (d *DynamicExperiment) IsEnabled(userID string) bool {\n // Récupère le pourcentage actuel depuis Redis (modifiable sans déploiement)\n percent, err := d.Client.Get(context.Background(), d.Name).Float64()\n if err != nil {\n return false // désactivé par défaut\n }\n \n hash := sha256.Sum256([]byte(userID))\n value := binary.BigEndian.Uint64(hash[:8]) % 1