A/B Testing in Go: Splitting Traffic Like a Pro

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

Introduction: Why Your Go Service Needs Experiments

\n\n

Imagine: you rewrote the payment page on a new stack. Everything works, the Go code flies, tests are green. But conversion dropped by 15%. No matter how hard you try, it's impossible to predict user behavior in production. This is where A/B testing in Go comes into play — the only way to make decisions based on data, not intuition.

\n\n

In this article, we'll break down how to implement split-testing Golang in your application. You'll learn how to implement a bucket system, avoid the curse of multiple comparisons, and not break production. Let's go!

\n\n

1. Basic Architecture of an A/B Test in Go

\n\n

Any A/B test Golang consists of three pillars: traffic splitter, logging system, and statistical engine. Let's start with the first one.

\n\n

1.1. Deterministic Distribution: Bucket Algorithm

\n\n

Key rule: one user should always fall into the same group. We use consistent hashing based on the user ID:

\n\n
package abtest\n\nimport (\n    "crypto/sha256"\n    "encoding/binary"\n    "fmt"\n)\n\n// Experiment defines the structure of an experiment\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 returns the group name for 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\n

Why sha256? It provides a uniform distribution and is resistant to collisions. Bucket testing Go with this approach guarantees that a user won't jump between groups when the server restarts.

\n\n

1.2. Middleware for HTTP Server

\n\n

Integrate A/B testing Go into your router. Example with 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") // or from 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

Now in handlers you can read the group via r.Context().Value("ab_group"). Simple and effective.

\n\n

2. Advanced Techniques: Feature Flags and Multi-Armed Bandits

\n\n

Simple A/B is good, but in real experiments in Golang flexibility is often needed.

\n\n

2.1. Feature Flags with Dynamic Rollout

\n\n

Sometimes you need to enable a feature for 10% of users, then gradually increase to 100%. Implement via Redis:

\n\n
type DynamicExperiment struct {\n    Client *redis.Client\n    Name   string\n}\n\nfunc (d *DynamicExperiment) IsEnabled(userID string) bool {\n    // Get the current percentage from Redis (can be changed without redeploy)\n    percent, err := d.Client.Get(context.Background(), d.Name).Float64()\n    if err != nil {\n        return false // disabled by default\n    }\n    \n    hash := sha256.Sum256([]byte(userID))\n    value := binary.BigEndian.Uint64(hash[:8]) % 1                

Blogs

Book Recommendations