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 产生随机数的速度慢很多
参考文章: