golang实践-随机数的那点事儿

来源:互联网 发布:平面设计和淘宝美工 编辑:程序博客网 时间:2024/06/05 21:57

序:不同种子竟然可以得到相同的结果

我们用随机数,是期望每次得到的结果不同,因此我们传递不同的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,他们的随机结果也会相同。

我擦!咋个了

math.rand 解读

针对这个问题,立马看了代码,很容易发现,种子的问题。代码中,核心的初始化如下:

//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)}

到此,我们可以非常明确:

相同种子,每次结果必然相同,这就是伪随机数。

此外,尽管算法方面有改进,但即使种子不同,但很可能出现同样规律的结果。比如:

  • 前面提到的0,math.MinInt32
  • 还可以很快发现的 2,15764469
  • ……

真正的随机数

go语言中,为密码提供了另外的随机数获取途径,那就是"crypto/rand"包。代码中注释非常明确说明,数据源来自于哪里。这些数据来自于每台机器非常清晰:

// Package rand implements a cryptographically secure// pseudorandom number generator.package randimport "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来倍

0 0