如何在 Go 中生成随机字符串而不抓狂?
\n\n您是否曾遇到过需要快速创建唯一标识符、临时密码或 API 令牌的情况?如果您使用 Go 编程,那么迟早会面临一个问题:“如何生成固定长度的随机字符串?”这个问题在 Stack Overflow 上获得了数千次浏览,这并非没有原因——这个看似简单的任务实际上隐藏着几个陷阱。
\n\n在本文中,我们将深入探讨在 Go 中生成随机字符串,从简单方案到生产级方法。您将了解为什么 math/rand 可能很危险,如何使用 crypto/rand 确保安全,以及如何编写无需多余依赖的精简代码。
为什么密码生成不应该使用 math/rand?
\n\n让我们从最流行但并非总是正确的解决方案开始。许多新手会写出类似这样的代码:
\n\npackage main\n\nimport (\n "math/rand"\n "time"\n)\n\nconst letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"\n\nfunc RandString(n int) string {\n b := make([]byte, n)\n for i := range b {\n b[i] = letters[rand.Intn(len(letters))]\n }\n return string(b)\n}\n\nfunc main() {\n rand.Seed(time.Now().UnixNano())\n println(RandString(10))\n}\n\n这段代码可以工作,但它有一个严重缺陷:math/rand 使用确定性算法。如果攻击者知道了种子(或生成时间),他们就能重现整个序列。对于测试数据或文件名来说这没问题,但对于秘密令牌和密码来说——绝对不行。
使用 crypto/rand:在 Go 中安全生成随机字符串
\n\n对于生产环境,正确的选择是 crypto/rand 包。它使用操作系统的加密安全随机数生成器。以下是使用 crypto/rand 在 Go 中生成随机字符串的示例:
package main\n\nimport (\n "crypto/rand"\n "math/big"\n)\n\nconst charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()"\n\nfunc SecureRandomString(length int) (string, error) {\n result := make([]byte, length)\n for i := range result {\n num, err := rand.Int(rand.Reader, big.NewInt(int64(len(charset))))\n if err != nil {\n return "", err\n }\n result[i] = charset[num.Int64()]\n }\n return string(result), nil\n}\n\nfunc main() {\n str, err := SecureRandomString(16)\n if err != nil {\n panic(err)\n }\n println(str)\n}\n\n请注意:我们使用 rand.Int 配合 rand.Reader——这保证了每个字符都是随机且不可预测地选出的。对于Go 中的随机字符串来说,这是黄金标准。
性能优化:如何加速生成过程
\n\n为每个字符调用 rand.Int 的循环在生成数十万字符串时可能会很慢。解决方案是一次性生成所有随机字节,然后将它们映射到我们的字母表上。这个技巧在 Stack Overflow 上关于 Golang random string 的讨论中经常出现:
package main\n\nimport (\n "crypto/rand"\n "encoding/binary"\n)\n\nconst charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"\n\nfunc FastRandomString(length int) string {\n result := make([]byte, length)\n // 生成随机 64 位数字\n buf := make([]byte, 8*length)\n if _, err := rand.Read(buf); err != nil {\n panic(err)\n }\n for i := 0; i < length; i++ {\n // 为每个字符取 8 个字节\n val := binary.LittleEndian.Uint64(buf[i*8 : (i+1)*8])\n // 使用取模避免偏差 (bias)\n result[i] = charset[val%uint64(len(charset))]\n }\n return string(result)\n}\n\nfu