随机性——这件事太严肃,不能交给运气来决定
\n\n每个写过Go代码的人,都曾遇到过生成随机数的问题。随机数生成器(RNG) 是个狡猾的东西。表面上看很简单:调用 rand.Intn() 就能得到一个数字。但如果你不重视 正确的种子设置(seed),你的“随机性”就会变成可预测的序列,从而破坏测试,在密码学中掉链子,或者每次运行时都给出相同的结果,让你大吃一惊。
在Stack Overflow上,“How to properly seed random number generator”这个问题获得了数千次浏览。人们在使用 time.Now().UnixNano() 时感到困惑,在不必要的地方使用 crypto/rand,并且饱受性能低下的困扰。今天,我们将以Go为例来探讨这个话题——没有废话,只有真实的基准测试和现成的解决方案。
为什么种子不仅仅是一种形式?
\n\n种子(Seed) 是生成器的初始状态。相同的种子会产生相同的数字序列。这对于调试很方便,但如果你期望不可预测性,在生产环境中就是灾难性的。
\n\n在Go中,标准包 math/rand 使用确定性算法。如果你忘记调用 rand.Seed(),那么……在Go 1.20之前,生成器默认使用固定种子(通常是1)。从Go 1.20开始,math/rand 会在程序启动时自动使用随机值进行种子设置。然而,在关键位置依赖这一点是不好的实践。
新手的主要问题是试图为每个随机数都设置种子:
\n\n// ❌ 不好:每次都重新创建生成器\nfunc getRandom() int {\n r := rand.New(rand.NewSource(time.Now().UnixNano()))\n return r.Intn(100)\n}\n\n为什么这不好?time.Now().UnixNano() 提供纳秒,但实际上精度取决于操作系统。例如,在Windows上,精度可能约为15毫秒。如果你连续两次调用该函数,你会得到 相同 的数字。此外,每次调用 rand.NewSource 都会创建一个新对象,增加垃圾回收器的负担。
Go中种子设置的三大方法
\n\n1. 经典方法:全局种子只设置一次
\n\n对于大多数应用程序来说,最简单且正确的做法是在程序启动时只设置一次种子。
\n\npackage main\n\nimport (\n "fmt"\n "math/rand"\n "time"\n)\n\nfunc main() {\n // ✅ 好:只设置一次种子\n rand.Seed(time.Now().UnixNano())\n\n for i := 0; i < 5; i++ {\n fmt.Println(rand.Intn(100))\n }\n}\n\n重要提示: time.Now().UnixNano() 提供了一个唯一值,但如果程序在同一纳秒内多次启动(可能性很小,但理论上存在),种子将会相同。对于99%的应用程序来说,这已经足够了。
2. 用于隔离的本地生成器
\n\n当你拥有多个goroutine,或者你不想影响全局状态时,可以创建自己的 rand.Rand 实例。
func NewSeededGenerator() *rand.Rand {\n seed := time.Now().UnixNano()\n return rand.New(rand.NewSource(seed))\n}\n\n// 使用方式:\nrng := NewSeededGenerator()\nfor i := 0; i < 3; i++ {\n fmt.Println(rng.Intn(100))\n}\n\n这种方法提供了完全的控制:不同的goroutine可以拥有自己的生成器,并且它们不会互相阻塞(全局的 rand 使用互斥锁)。
3. 用于关键任务的加密安全种子
\n\n如果你需要真正的随机性(令牌、密码、密码学)——忘掉 math/rand。请使用 crypto/rand:
import (\n "crypto/rand"\n "encoding/binary"\n "math/rand"\n)\n\nfunc CryptoSeed() int64 {\n var buf [8]byte\n _, err := rand.Read(buf[:])\n if err != nil {\n