Go语言 随机数

2018-08-18  本文已影响0人  小杰的快乐时光

Go语言标准库中关于随机函数提供了两种包,分别是“math/rand”和“crypto/rand”。

math/rand:实现伪随机数生成器。
crypto/rand:实现用于加解密的更安全的随机数生成器。

关于伪随机数
我们使用以下代码随机输出5个随机数

import (
   "fmt"
   "math/rand"
)
func main() {
   for i := 0; i < 5; i++ {
      fmt.Println(rand.Int())
   }
}
-----output-----
5577006791947779410
8674665223082153551
6129484611666145821
4037200794235010051
3916589616287113937

从结果上来看没什么问题,但是如果多运行几次,就会发现每次输出的结果是相同的,这也就是说,虽然单次输出的结果是随机数,但是多次输出的结果却是相同的,这样很容易被找出规律,因此是伪随机数。
那么出现上述情况的原因是什么呢?让我们来看看math/rand的源码
rand.go源码中初始化过程

func NewSource(seed int64) Source {
   var rng rngSource
   rng.Seed(seed) //使用随机种子seed初始化
   return &rng
}

rng.Seed(seed)在 rng.go 中源码如下:

// Seed uses the provided seed value to initialize the generator to a deterministic state.
func (rng *rngSource) Seed(seed int64) { //int64远大于int32,所以传入的seed很容易造成rngSource在初始化时,出现重复的.
   rng.tap = 0
   rng.feed = _LEN - _TAP

   seed = seed % _M  // 随机种子等于 对_M = (1 << 31) - 1求模,这里会产生大量同余整数
   if seed < 0 {
      seed += _M
   }
   if seed == 0 {
      seed = 89482311
   }

   x := int32(seed)
   for i := -20; i < _LEN; i++ {
      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
      }
   }
}

每次Int/intn/Uint32/Int31,其实都是调用Int63。该方法从池中获取内部两个索引指向的缓存数值相加(同时会更新其中一条,下次使用)。

// Int63 returns a non-negative pseudo-random 63-bit integer as an int64.
func (rng *rngSource) Int63() int64 {
   return int64(rng.Uint64() & _MASK)
}

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

因此相同的随机种子,在多次产生随机数下都会是相同的,因此称为伪随机数
因此,我们使用设置一个随机种子

import (
   "fmt"
   "math/rand"
)
func main() {
   rand.New(rand.NewSource(10))
   for i := 0; i < 5; i++ {
      fmt.Println(rand.Int())
   }
}
----output----
5577006791947779410
8674665223082153551
6129484611666145821
4037200794235010051
3916589616287113937

然后经过多次输出后会发现,每次结果还是相同的,难道代码写错了?我们来看看 rand.Int() 的源码:rand.go 中 rand.Int() 返回 globalRand.Int()。

func Int() int { return globalRand.Int() }

我们再看看 globalRand 的源码,原来rand.Int()下的随机数都是以1为种子。

var globalRand = New(&lockedSource{src: NewSource(1).(Source64)})

因此我们来修改一下代码

import (
   "fmt"
   "math/rand"
)
func main() {
   rand1 := rand.New(rand.NewSource(10))
   for i := 0; i < 5; i++ {
      fmt.Println(rand1.Int())
   }
}
-----output-----
5221277731205826435
3852159813000522384
8532807521486154107
3888302351045490779
4512466281294657143

这下终于跟之前的不一样了,但是多次运行后发现,第一次结果变了,但之后的结果还是一样的,那该怎么解决呢?

这时候我们可以考虑使用时间作为随机种子,这样每次结果就绝对不一样了

import (
   "fmt"
   "math/rand"
   "time"
)
func main() {
   rand1 := rand.New(rand.NewSource(time.Now().UnixNano()))
   for i := 0; i < 5; i++ {
      fmt.Println(rand1.Int())
   }
}

上面我们是使用 math/rand 包做随机数,如果我们改为 crypto/rand 包会不会有其他的变化呢?
我们来看看 crypto 包下的rand源码

//包rand实现了加密安全
//伪随机数生成器。
package rand

import "io"

// Reader是一个加密的全局共享实例
//强伪随机生成器。
//在Linux上,Reader使用getrandom(2)(如果可用),否则使用/ dev / urandom。
//在OpenBSD上,Reader使用getentropy(2)。
//在其他类Unix系统上,Reader从/ dev / urandom读取。
//在Windows系统上,Reader使用CryptGenRandom API。
var Reader io.Reader

crypto/rand 包中的随机数是利用当前系统的一些特征,比如内存的使用,文件的使用量,不同类型的进程数量等等来进行计算随机数,因此可能重复的几率很低。

下面是使用 crypto/rand 包 产生随机数的代码实例

import (
   "fmt"
   "crypto/rand"
)
func main() {
   b := make([]byte, 10)
   n, err := rand.Read(b) //在byte切片中随机写入元素
   fmt.Println(n, err, b)
}
---output---
10 <nil> [114 240 66 169 137 70 27 41 148 202]

注意:crypto/rand 包 产生随机数的速度要比 math/rand 产生随机数的速度慢很多

参考文章:

https://blog.csdn.net/libra412/article/details/41925051

https://blog.csdn.net/qq_26981997/article/details/56837947

上一篇 下一篇

猜你喜欢

热点阅读