math/rand需显式seed(如time.Now().UnixNano())以避免固定序列,高并发应避免共享全局实例而用独立rand.Rand或sync.Pool;crypto/rand仅适用于密钥等高安全场景,非必要勿用。

用 math/rand 生成普通随机数,但必须自己 seed
Go 的 math/rand 默认是伪随机,不调用 rand.Seed() 或 rand.New() 初始化的话,每次运行都返回相同序列——这不是 bug,是设计如此。常见错误是写完 rand.Intn(10) 发现结果总一样,其实只是忘了初始化。
正确做法是用当前时间做 seed,但注意:不要在多个 goroutine 里共用同一个全局 rand.Rand 实例且频繁调用 Seed(),这会引发竞态。更稳妥的是显式创建独立实例:
src := rand.NewSource(time.Now().UnixNano()) r := rand.New(src) n := r.Intn(100)
如果只是临时、单次、非安全场景用(比如 CLI 工具抽个测试 ID),也可以直接用包级函数,但务必只调用一次 rand.Seed(),且放在 main() 开头:
func main() {
rand.Seed(time.Now().UnixNano())
fmt.Println(rand.Intn(10))
}
crypto/rand 是真随机,但别用来做游戏或排序
crypto/rand 从操作系统熵池读取,速度慢、有阻塞风险,适合生成密钥、token、salt 这类对随机性质量要求极高的场景。拿它来生成游戏骰子点数或打乱切片顺序,纯属杀鸡用牛刀,还会拖慢性能,甚至在低熵环境(如容器、CI)里卡住。
立即学习“go语言免费学习笔记(深入)”;
典型误用是看到 “crypto” 就觉得“更高级”,于是把所有随机逻辑都换成 crypto/rand。实际上它连 Intn() 都没有,得自己读字节再转换:
buf := make([]byte, 8)
_, err := rand.Read(buf)
if err != nil { panic(err) }
n := int(binary.LittleEndian.Uint64(buf)) % 100
这段代码不仅啰嗦,还可能因 rand.Read() 返回部分字节而引入偏差(虽然概率低)。除非你在生成 JWT 密钥或 AES IV,否则没必要碰它。
并发安全:别在 goroutine 里共享默认 rand 实例
包级函数如 rand.Intn()、rand.Float64() 共享一个全局 *rand.Rand,内部用 mutex 保护——看似线程安全,但实际会成为性能瓶颈。压测时容易发现 runtime.futex 占比飙升。
高并发场景下,应该为每个 goroutine 分配独立的 *rand.Rand 实例,或者用 sync.Pool 复用:
- 用
sync.Pool时,注意New函数不能依赖外部状态(比如不能每次都用新时间 seed) - 如果 goroutine 生命周期短,直接 new 更简单;生命周期长、调用频繁,再考虑 pool
- seed 值建议用
time.Now().UnixNano() ^ int64(goroutineID)类方式微调,避免不同实例初始状态雷同
Go 1.20+ 的变化:默认 rand 已自动 seed,但仅限包级函数
Go 1.20 起,math/rand 包级函数(如 rand.Intn())首次调用时会自动调用 rand.Seed(time.Now().UnixNano())。这意味着你不再会得到固定序列——但这也掩盖了老代码中“忘记 seed”的问题,让迁移旧项目时更难定位随机性异常。
这个自动 seed 只影响包级函数,不影响你手动 new 的 *rand.Rand 实例。所以如果你写的是库,暴露了 *rand.Rand 参数,使用者仍需自己负责 seed 和并发安全。
另外,自动 seed 不解决可重现性需求:单元测试需要固定 seed 才能断言结果。这时候依然得显式传入 rand.New(rand.NewSource(42))。
真正容易被忽略的是:自动 seed 后,你没法再通过“不调用 Seed()”来触发确定性行为。想复现某次随机结果,必须记录 seed 值并显式传入——这点在调试分布式任务时特别关键。










