因为math/rand默认使用固定种子1,不显式初始化就调用rand.Intn会得到完全相同序列;Go 1.20+应弃用rand.Seed(),改用rand.New(rand.NewSource(time.Now().UnixNano()))创建独立实例,并发时需避免共享全局实例以防panic或重复值。

为什么 rand.Intn(100) 每次运行结果都一样
因为 math/rand 默认使用固定种子 1,不显式初始化就调用包级函数(如 rand.Intn),必然得到完全相同的序列。这不是 bug,是设计——方便测试复现,但用在生成 ID、验证码等场景就是硬伤。
Go 1.20+ 已弃用 rand.Seed(),正确做法是显式创建新实例:
- 全局声明一个:
var r = rand.New(rand.NewSource(time.Now().UnixNano())) - 然后用
r.Intn(100),不是rand.Intn(100) - 别在
init()或循环里反复调rand.New—— 实例可复用,new 开销小但没必要
并发环境下 math/rand panic 或返回重复值怎么办
math/rand 的包级函数(rand.Intn 等)背后共享一个全局 *rand.Rand,它不是并发安全的。多 goroutine 同时调用会触发 fatal error: concurrent map writes,或更隐蔽地——状态被覆盖,导致不同 goroutine 拿到一模一样的随机数。
- 最简单解法:每个 goroutine 自己
rand.New(rand.NewSource(time.Now().UnixNano())),但注意纳秒时间戳在高并发下极易重复 - 更稳妥:用
time.Now().UnixNano() ^ int64(unsafe.Pointer(&x))混入局部地址防碰撞(x是函数内定义的任意变量) - 绝对别用
sync.Mutex包裹全局调用——锁争用会让随机数生成变成串行瓶颈 - 如果 goroutine 数量稳定(比如 worker pool),提前建好
[]*rand.Rand池,按索引取用,省 GC 压力
生成 token 或 session ID 该用 crypto/rand 还是 math/rand
必须用 crypto/rand。哪怕只是生成一个 16 字节的 session ID,用 math/rand 就等于把登录凭证明文贴在请求头里——攻击者只要知道你用了 time.Now().UnixNano() 做 seed,就能暴力穷举出全部可能值。
立即学习“go语言免费学习笔记(深入)”;
-
crypto/rand从系统熵池读取(Linux/dev/urandom,WindowsBCryptGenRandom),不可预测、无状态、无需 seed - 正确写法:
buf := make([]byte, 16); _, err := rand.Read(buf),必须检查err(Windows 或低熵容器中可能返回io.EOF) - 别手搓:
rand.Intn(256)循环拼 byte、fmt.Sprintf("%x", rand.Uint64())都不安全,前者非密码学安全,后者长度不固定、字符集受限 - 字符串编码用
encoding/base64.RawURLEncoding.EncodeToString(buf),别用 hex —— 膨胀率高且含大小写字母数字外的符号
crypto/rand 为什么不能用来 shuffle 列表或掷骰子
它慢,而且没必要。虽然现代机器上单次 crypto/rand.Read 只耗 100–300ns,但比起 math/rand 的纳秒级开销,它本质是系统调用,有上下文切换和熵池访问成本。更重要的是:它不提供 Intn 或 Perm 这类便利方法,你得自己读字节、转整数、做拒绝采样防偏斜——代码啰嗦,还容易出错。
- 洗牌用户列表、生成游戏骰子点数、模拟抽样统计——全用
math/rand,只要 seed 合理、实例隔离,完全够用 - 别被 “crypto” 名字误导:它的唯一使命是生成密钥、token、salt、IV,不是“更高级的随机数”
- 高频路径(比如每秒万级 HTTP 请求)若误用
crypto/rand,真正卡住你的往往不是熵读取,而是后续 base64 编码或 JSON 序列化——但错误源头在选错了包
真正容易被忽略的点是:crypto/rand 的 Read 返回实际读取字节数,必须校验是否等于预期长度;而 math/rand 的 seed 碰撞问题,在容器或 CI 环境中因时钟精度下降会更严重,不是本地跑通就万事大吉。


















