
本文详解 go 中 math/rand 的常见误用:避免循环内重复 seed、理解 intn(n) 的取值范围,并提供安全、可复现的随机数生成实践方案。
在 Go 中模拟抛硬币(即生成等概率的 0 或 1)时,初学者常因对 math/rand 的设计机制理解不足而陷入两个典型陷阱:在循环中反复调用 rand.Seed() 和 误用 rand.Intn(n) 的参数含义。这不仅导致结果完全不随机(如始终输出 0),还可能掩盖并发安全与可测试性问题。
✅ 正确做法:单次初始化 + 合理范围
rand.Seed() 仅需在程序启动时调用一次(通常在 main() 开头或 init() 函数中),用于初始化全局随机数生成器的种子。若在高频循环中反复调用(如每毫秒多次),由于 time.Now().UnixNano() 在纳秒级精度下可能未变化(尤其在短循环或高负载环境下),会导致多次使用相同种子,从而产生完全相同的“随机”序列。
更关键的是:rand.Intn(n) 返回的是 [0, n) 区间内的整数(含 0,不含 n)。因此 rand.Intn(1) 永远只返回 0;要获得 0 或 1,必须传入 2:
package main
import (
"fmt"
"math/rand"
"time"
)
func main() {
// ✅ 正确:仅在程序开始时 Seed 一次
rand.Seed(time.Now().UnixNano())
var heads, tails int
for heads < 5 && tails < 5 {
// ✅ 正确:Intn(2) → 返回 0 或 1
if rand.Intn(2) == 0 {
heads++
} else {
tails++
}
}
fmt.Printf("Heads: %d, Tails: %d\n", heads, tails)
}⚠️ 注意事项与现代推荐
-
Go 1.20+ 更推荐使用 rand.New(rand.NewSource(...)):全局 rand.* 函数(如 rand.Intn)是非线程安全的,且无法独立控制种子。生产代码应显式创建私有 *rand.Rand 实例:
src := rand.NewSource(time.Now().UnixNano()) r := rand.New(src) coin := r.Intn(2) // 安全、可测试、支持并发
可测试性:为单元测试,可传入固定种子的 rand.Source(如 rand.NewSource(42)),确保行为可重现。
不要用 time.Now().UnixNano() 作为密码学随机源:math/rand 是伪随机数生成器(PRNG),不适用于加密场景。如需安全随机数,请使用 crypto/rand。
总结
生成一个公平的“硬币翻转”,核心是两点:一粒种子,一次播种;一个范围,正确理解——Intn(2) 是你的朋友,Intn(1) 是陷阱。结合显式随机源实例化,你将写出更健壮、可维护、可测试的 Go 随机逻辑。










