直接用rand.Intn不行,因权重非等概率;需用前缀和+二分查找实现O(log n)加权随机抽取,避免浮点误差与边界问题,并须预过滤权重≤0的奖品。
为什么直接用 rand.Intn 不行
因为奖品权重不是等概率分布。比如 a 奖品权重 70、b 是 20、c 是 10,你不能简单对 3 个奖品做 rand.intn(3)——那会变成均匀分布,完全忽略权重。核心是把「权重」映射成「连续区间」,再用随机数落点判断归属。
用前缀和 + 二分查找是最稳的方案
这是时间复杂度 O(log n)、空间 O(n),且无浮点误差的标准解法。步骤很直白:
- 预计算权重前缀和数组:
prefix[i]表示前 i 个奖品(含)的累计权重 - 生成
[1, totalWeight]范围内的随机整数target - 在
prefix中二分查找第一个 ≥target的索引
示例代码片段:
// weights = [70, 20, 10], total = 100
prefix := []int{70, 90, 100}
target := rand.Intn(100) + 1 // [1,100]
// target=75 → 二分找到 prefix[1]=90 ≥ 75 → 选第2个奖品(B)
注意:必须用 +1 偏移,因为前缀和从 1 开始计数;sort.Search 可直接复用。
避免用 rand.Float64() 做归一化采样
有人会把权重转成概率(如 0.7, 0.2, 0.1),再用 rand.Float64() 落区间。这看似简洁,但有隐患:
- 浮点精度问题可能导致边界误判(尤其权重值极大或极小时)
- 无法精确复现整数权重语义(比如权重 1 和 1000000 在 float64 下可能被压缩到同一精度桶)
- 如果后续要支持「权重动态更新」,浮点方案更难维护一致性
除非你的奖品总数极少(≤5)、权重全为小整数且不关心可重现性,否则别碰浮点归一化。
权重为 0 的奖品必须提前过滤
如果某奖品权重是 0,它不该出现在前缀和数组里,否则会导致 prefix 长度与实际可选奖品数不一致,二分结果错位。更危险的是,若所有权重都是 0,totalWeight 为 0,rand.Intn(0) 会 panic。
实操建议:
- 初始化时遍历一次权重切片,跳过 ≤0 的项,并记录有效奖品列表
- 检查
len(validWeights) == 0,提前返回错误或默认值 - 不要在每次抽取时动态过滤——前缀和只构建一次,性能关键
边界情况比想象中多:权重全 0、某个权重超 int 范围、奖品名重复但权重不同……这些都在初始化阶段掐掉最省心。











