A/B-Testing in Go: Traffic wie ein Profi splitten

Online Python-Trainer für Anfänger

Lernen Sie Python einfach ohne theoretische Überlastung. Lösen Sie praktische Aufgaben mit automatischer Überprüfung und schreiben Sie Code direkt im Browser.

Kurs starten

Einleitung: Warum Ihr Go-Service Experimente braucht

\n\n

Stellen Sie sich vor: Sie haben die Zahlungsseite mit einem neuen Stack neu geschrieben. Alles funktioniert, der Go-Code fliegt, die Tests sind grün. Aber die Konversion ist um 15 % gefallen. So sehr Sie sich auch bemühen, unter realen Bedingungen ist das Benutzerverhalten nicht vorhersagbar. Hier kommt das A/B-Testing in Go ins Spiel – die einzige Möglichkeit, Entscheidungen auf Basis von Daten und nicht von Intuition zu treffen.

\n\n

In diesem Artikel werden wir uns ansehen, wie Sie Split-Testing in Golang in Ihre Anwendung integrieren. Sie erfahren, wie Sie ein Bucket-System implementieren, den Fluch des multiplen Vergleichs vermeiden und die Produktion nicht beschädigen. Los geht's!

\n\n

1. Grundlegende Architektur eines A/B-Tests in Go

\n\n

Jeder A/B-Test in Golang besteht aus drei Säulen: dem Traffic-Splitter, dem Logging-System und der Statistik-Engine. Beginnen wir mit dem ersten.

\n\n

1.1. Deterministische Verteilung: Der Bucket-Algorithmus

\n\n

Die wichtigste Regel: Ein Benutzer sollte immer in dieselbe Gruppe fallen. Wir verwenden konsistentes Hashing basierend auf der Benutzer-ID:

\n\n
package abtest\n\nimport (\n    "crypto/sha256"\n    "encoding/binary"\n    "fmt"\n)\n\n// Experiment definiert die Struktur eines Experiments\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 gibt den Gruppennamen für die userID zurück\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\n

Warum sha256? Es bietet eine gleichmäßige Verteilung und ist kollisionsresistent. Bucket-Testing in Go mit diesem Ansatz stellt sicher, dass ein Benutzer beim Neustart des Servers nicht zwischen den Gruppen wechselt.

\n\n

1.2. Middleware für den HTTP-Server

\n\n

Integrieren Sie A/B-Testing in Go in Ihren Router. Ein Beispiel mit net/http:

\n\n
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") // oder aus 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\n

Jetzt können Sie in den Handlern die Gruppe über r.Context().Value("ab_group") auslesen. Einfach und effektiv.

\n\n

2. Fortgeschrittene Techniken: Feature Flags und mehrarmige Banditen

\n\n

Einfaches A/B ist gut, aber in realen Experimenten in Golang wird oft Flexibilität benötigt.

\n\n

2.1. Feature Flags mit dynamischer Aktivierung

\n\n

Manchmal muss man eine Funktion für 10 % der Benutzer aktivieren und dann schrittweise auf 100 % erhöhen. Wir implementieren dies über Redis:

\n\n
type DynamicExperiment struct {\n    Client *redis.Client\n    Name   string\n}\n\nfunc (d *DynamicExperiment) IsEnabled(userID string) bool {\n    // Den aktuellen Prozentsatz aus Redis holen (kann ohne Deployment geändert werden)\n    percent, err := d.Client.Get(context.Background(), d.Name).Float64()\n    if err != nil {\n        return false // standardmäßig deaktiviert\n    }\n    \n    hash := sha256.Sum256([]byte(userID))\n    value := binary.BigEndian.Uint64(hash[:8]) % 1                

Blogs

Buchempfehlungen