我们用随机数,是期望每次得到的结果不同,因此我们传递不同的seed,来获取。但事实上,即使种子不同,我们也可能会得到重复、且有规律的取值。运行以下代码看看:
func main() {
now := time.Now()
after := now.Add(time.Duration((1 << 31) - 1))
fmt.Printf("seed:%v,result=%v\n",now.Format("2006:01:02 15:04:05"),randPrint(now.UnixNano()))
fmt.Printf("seed:%v,result=%v\n",after.Format("2006:01:02 15:04:05"),randPrint(after.UnixNano()))
}
func randPrint(seed int64) []int {
var (
number = 10
result = make([]int, number)
)
rand.Seed(seed)
for i := 0; i < number; i++ {
result[i]=rand.Intn(100)
}
return result
}
不论你在什么时候,运行的两行结果肯定是相通的。我运行时,得到的输出:
seed:2017:02:24 11:48:32,result=[61 82 94 81 7 37 36 21 13 93]
seed:2017:02:24 11:48:34,result=[61 82 94 81 7 37 36 21 13 93]
此外,如果seed=math.MinInt32、-2058001336、0、89482311,他们的随机结果也会相同。
我擦!咋个了
针对这个问题,立马看了代码,很容易发现,种子的问题。代码中,核心的初始化如下:
//rng.go,rand初始化的相关代码
func (rng *rngSource) Seed(seed int64) {
rng.tap = 0
rng.feed = _LEN - _TAP
seed = seed % _M //<--对_M = (1 << 31) - 1求模
if seed < 0 { //转为正整数
seed += _M
}
if seed == 0 {
seed = 89482311 //0无法进行后续运算,seedrand(x)里对x进行了整除
}
x := int32(seed)
for i := -20; i < _LEN; i++ { //对缓冲池rng.vec初始化,后续将从rng.vec取值
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 ^= rng_cooked[i]
rng.vec[i] = u
}
}
}
由于内部基于math.MaxInt32(1 << 31) - 1)求模,因此会有大量的同余整数。当种子为math.MinInt32、-2058001336、89482311的时候,余数都是89482311;0,则是被定向到了89482311,属于例外。
问题的原因这下也清楚了,由于int64远大于int32,所以传入的seed很容易造成rngSource在初始化时,出现重复的.
在看代码,会发现规律都相同。每次Int/intn/Uint32/Int31,其实都是调用Int63。该方法从池中获取内部两个索引指向的缓存数值相加(同时会更新其中一条,下次使用)。
//rng.go,rand初始化的相关代码
// Uint64 returns a non-negative pseudo-random 64-bit integer as an uint64.
func (rng *rngSource) Uint64() uint64 {
rng.tap--
if rng.tap < 0 {
rng.tap += _LEN
}
rng.feed--
if rng.feed < 0 {
rng.feed += _LEN
}
x := rng.vec[rng.feed] + rng.vec[rng.tap]
rng.vec[rng.feed] = x
return uint64(x)
}
到此,我们可以非常明确:
相同种子,每次结果必然相同,这就是伪随机数。
此外,尽管算法方面有改进,但即使种子不同,但很可能出现同样规律的结果。比如:
go语言中,为密码提供了另外的随机数获取途径,那就是"crypto/rand"
包。代码中注释非常明确说明,数据源来自于哪里。这些数据来自于每台机器非常清晰:
// Package rand implements a cryptographically secure
// pseudorandom number generator.
package rand
import "io"
// Reader is a global, shared instance of a cryptographically
// strong pseudo-random generator.
//
// On Linux, Reader uses getrandom(2) if available, /dev/urandom otherwise.
// On OpenBSD, Reader uses getentropy(2).
// On other Unix-like systems, Reader reads from /dev/urandom.
// On Windows systems, Reader uses the CryptGenRandom API.
var Reader io.Reader
以Linux为例,优先调用getrandom(2),其实就是/dev/random
优先。与/dev/urandom两个文件,他们产生随机数的原理其实是差不多的,本质相同:都是利用当前系统的熵池来计算出固定一定数量的随机比特,然后将这些比特作为字节流返回。
熵池就是当前系统的环境噪音,熵指的是一个系统的混乱程度,系统噪音可以通过很多参数来评估,如内存的使用,文件的使用量,不同类型的进程数量等等。
由于环境噪声好无规律,更不会用种子来初始化,因此每次访问,其逻辑都是不可预测的。不过使用的时候要注意,尽管没有规律,但转换为数字或字符串后,依然可能会重复。
使用的时候非常简单,自己先初始化一个[]byte,然后调用API就好了,之后要注意自己转换。比如要随机生成一个int32:
var x uint32
binary.Read(crand.Reader,binary.BigEndian,&x)
fmt.Println(x)
代码非常简单。但要注意,通过这种方式,要比math.rand慢10来倍。