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

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

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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值