go 1.20+ 应优先使用 math/rand/v2 获取安全随机数,其默认使用加密熵源、零配置、线程安全;需可重现时才回退 math/rand 并手动创建 *rand.rand 实例。

Go 1.20+ 必须用 math/rand/v2,旧版 math/rand 全局函数已不安全
你跑出来的随机数每次一模一样?不是代码写错了,是没用对包。Go 1.20 起,math/rand 的全局函数(如 rand.Intn()、rand.Float64())默认仍用固定种子 1,且不自动从系统取熵——这意味着不显式初始化,结果就确定性重复。
math/rand/v2 是官方推荐的现代替代方案:它零配置、线程安全、默认使用 /dev/urandom 等加密安全熵源,不暴露种子接口,也不依赖时间戳。你只要导入、调用,就能得到真正不可预测的随机数。
- ✅ 正确做法:
import "math/rand/v2"<br>num := rand.IntN(100) // [0, 100)
- ❌ 别再用:
import "math/rand"+rand.Intn(100)(除非你明确要可重现,且接受不安全) - ⚠️ 注意:
math/rand/v2不支持固定种子——需要可重现(比如单元测试),必须降级回math/rand并手动构造*rand.Rand实例
需要可重现随机数时,必须自己建 *rand.Rand 实例,别碰全局状态
测试、压测、游戏存档回放……这些场景要求“相同输入 → 相同随机序列”。math/rand/v2 故意不支持这个,所以你得退回 math/rand,但绝不能用 rand.Seed()(Go 1.20+ 已弃用)或全局函数。
正确路径是:用 rand.NewSource() 创建一个带固定或时间种子的源,再用它生成独立的 *rand.Rand 实例。
- ? 固定种子(测试用):
src := rand.NewSource(42)<br>r := rand.New(src)<br>fmt.Println(r.Intn(10)) // 每次运行都输出相同序列
- ? 时间种子(生产非关键逻辑):
r := rand.New(rand.NewSource(time.Now().UnixNano()))
- ⚠️ 切忌:在函数里反复调用
rand.Seed(),或让多个模块共享同一个*rand.Rand实例却不加锁——会破坏可重现性或引发竞态
Intn(n) 是最常用方法,但范围计算和边界容易写错
rand.Intn(n) 返回的是 [0, n)(左闭右开),这是所有范围推导的起点。很多人想生成 [5, 15] 却写成 5 + rand.Intn(15),结果是 [5, 20),完全跑偏。
- ? 闭区间
[min, max]:min + r.Intn(max-min+1)(例如骰子:1 + r.Intn(6)) - ? 左闭右开
[min, max):min + r.Intn(max-min)(例如生成[5, 15):5 + r.Intn(10)) - ⚠️
n == 0会 panic,用户输入做参数前务必校验:if n - ? 浮点数同理:
a + r.Float64()*(b-a)得到[a, b);要包含b需额外处理,通常没必要
高并发下别共享 *rand.Rand,也别全靠全局锁扛压
虽然 *rand.Rand 实例本身是并发安全的(内部有锁),但在高频 goroutine 场景下,这个锁会成为瓶颈——尤其当你每毫秒调用几十次 Intn() 时。
- ⚡ 更优解:每个 goroutine 持有自己独立的
*rand.Rand实例(种子可用time.Now().UnixNano() ^ int64(goroutineID)稍作区分) - ⚡ 或用
sync.Pool复用实例,避免频繁分配:var randPool = sync.Pool{New: func() interface{} {<br> return rand.New(rand.NewSource(time.Now().UnixNano()))<br>} - ⚠️ 绝对不要:在循环里反复创建新实例却不释放(小对象还好),或把一个实例传给几十个 goroutine 还不做任何隔离
真正卡住人的从来不是语法,而是混淆「可重现」和「不可预测」——生产环境该用 math/rand/v2,测试才用带固定种子的 math/rand 实例。很多人第一次看到全是一样的数字,就以为代码崩了,其实只是还没意识到 Go 把安全默认值放在了第一位。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











