math/rand不能用于安全场景,因其是基于可预测种子的确定性伪随机数生成器,只要知道seed和调用顺序就能完全复现序列;crypto/rand才依赖操作系统熵池(如/dev/urandom),提供不可预测、密码学安全的随机数。

为什么 math/rand 不能用于安全场景
直接用 math/rand 生成 token、密钥或验证码,等于把门钥匙刻在玻璃门上——看着随机,实则可预测。它基于确定性种子,只要知道初始 seed 和调用顺序,就能完全复现整个序列。crypto/rand 才是操作系统提供的真随机源(如 Linux 的 /dev/urandom),依赖硬件噪声或系统熵池,无法被还原或预测。
常见错误现象:用 math/rand.Intn(100) 做一次性密码(OTP)或 JWT 密钥,上线后被批量撞库或重放攻击。
- 永远不要用
math/rand生成任何需要保密性或抗预测性的值 - 即使加了
time.Now().UnixNano()作 seed,也不提升安全性 -
crypto/rand是标准库,无需额外安装,但需显式导入:"crypto/rand"
crypto/rand.Read() 是最常用也最容易出错的入口
它不返回整数,只往 []byte 里填字节,你需要自己转换。很多人卡在这步:想生成一个 0–99 的随机数,却试图对 rand.Read() 返回的单字节做模运算,结果分布严重偏斜(0–255 模 100,0–55 出现概率比 56–99 高一倍)。
正确做法是用 rand.Int() 或 rand.Intn() ——它们内部已处理均匀分布问题,且底层仍走 crypto/rand。
- 生成指定范围整数:
n, _ := rand.Int(rand.Reader, big.NewInt(100))→n.Int64()得到 0–99 - 更简洁:
n, _ := rand.Intn(100)(Go 1.22+ 支持,注意不是math/rand.Intn) - 如果必须用
Read()(比如生成 32 字节密钥),务必检查返回 err:if n, err := rand.Read(buf); err != nil { ... } -
rand.Reader是全局可用的 *rand.Reader,线程安全,可直接用
生成固定长度随机字符串(如 API Key)的可靠写法
别手写循环调用 rand.Intn() 拼字符——容易漏错误处理,且字符集若含重复或非 ASCII 字符,会导致分布不均。推荐组合 crypto/rand + 查表法:
var letters = []byte("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
func RandString(n int) (string, error) {
b := make([]byte, n)
if _, err := rand.Read(b); err != nil {
return "", err
}
for i := range b {
b[i] = letters[b[i]%byte(len(letters))]
}
return string(b), nil
}
注意点:
- 查表长度要是 2 的幂(如 64)时,
b[i] % len(letters)才无偏斜;否则要用拒绝采样(rejection sampling),但对 62 字符集影响极小,实际可忽略 - 不要用
fmt.Sprintf("%x", rand.Reader)直接转十六进制——它不控制长度,且可能因读取失败返回空串 - 如果要求必须含大小写字母+数字+符号,得单独构造字符集并做两次采样(先定类型,再定字符),否则符号出现概率会偏低
并发环境下 crypto/rand 的性能和注意事项
rand.Reader 本身是并发安全的,但频繁调用 rand.Read() 小缓冲区(如每次 1 字节)会触发大量系统调用,拖慢性能。Linux 下 /dev/urandom 已经做了用户态缓冲,但 Go 的封装没做批量预取。
- 批量读比单字节读快 10 倍以上:要 100 个随机字节,就
rand.Read(make([]byte, 100)),别循环 100 次 - 高吞吐服务(如每秒万级 token 生成)可考虑初始化时预读 1KB 到内存池,按需切片,但需自行管理生命周期和重填逻辑
- 容器环境(尤其是 init 容器或低熵 VM)启动时熵不足,
rand.Read()可能短暂阻塞——可通过cat /proc/sys/kernel/random/entropy_avail检查,生产环境建议配haveged或rng-tools
真正难的不是写对函数,而是意识到:安全随机数不是“够乱就行”,而是“攻击者在已知部分输出的前提下,无法缩小其余输出的可能空间”。哪怕一行代码写错,整条链路就失效。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











