math/rand 默认全局实例非并发安全,多goroutine调用会panic或返回重复值;crypto/rand用于密码学场景,天生并发安全但较慢;推荐为每个goroutine创建独立*rand.rand实例并合理设种子。

为什么 math/rand 在并发下会 panic 或返回重复值
因为 math/rand 的默认全局 Rand 实例(rand.Intn 等函数背后用的)不是并发安全的。多个 goroutine 同时调用 rand.Intn(100),可能触发 fatal error: concurrent map writes(如果底层用了 map),或更隐蔽地——种子没变、状态被覆盖,导致不同 goroutine 拿到完全相同的随机序列。
- 别在 init 函数里只调一次
rand.Seed(time.Now().UnixNano())就完事:这只能初始化全局实例,不解决并发竞争 - 每个 goroutine 自己 new 一个
rand.New并传入独立rand.NewSource是安全的,但要注意种子不能雷同(比如都用time.Now().UnixNano(),纳秒级在高并发下极易重复) - 更简单做法:用
rand.New(rand.NewSource(time.Now().UnixNano() ^ int64(goID())))——但 goID 不易获取,不推荐
什么时候必须用 crypto/rand,而不是修修补补 math/rand
crypto/rand 提供的是密码学安全伪随机数(CSPRNG),核心用途只有一个:生成密钥、token、salt、nonce 这类不能被预测的东西。它不快,也不适合做游戏逻辑或负载均衡的随机选节点。
- 如果你在写
http.HandlerFunc里生成 JWT token 的签名密钥,必须用crypto/rand;用math/rand就是把密钥暴露给攻击者 -
crypto/rand没有Intn这种便利方法,得自己读字节再裁剪:比如取 0–99 范围,得用rand.Read(buf[:])+ 模运算 + 拒绝采样防偏斜 - 它在 Windows 上走
BCryptGenRandom,Linux/macOS 走/dev/urandom,不依赖 Go 运行时,所以天生并发安全——但这不是设计目标,只是副作用
并发安全又够快的 math/rand 替代方案
Go 1.20+ 开始,math/rand 加了 rand.NewPCG 和线程本地支持,但最实用的还是显式管理独立实例 + 合理种子。
- 为每个 goroutine 创建独立
*rand.Rand:用rand.New(rand.NewSource(seed)),seed 推荐用time.Now().UnixNano() + uintptr(unsafe.Pointer(&x))(x 是局部变量)避免碰撞 - 如果 goroutine 数量固定且不多(比如 worker pool),提前建好
[]*rand.Rand池,按索引取用,比每次 new 更省 GC 压力 - 切忌用
sync.Mutex包裹全局rand调用:锁争用会让随机数生成变成串行瓶颈,尤其在大量 goroutine 场景下
crypto/rand 读取失败时的真实表现和应对
crypto/rand 的 Read 方法返回 error,不是 panic。但在容器环境(如某些 Kubernetes 配置错误的 Pod)、chroot 或嵌入式系统中,/dev/urandom 可能不可读,这时你会拿到 io.ErrUnexpectedEOF 或 syscall.EPERM。
立即学习“go语言免费学习笔记(深入)”;
- 不要忽略错误:写成
_, _ = rand.Read(buf[:])是危险的,buf 会保持零值 - 生产代码里至少要 log 错误并 fallback 到 panic(因为 CSPRNG 失败通常意味着系统异常,不该静默降级)
- 测试时可以用
rand.Reader = &fakeReader{}(需导出包变量)来模拟失败,但注意crypto/rand的Reader是 var,可直接赋值
*rand.Rand 实例;混用或忽略错误,迟早在线上咬你一口。










