
math/rand 生成的随机数默认不安全,每次运行结果都一样
直接调用 rand.Intn(10) 得到的永远是 1、5、8、8、2……这种固定序列。因为 rand 包内部用的是确定性算法,且默认种子是 1 —— 它根本没“随机”过。
必须手动用当前时间初始化种子,否则所有程序实例、所有测试、所有 Docker 容器跑出来的随机序列完全一致:
rand.Seed(time.Now().UnixNano())
注意:Go 1.20+ 已废弃 rand.Seed(),改用新方式(见下一条)。
Go 1.20+ 推荐用 rand.New(rand.NewSource()) 构造独立随机生成器
全局 rand 函数(如 rand.Intn())共享一个包级状态,多 goroutine 并发调用会竞争,还可能被其他库意外重置种子。更稳妥的做法是自己创建隔离的生成器:
立即学习“go语言免费学习笔记(深入)”;
- 用
rand.NewSource(time.Now().UnixNano())创建种子源 - 再用
rand.New()包装成线程安全的*rand.Rand实例 - 后续所有操作都调用该实例的方法,比如
r.Intn(100)
示例:
r := rand.New(rand.NewSource(time.Now().UnixNano())) n := r.Intn(100) // 每次运行结果不同,且不干扰其他 r 实例
crypto/rand 才是真随机,但开销大、不可复现
crypto/rand 从操作系统熵池读取(Linux 的 /dev/urandom,Windows 的 BCryptGenRandom),输出不可预测、不可复现,适合生成密钥、token、salt 等安全敏感场景。
但它不能替代 math/rand 做模拟、测试数据或游戏逻辑 —— 因为:
- 性能差:每次调用都要进内核,比
math/rand慢 100 倍以上 - 不可复现:无法设置种子,调试和单元测试会变得极其困难
- 可能阻塞:极少数低熵环境(如容器启动初期)下
Read()可能短暂等待
用法示例(生成 8 字节随机 ID):
buf := make([]byte, 8)
_, err := crypto/rand.Read(buf)
if err != nil {
panic(err)
}
测试时别硬编码 seed,用 t.Setenv 或参数控制
写单元测试时,需要可复现的随机行为,但又不能把 rand.Seed(42) 写死在代码里 —— 这会让所有测试共享同一份随机流,相互干扰。
推荐做法是:让测试通过环境变量或命令行参数注入种子,比如:
- 测试前设
t.Setenv("RAND_SEED", "12345") - 主逻辑中检查
os.Getenv("RAND_SEED"),存在则解析为 int64 并传给rand.NewSource() - CI 中可统一设固定 seed,本地开发则留空,用系统时间
这样既保测试稳定,又不牺牲日常开发的随机性。
真正麻烦的不是选哪个包,而是混用它们:比如误把 crypto/rand 当作 math/rand 的“升级版”去生成抽奖号码 —— 结果服务一压测就卡住,还查不出为什么。










