Go标准库math/rand不提供加权随机选择,需手动构建前缀和数组并用sort.Search实现O(log n)查找;权重须非负且总和大于0,前缀和应预计算而非每次抽奖重复生成。

为什么 math/rand 的默认 Float64() 不够用
直接用 rand.Float64() 均匀采样,再映射到奖品列表,只能实现等概率抽奖。权重逻辑必须自己建模——本质是把权重数组转成一个“累积分布”,然后用随机数落点查表。Go 标准库不提供现成的加权随机选择函数,得手动构造前缀和 + 二分查找,否则遍历匹配在权重项多时(比如上千个奖品)会明显变慢。
用 sort.Search 实现 O(log n) 加权随机抽取
核心思路:预计算权重前缀和数组,每次抽奖生成 [0, totalWeight) 内的随机浮点数,用 sort.Search 在前缀和中找第一个 ≥ 该值的位置。这个位置就是中奖索引。
实操要点:
- 权重必须是非负整数或浮点数;全零会导致除零 panic,需提前校验
- 前缀和用
float64累加可避免整数溢出,但要注意浮点精度对极小权重的影响(比如 1e-15 权重可能被截断) -
sort.Search要求切片升序,前缀和天然满足,无需额外排序 - 务必使用
rand.New(rand.NewSource(time.Now().UnixNano()))初始化独立 rng,避免多个 goroutine 共享全局rand导致重复序列
// 示例:权重 [10, 20, 70] → 前缀和 [10, 30, 100]
weights := []float64{10, 20, 70}
prefix := make([]float64, len(weights))
prefix[0] = weights[0]
for i := 1; i r := rand.New(rand.NewSource(time.Now().UnixNano()))
val := r.Float64() * total
idx := sort.Search(len(prefix), func(i int) bool { return prefix[i] >= val })
// idx 即中奖下标
遇到 panic: invalid argument to Intn 怎么办
这个错误通常出现在你误用了 rand.Intn(0)——比如权重全为零、或前缀和长度为 0 时调用 sort.Search 未加保护。更隐蔽的情况是:你在初始化前缀和前没检查 len(weights) == 0,导致 prefix 为空,sort.Search(0, ...) 返回 0,后续用该索引访问原数组就 panic。
安全写法必须包含:
- 初始化前断言
len(weights) > 0 - 检查所有权重 ≥ 0,且至少有一个 > 0
- 若允许动态更新权重,每次变更后要重新计算前缀和,不能复用旧数组
要不要用 github.com/yourbasic/rand 这类第三方包
它提供了 Weighted 类型封装前缀和与查找逻辑,接口更简洁。但代价是:内部仍用相同算法,且引入了额外依赖。如果你项目已用 Go 1.21+,sort.Search 性能足够好,无必要引入;只有当需要支持「在线增删权重」或「带缓存的高频抽奖」时,才值得评估专用库。注意某些包默认用全局 rand,并发场景下必须显式传入自定义 *rand.Rand 实例。
真正容易被忽略的是权重归一化时机——很多人在每次抽奖前都重新算一遍前缀和,这在高频调用时成了性能瓶颈。前缀和只需在权重变更后重算,静态配置应只做一次。











