Introducción: Por qué su servicio Go necesita experimentos
\n\nImagine: 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\nEn 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\n1. Arquitectura básica de una prueba A/B en Go
\n\nCualquier 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\n1.1. Asignación determinista: algoritmo de buckets
\n\nRegla clave: un usuario siempre debe caer en el mismo grupo. Usamos hashing consistente basado en el ID del usuario:
\n\npackage 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.
1.2. Middleware para servidor HTTP
\n\nIntegramos las pruebas A/B en Go en su enrutador. Ejemplo con 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") // 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\nAhora en los handlers puede leer el grupo a través de r.Context().Value("ab_group"). Simple y efectivo.
2. Técnicas avanzadas: Feature Flags y bandidos multi-brazo
\n\nUn A/B simple está bien, pero en experimentos reales en Golang a menudo se necesita flexibilidad.
\n\n2.1. Feature Flags con activación dinámica
\n\nA 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\ntype 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