必须使用crypto/rand.Read生成安全随机字符串,而非math/rand;字符集应为无重复ASCII字符串,通过字节模运算映射,且必须检查rand.Read返回的error。

用 rand.Read 生成安全随机字符串,别用 math/rand
Go 中最常踩的坑是误用 math/rand——它不安全、可预测,且默认 seed 相同导致每次运行结果一样。验证码、token 等场景必须用密码学安全的随机源,即 crypto/rand 的 rand.Read。
-
math/rand.New(rand.NewSource(time.Now().UnixNano()))仍不安全,仅适合模拟或测试 -
crypto/rand.Read返回error,必须检查,否则可能拿到全零字节 - 不要自己拼接字母表再索引:避免模偏差(如 256 % 62 ≠ 0),直接用
rand.Read生成字节后映射更可靠
字符集选 "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" 要去重且顺序固定
常见错误是用 rand.Perm(62) 或打乱 slice——这没意义,只要字符集确定、映射均匀即可。关键点在于:字符集字符串本身不能含重复字符,且长度应为 2 的幂次附近(非必需但利于理解偏差)。
- 推荐用字符串字面量定义:
const charset = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" - 别用
strings.Split或[]rune,直接按byte索引更快,且 ASCII 范围内无编码问题 - 若需排除易混淆字符(如 0/O/l/I),就提前从
charset中删掉,而不是生成后再过滤——后者会破坏长度确定性
生成固定长度字符串的完整写法(带错误处理)
以下是最简可靠实现,生成 6 位验证码:
func RandString(n int) (string, error) {
const charset = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
b := make([]byte, n)
_, err := rand.Read(b)
if err != nil {
return "", err
}
for i := range b {
b[i] = charset[b[i]%byte(len(charset))]
}
return string(b), nil
}
-
rand.Read(b)填充原始字节,b[i] % len(charset)映射到合法索引 - 模运算虽有轻微偏差(256 % 62 = 8,前 8 个字符概率略高),但对 6~12 位验证码影响可忽略;如需严格均匀,改用拒绝采样(但极少必要)
- 不要在循环里调
rand.Intn——它底层仍用math/rand,且性能差
并发安全与初始化成本
crypto/rand.Read 本身是线程安全的,无需额外 sync;但注意:它依赖操作系统熵池,首次调用可能稍慢(尤其容器环境),不过后续极快。
立即学习“go语言免费学习笔记(深入)”;
- 不要为每次调用新建 reader(如
rand.Reader是全局变量,直接用crypto/rand.Read即可) - 避免在 HTTP handler 里反复调用却不检查 error——如果系统熵枯竭(罕见),会返回
io.ErrUnexpectedEOF,需记录日志而非 panic - 如果项目已用
golang.org/x/crypto/chacha20poly1305等,说明 crypto/rand 已被间接引入,无额外依赖负担
真正要盯住的是字符集是否真不含歧义字符、错误是否被吞掉、以及长度是否严格满足业务要求——其他都是次要的。


















