功能说明
灰度发布、A/B 实验、负载分配常需要按比例选择:百分之一的流量进入金丝雀,百分之三十走稳定版本,其余走旧版本。加权随机只需要生成一个零到总权重之间的随机数,再顺序累减各选项权重,落在哪个区间就选中哪个,时间复杂度与选项数成正比。
示例用权重一、三、六的三项做一万次抽样,期望比例分别是百分之十、百分之三十和百分之六十,打印实际命中次数与百分比,便于确认实现正确。
关键代码
Pick 先校验选项非空、权重非负且总和为正,避免非法配置被静默接受。随机数用传入的 rand.Rand,便于固定种子复现。循环中若目标值小于当前权重即命中;否则减去当前权重继续。由于浮点误差,循环末尾兜底返回最后一项,保证函数总有结果。main 固定种子四十二,因此每次运行输出完全一致。
使用注意
math/rand 不适合安全场景,抽奖或令牌应使用 crypto/rand。权重为浮点数时可能出现极小概率的偏差,要求严格比例时可用整数权重。零权重选项永远不会被选中,但仍参与遍历。选项很多且调用频繁时,可以预计算累积权重并二分查找,把每次选择的复杂度降为对数级。修改切片后需要重新计算总权重。
验证方式
一万次抽样后三项的命中次数应接近一千、三千、六千,打印的百分比约为 10.0、30.0、60.0。负数权重会返回错误,输出提示为 true。把种子换成其他值,计数会变化但比例保持稳定;把某一项权重改为零,它的计数应保持为零。
代码示例
package main
import (
"errors"
"fmt"
"math/rand"
)
type Choice[T any] struct {
Item T
Weight float64
}
func Pick[T any](r *rand.Rand, choices []Choice[T]) (T, error) {
var zero T
if len(choices) == 0 {
return zero, errors.New("no choices")
}
total := 0.0
for _, choice := range choices {
if choice.Weight < 0 {
return zero, fmt.Errorf("negative weight for %v", choice.Item)
}
total += choice.Weight
}
if total <= 0 {
return zero, errors.New("total weight must be positive")
}
target := r.Float64() * total
for _, choice := range choices {
if target < choice.Weight {
return choice.Item, nil
}
target -= choice.Weight
}
return choices[len(choices)-1].Item, nil
}
func main() {
rng := rand.New(rand.NewSource(42))
choices := []Choice[string]{
{Item: "canary", Weight: 1},
{Item: "stable", Weight: 3},
{Item: "legacy", Weight: 6},
}
counts := map[string]int{}
for i := 0; i < 10000; i++ {
item, err := Pick(rng, choices)
if err != nil {
panic(err)
}
counts[item]++
}
for _, choice := range choices {
fmt.Printf("%s weight=%.0f picked=%d (%.1f%%)\n", choice.Item, choice.Weight, counts[choice.Item], float64(counts[choice.Item])/100)
}
_, err := Pick(rng, []Choice[string]{{Item: "bad", Weight: -1}})
fmt.Println("negative weight rejected:", err != nil)
}