Go 随机数入门:math/rand、crypto/rand 和安全 Token

本文讲解 Go 中 math/rand 和 crypto/rand 的区别,说明如何生成测试随机数、业务随机值和安全 Token。

随机数也分场景

很多程序都需要随机数:生成验证码、抽样、打乱列表、测试数据、游戏掉落、会话 token、密码重置链接。它们看起来都叫“随机”,但安全要求完全不同。Go 里常见两个包:math/randcrypto/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
}

这里的 randcrypto/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/randcrypto/rand 解决的是不同问题。普通模拟、测试、打乱列表、非安全抽样,用 math/rand;会话 token、密码重置链接、密钥材料等安全敏感值,用 crypto/rand

随机代码要让场景清楚。测试中使用固定种子保证可复现,安全 token 使用足够字节数并编码成 URL 安全字符串。不要因为两个包都叫 rand 就混在一起。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页