最简无状态 a/b 分流需基于稳定标识哈希取模:先取请求中 x-user-id 等固定字段,空则设为 "anon";再用 sha256.sum256 哈希后取首字节 % 2 映射到 a 或 b;务必在缓存 key、下游调用、埋点日志中显式携带 version,避免仅设 header 导致分流失效。

怎么用 net/http 做最简 A/B 分流
Go 里没有内置 A/B 测试框架,但靠标准库就能实现核心逻辑:按请求特征(比如用户 ID、cookie、Header)决定返回版本 A 还是 B。关键不是“搭系统”,而是“不引入额外状态”地做无状态分流。
常见错误是直接在 handler 里用 rand.Intn(2) —— 看似随机,实则对同一用户多次请求可能分到不同版本,破坏实验一致性。
- 必须基于稳定标识做哈希,比如
r.Header.Get("X-User-ID")或http.Cookie中的user_id - 推荐用
sha256.Sum256哈希后取低字节,再% 2,比rand可复现 - 别用时间戳或请求序号——它们无法跨实例一致
func abHandler(w http.ResponseWriter, r *http.Request) {
id := r.Header.Get("X-User-ID")
if id == "" {
id = "anon"
}
h := sha256.Sum256([]byte(id))
slot := int(h[0]) % 2 // 稳定映射到 0 或 1
version := []string{"A", "B"}[slot]
w.Header().Set("X-AB-Version", version)
// 后续逻辑按 version 分支处理
}
为什么不能只靠中间件加 Header 就算完成 A/B
加个 X-AB-Version 是起点,不是终点。真实场景中,A/B 影响的是业务逻辑分支、模板渲染、甚至下游 API 调用参数——这些都得显式适配,否则 Header 只是摆设。
典型坑:前端看到 Header 就以为“已生效”,结果后端数据库查询、缓存 key、埋点上报全走统一路径,AB 实际没产生任何差异。
- 缓存 key 必须包含 AB 版本,例如
cacheKey := fmt.Sprintf("home:%s:%s", userID, version) - 调用下游服务时,需透传
version并让对方识别(比如加 query 参数?ab=v2) - 埋点日志里漏掉
ab_version字段,后续根本没法统计转化率差异
gorilla/sessions 存 AB 分组会导致什么问题
用 session 强制记住用户版本看似稳妥,实际会破坏水平扩展能力和测试可重复性。
错误现象:本地开发一切正常,上线后部分用户版本来回跳变;压测时 session 存储成瓶颈;灰度期间想快速切回旧版,却要清空所有 session 数据。
- session 依赖存储(Redis / 文件),增加运维复杂度和延迟
- 用户换设备、清 cookie 后丢失分组,导致同一个人被重复实验
- 无法做「固定百分比流量进 B」这种全局策略,只能做到「某用户固定进 B」
- 真正需要持久化分组的场景极少(比如法律要求用户明确同意才进新流程),默认应优先无状态
如何验证 AB 分流是否均匀且无偏
上线后第一件事不是看转化率,而是确认分流本身是否符合预期——95% 的 AB 失败源于分流歪了。
常见偏差:ID 字符串哈希后取模,但大量用户 ID 为空或为固定值(如 "-" ),导致 90% 流量进 A;或用客户端 IP 哈希,结果内网用户全挤在同一个 C 段,全部分到同一组。
- 上线前用真实日志抽样跑一遍分流逻辑,统计各版本占比(目标误差
- 监控
ab_version上报分布,告警偏离阈值(如连续 5 分钟 B 版占比 - 避免用易受攻击或伪造的字段(如
User-Agent)做分流依据
真正的难点从来不在代码几行写完,而在于你选的那个分流依据,在真实流量里是否真的独立、稳定、可追溯。漏掉这一环,后面所有数据结论都不可信。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











