引言
大家刚开始使用随机数的时候可能会这样写, 但是他会产生一个问题,这是什么问题呢
func main() {
for i := 0; i < 10; i++ {
rand.Seed(time.Now().Unix())
fmt.Println(rand.Intn(100))
}
}
发现打印出来的结果都是相同的
让我们看看用代码分析为什么产生这个问题
首先golang定义一个长度为607个原素的数组,类型是int64
rand/rng.go文件
var (
// rngCooked used for seeding. See gen_cooked.go for details.
rngCooked [607]int64 = [...]int64{1395769623340756751...}
)
通过seed方法给以上的 rngCooked 数组初始化每个元素
// Seed uses the provided seed value to initialize the generator to a deterministic state.
func (rng *rngSource) Seed(seed int64) {
rng.tap = 0
rng.feed = rngLen - rngTap // rngLen和rngTap是常量
seed = seed % int32max
if seed < 0 {
seed += int32max
}
if seed == 0 {
seed = 89482311
}
x := int32(seed)
for i := -20; i < rngLen; i++ { // 为什么要冗余20次,我理解是让数据更加随机,你也可以设置100次,当然数据更随机
x = seedrand(x)
if i >= 0 {
var u int64
u = int64(x) << 40
x = seedrand(x)
u ^= int64(x) << 20
x = seedrand(x)
u ^= int64(x)
u ^= rngCooked[i]
rng.vec[i] = u
}
}
}
调用rand.Int31n(100)方法获得100以内的随机数
func (r *Rand) Int31n(n int32) int32 {
if n <= 0 {
panic("invalid argument to Int31n")
}
if n&(n-1) == 0 { // n is power of two, can mask
return r.Int31() & (n - 1)
}
max := int32((1 << 31) - 1 - (1<<31)%uint32(n))
v := r.Int31()
for v > max {
v = r.Int31()
}
return v % n
}
以上就是获得随机数的基本流程,但是似乎并没有解释 循环读取的随机数总是相同的,让我们再来看一段代码
这个代码运用了典型的 线性同余法(LCG)来获取随机数,线性同余法作为一种伪随机数生成算法,在许多应用场景中基本满足了需求。
// seed rng x[n+1] = 48271 * x[n] mod (2**31 - 1)
func seedrand(x int32) int32 {
const (
A = 48271
Q = 44488
R = 3399
)
hi := x / Q
lo := x % Q
x = A*lo - R*hi
if x < 0 {
x += int32max
}
return x
}
结论:所以我们可以看出,当X相同的情况下, 生成的随机数序列也是相同的,同一时刻拿到的随机数也就是相同的
安全性
我们在获取随机数的时候是用的排他锁,也就是并发安全的
func (r *lockedSource) Int63() (n int64) {
r.lk.Lock()
n = r.source().Int63()
r.lk.Unlock()
return
}
非并发安全情况
var (
rrRand = rand.New(rand.NewSource(time.Now().Unix()))
rrRand.Intn(100)
此时并发访问rrRand.Intn(100) 就会出现panic
)
func (r *Rand) Seed(seed int64) {
if lk, ok := r.src.(*lockedSource); ok { // 因为rand.NewSource(time.Now().Unix())不是lockedSource,所以不会走这个逻辑
lk.seedPos(seed, &r.readPos)
return
}
r.src.Seed(seed) // 会走这个逻辑
r.readPos = 0
}
在什么情况下需要指定不同的seed
比如模拟投掷骰子的实验,每次实验投掷10次骰子,并记录每次投掷的结果。需要运行3次实验,每次实验使用不同的种子值。总之,在业务中,或者在大多数情况下我们不需要指定种子值(go 1.20以后)
应用
- 在go 1.20以前,需要在程序启动时手动加上 rand.Seed(time.Now().UnixNano()) 种子值,但是在go 1.20以后就直接使用rand.IntN(100) 获取随机数就可以了 ,如下代码
func (r *lockedSource) source() *rngSource {
if r.s == nil {
var seed int64
if randautoseed.Value() == "0" {
seed = 1 // 环境变量为0,seed默认1,每次获得的随机数序列是一样的
} else {
seed = int64(fastrand64())
}
r.s = newSource(seed)
}
return r.s
}
- 如果需要特定的随机序列 请单独使用 rrRand = rand.New(rand.NewSource(time.Now().UnixNano())),这样就不会影响其他使用方使用全局随机数,但是请记住这个不是并发安全的,在并发的情况下需要加排他锁
- math.rand包产生的随机数是伪随机数,也就是会存在周期性重复,
对于公式 RandSeed = (A * RandSeed + B) % M 要达到周期重复需满足以下条件
- B,M互质;
- M的所有质因数都能整除A-1;
- 若M是4的倍数,A-1也是;
- A,B,N[0]都比M小;
- A,B是正整数。
- 如果需要真正意义的随机数,请使用encrypt/rand包 ,由于crypto/rand包使用的是基于密码学的安全随机数生成器(CSPRNG),因此生成的随机数序列是不可预测的、不可重复的。这意味着,即使使用相同的种子值,每次生成的随机数序列也应该是不同的。