Pruebas A/B en Go: Dividiendo el tráfico como un profesional

Entrenador de Python en Línea para Principiantes

Aprende Python fácilmente sin sobrecargarte de teoría. Resuelve tareas prácticas con verificación automática, recibe pistas y escribe código directamente en el navegador, sin instalar nada.

Start Course

Introducción: Por qué su servicio Go necesita experimentos

\n\n

Imagine: reescribió la página de pago con una nueva pila. Todo funciona, el código en Go vuela, las pruebas están en verde. Pero la conversión cayó un 15%. Por más que lo intente, en condiciones reales es imposible predecir el comportamiento de los usuarios. Aquí es donde entran en escena las pruebas A/B en Go, la única forma de tomar decisiones basadas en datos, no en la intuición.

\n\n

En este artículo, veremos cómo implementar pruebas divididas en Golang en su aplicación. Aprenderá cómo implementar un sistema de buckets, evitar la maldición de las comparaciones múltiples y no romper la producción. ¡Vamos allá!

\n\n

1. Arquitectura básica de una prueba A/B en Go

\n\n

Cualquier prueba A/B en Golang se compone de tres pilares: el divisor de tráfico, el sistema de registro y el motor estadístico. Empecemos con el primero.

\n\n

1.1. Asignación determinista: algoritmo de buckets

\n\n

Regla clave: un usuario siempre debe caer en el mismo grupo. Usamos hashing consistente basado en el ID del usuario:

\n\n
package abtest\n\nimport (\n    "crypto/sha256"\n    "encoding/binary"\n    "fmt"\n)\n\n// Experiment define la estructura del experimento\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 devuelve el nombre del grupo para 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

¿Por qué sha256? Proporciona una distribución uniforme y es resistente a colisiones. Las pruebas de bucket en Go con este enfoque garantizan que un usuario no salte entre grupos al reiniciar el servidor.

\n\n

1.2. Middleware para servidor HTTP

\n\n

Integramos las pruebas A/B en Go en su enrutador. Ejemplo con 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") // o desde 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

Ahora en los handlers puede leer el grupo a través de r.Context().Value("ab_group"). Simple y efectivo.

\n\n

2. Técnicas avanzadas: Feature Flags y bandidos multi-brazo

\n\n

Un A/B simple está bien, pero en experimentos reales en Golang a menudo se necesita flexibilidad.

\n\n

2.1. Feature Flags con activación dinámica

\n\n

A veces es necesario activar una función para el 10% de los usuarios y luego aumentarla gradualmente al 100%. Lo implementamos a través de Redis:

\n\n
type DynamicExperiment struct {\n    Client *redis.Client\n    Name   string\n}\n\nfunc (d *DynamicExperiment) IsEnabled(userID string) bool {\n    // Obtenemos el porcentaje actual de Redis (se puede cambiar sin desplegar)\n    percent, err := d.Client.Get(context.Background(), d.Name).Float64()\n    if err != nil {\n        return false // por defecto desactivado\n    }\n    \n    hash := sha256.Sum256([]byte(userID))\n    value := binary.BigEndian.Uint64(hash[:8]) % 1                

Blogs

Book Recommendations