go 的 math/rand 默认不随机是因为默认种子固定为1,导致相同序列;go 1.20+ 虽自动初始化但仍有复现风险;应避免 rand.seed(),改用显式构造独立 *rand.rand 实例并注意并发与性能。

Go 的 math/rand 默认不“随机”——它每次运行都返回相同序列,根本原因不是 bug,而是设计如此:默认种子固定为 1。
为什么 rand.Intn(100) 总是返回 84、84、84…
因为没设种子,math/rand 使用全局默认源,其初始种子硬编码为 1。相同种子 → 相同线性同余序列 → 首次调用 rand.Intn(100) 恒得 84。这不是偶然,是确定性行为。
- 现象:
rand.Intn(100)在未初始化时,无论运行多少次,前几个数完全一致 - 根源:Go 1.20 前默认种子是 1;Go 1.20+ 启动时自动用 runtime 随机数初始化,但**仅限首次调用**,且仍可能复现(尤其在容器或快速重启场景)
- 误区:以为
time.Now().Unix()就够了 —— 秒级精度下,同一秒内多次调用会反复设相同种子,反而强化重复
rand.Seed() 已弃用,但很多人还在用
Go 1.20 起 rand.Seed() 被标记为 deprecated,因为它修改全局状态,且并发调用会 panic(文档明确写 Seed should not be called concurrently with any other Rand method)。
- 危险操作:
rand.Seed(time.Now().UnixNano())放在循环里或 HTTP handler 中 → 高并发下直接卡死或超时 - 替代方案必须显式构造独立实例:
r := rand.New(rand.NewSource(time.Now().UnixNano())) - 注意:
rand.NewSource()返回的是rand.Source接口,不能直接调用Int63()等方法,必须包一层rand.New()
并发场景下,别共享同一个 *rand.Rand
单个 *rand.Rand 实例本身是并发安全的(内部带锁),但锁争用会拖慢性能;更糟的是,如果多个 goroutine 都试图调用它的 Seed() 方法(哪怕你没写,别人库可能间接调),就会 panic。
- 安全做法:每个 goroutine 自建实例,或用
sync.Pool复用 —— 尤其在 Web server 或 worker pool 中 - 反模式:
var globalRand = rand.New(rand.NewSource(time.Now().UnixNano()))然后到处用globalRand.Intn()—— 看似简洁,实则埋雷 - 性能提示:
rand.Float64()比Intn()开销略小;若需大量随机整数,优先考虑Int63n()(支持 int64 范围,避免 int32 溢出)
测试时需要可复现,但生产要真变化
种子不是越“随机”越好,而是要匹配场景:单元测试需固定种子保证可复现;生产环境需纳秒级变化避免序列碰撞。
- 测试写法:
r := rand.New(rand.NewSource(42))—— 所有 test 运行结果一致,便于断言 - 生产写法:
r := rand.New(rand.NewSource(time.Now().UnixNano()))—— 但注意:不能在热路径反复 new,应提前构建并复用 - 容易忽略的细节:
rand.Intn(0)会 panic;负数范围必须手动偏移,比如r.Intn(10) - 5表示 [-5, 5)
真正难的不是写对一行种子代码,而是在高并发、长生命周期、多 goroutine 共享状态的系统里,让随机性既稳定又不可预测——这要求你理解 Source 和 Rand 的分离设计,而不是把 rand.Intn 当黑盒调用。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











