因为rand.intn()使用带sync.mutex的全局rand.rand实例,100个goroutine并发调用时争抢同一把锁,导致串行排队和runtime.futex高占比;正确做法是每个goroutine新建独立rand.rand实例并用差异化种子(如time.now().unixnano() ^ int64(unsafe.pointer(&x)))彻底消除锁竞争。

math/rand全局函数为什么一并发就变慢
因为 rand.Intn() 这类包级函数背后是同一个带 sync.Mutex 的全局 *rand.Rand 实例。100 个 goroutine 同时调用,实际是排队抢锁,pprof 里一眼看到 runtime.futex 占比飙升。这不是算法慢,是锁争用把吞吐压垮了。
- 哪怕只读状态(比如
Intn),内部也要更新原子计数器,争用不可避免 - Go 1.20+ 虽默认初始化了全局源,但没解决并发瓶颈
- 别指望加个
sync.Mutex包一层能救——串行化比原来还糟
每个goroutine该不该自己new一个rand.Rand
该,而且最简单可靠。结构体只有约 40 字节,分配开销可忽略,关键是彻底消灭锁竞争。
- 种子不能雷同:高并发下
time.Now().UnixNano()极易重复,得加扰动,比如time.Now().UnixNano() ^ int64(unsafe.Pointer(&x)) - 更稳妥的做法是启动时用
crypto/rand.Read()生成一次 8 字节种子,之后分发给各实例 - 别在
init()里只调一次rand.Seed()—— 那只初始化全局实例,不解决 goroutine 间竞争
crypto/rand真比math/rand慢到不能用吗
单次 crypto/rand.Read() 在现代 Linux 上约 100–300ns,远低于 HTTP handler 常见开销。所谓“慢”,几乎全是误用导致的:
在 Golang 中使用 samber/hot 进行内存缓存,支持 LRU、LFU、TinyLFU、W‑TinyLFU、S3FIFO、ARC、TwoQueue、SIEVE、FIFO 等淘汰算法,提供 TTL、缓存加载器及分片功能。
- 在 tight loop 里反复调
Read()每次只读 1 字节 → 改成一次性读大 buffer(如 1KB) - 为每个验证码新开 goroutine 调
crypto/rand→ 它天生并发安全,直接复用即可 - 拿它生成游戏抽卡结果 → 杀鸡用牛刀,该换回
math/rand
什么时候必须放弃math/rand改用crypto/rand
只要涉及「不能被预测」或「不能被重现」,就没有模糊地带。比如:
- 生成 JWT token 的密钥、session ID、API key、salt、nonce
- HTTP handler 里返回的验证码字符串(哪怕只是短时效)
- 用
rand.Perm(n)打乱用户 ID 列表后暴露给前端——对方知道大致时间就能还原顺序
最容易被忽略的是:你写的代码是否会被别人当成“安全组件”间接调用。一旦边界模糊,就该默认走 crypto/rand。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










