随机数也分场景
很多程序都需要随机数:生成验证码、抽样、打乱列表、测试数据、游戏掉落、会话 token、密码重置链接。它们看起来都叫“随机”,但安全要求完全不同。Go 里常见两个包:math/rand 和 crypto/rand。前者适合模拟、测试、非安全业务随机;后者适合安全 token、密钥、密码重置码这类不能被预测的场景。
混用这两个包是常见错误。math/rand 生成的是伪随机数,如果种子可预测,结果就可预测。它不适合生成登录 token。crypto/rand 使用加密安全随机源,速度较慢,但安全性更适合敏感用途。
这篇文章用几个例子讲清楚怎么选。
math/rand 适合普通随机
生成随机整数:
r := rand.New(rand.NewSource(time.Now().UnixNano()))
fmt.Println(r.Intn(100))
Intn(100) 返回 [0, 100) 范围内的整数。注意不包含 100。
随机选择一个元素:
func PickName(names []string, r *rand.Rand) (string, error) {
if len(names) == 0 {
return "", fmt.Errorf("names is empty")
}
return names[r.Intn(len(names))], nil
}
把 *rand.Rand 作为参数传入,测试更容易。测试可以使用固定种子:
r := rand.New(rand.NewSource(1))
name, err := PickName([]string{"a", "b", "c"}, r)
固定种子让测试结果稳定。不要在测试里依赖当前时间生成随机数,否则失败很难复现。
打乱列表
func ShuffleStrings(items []string, r *rand.Rand) {
r.Shuffle(len(items), func(i, j int) {
items[i], items[j] = items[j], items[i]
})
}
使用:
r := rand.New(rand.NewSource(time.Now().UnixNano()))
items := []string{"Go", "PHP", "Python"}
ShuffleStrings(items, r)
fmt.Println(items)
Shuffle 会原地修改切片。如果要保留原顺序,先复制。
copyItems := append([]string(nil), items...)
ShuffleStrings(copyItems, r)
普通抽样、展示顺序打散、测试数据生成,用 math/rand 就可以。
crypto/rand 生成安全 Token
生成随机字节:
func RandomBytes(n int) ([]byte, error) {
b := make([]byte, n)
if _, err := rand.Read(b); err != nil {
return nil, fmt.Errorf("read random bytes: %w", err)
}
return b, nil
}
这里的 rand 是 crypto/rand:
import rand "crypto/rand"
生成 token:
func NewToken() (string, error) {
b, err := RandomBytes(32)
if err != nil {
return "", err
}
return base64.RawURLEncoding.EncodeToString(b), nil
}
RawURLEncoding 生成适合放进 URL 的字符串,不带 = 填充。32 字节随机数已经足够大,适合会话 token、密码重置 token 等场景。
使用:
token, err := NewToken()
if err != nil {
return err
}
fmt.Println(token)
不要用 math/rand 生成安全 token:
// 不适合安全用途
fmt.Sprintf("%d", rand.Int())
这种 token 可能被预测。
验证码不一定等于安全 token
短信验证码常见 6 位数字:
func NewCode(r *rand.Rand) string {
return fmt.Sprintf("%06d", r.Intn(1000000))
}
这类验证码位数短,本身可被穷举,所以必须配合过期时间、次数限制、风控和绑定手机号使用。如果验证码用于安全敏感场景,也可以用 crypto/rand 生成数字。
安全不是只看随机数来源,还要看整个流程:是否限流,是否过期,是否只能使用一次,错误消息是否泄露信息。
随机字符串不要用取模偷懒
有时你想从字符表里生成随机字符串:
const alphabet = "abcdefghijklmnopqrstuvwxyz0123456789"
如果只是测试数据,用 math/rand 可以:
func RandomName(r *rand.Rand, n int) string {
var b strings.Builder
for i := 0; i < n; i++ {
b.WriteByte(alphabet[r.Intn(len(alphabet))])
}
return b.String()
}
如果是安全 token,不要简单把随机字节对字符表长度取模。取模可能带来分布偏差。对普通业务影响不大,但安全场景更建议直接生成随机字节,再用 base64 或 hex 编码:
func SecureHex(n int) (string, error) {
b := make([]byte, n)
if _, err := rand.Read(b); err != nil {
return "", err
}
return hex.EncodeToString(b), nil
}
调用:
token, err := SecureHex(32)
if err != nil {
return err
}
hex 输出会比原始字节长一倍,32 字节会变成 64 个十六进制字符。base64.RawURLEncoding 更短,hex 更容易人工复制。两者都可以,关键是随机来源要使用 crypto/rand。
小结
Go 里 math/rand 和 crypto/rand 解决的是不同问题。普通模拟、测试、打乱列表、非安全抽样,用 math/rand;会话 token、密码重置链接、密钥材料等安全敏感值,用 crypto/rand。
随机代码要让场景清楚。测试中使用固定种子保证可复现,安全 token 使用足够字节数并编码成 URL 安全字符串。不要因为两个包都叫 rand 就混在一起。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。