math/rand 包
math/rand 包实现了伪随机数生成器
主要方法
(1)func Seed(seed int64)
设置随机种子,不设置则默认Seed(1)
(2)func Int() int
返回一个非负的伪随机int值
(3)func Int31() int32
返回一个int32类型的非负的31位伪随机数
(4)func Int63() int64
返回一个int64类型的非负的63位伪随机数
(5)func Intn(n int) int
返回一个取值范围在[0,n)的伪随机int值,如果n<=0会panic
(6)func Int31n(n int32) int32
返回一个取值范围在[0,n)的伪随机int32值,如果n<=0会panic
(7)func Int63n(n int64) int64
返回一个取值范围在[0, n)的伪随机int64值,如果n<=0会panic
(8)func Float32() float32
返回一个取值范围在[0.0, 1.0)的伪随机float32值
(9)func Float64() float64
返回一个取值范围在[0.0, 1.0)的伪随机float64值
(10)func Perm(n int) []int
返回一个有n个元素的,[0,n)范围内整数的伪随机排列的切片
代码示例
随机数由源生成。顶级函数(例如Float64和Int)使用默认的共享源,该源在每次运行程序时都会产生确定的值序列。 如果每次运行需要不同的行为, 请使用种子函数初始化默认的源。 默认的Source可安全地供多个goroutine并发使用,但不是由NewSource创建的Source。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
|
package main
import (
"fmt"
"math/rand"
"time"
)
func init(){
rand.Seed(time.Now().UTC().UnixNano())
}
func main() {
// launches 2 generators and the fanIn collector function
c := fanIn(genrt(), genrt())
for i := 0; i < 10000; i++ {
fmt.Println(<-c)
}
}
func fanIn(a <-chan int, b <-chan int) <-chan string {
c := make(chan string)
// launch collector from a to channel
go func() {
var count int
for {
count += <-a
c <- fmt.Sprintf("Tally of A is: %d", count)
}
}()
// launch collector from b to channel
go func() {
var count int
for {
count += <-b
c <- fmt.Sprintf("Tally of B is: %d", count)
}
}()
return c
}
func genrt() <-chan int {
c := make(chan int)
// launch generator of Dice rolls
go func() {
for i := 0; ; i++ {
c <- rand.Intn(6) + 1
time.Sleep(time.Duration(500 * time.Millisecond))
}
}()
return c
}
|
打印输出
1
2
3
4
5
6
7
|
...
Tally of B is: 17656
Tally of A is: 17438
Tally of A is: 17440
Tally of B is: 17659
Tally of B is: 17660
Tally of A is: 17445
|
应用场景
(1)验证码
(2)随机密码
(3)抽奖
(4)随机算法
源码剖析
math/rand 源码其实很简单, 就两个比较重要的函数:
1
2
3
4
5
6
7
8
9
10
11
|
// Seed uses the provided seed value to initialize the generator to a deterministic state.
// Seed should not be called concurrently with any other Rand method.
func (r *Rand) Seed(seed int64) {
if lk, ok := r.src.(*lockedSource); ok {
lk.seedPos(seed, &r.readPos)
return
}
r.src.Seed(seed)
r.readPos = 0
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
// Seed uses the provided seed value to initialize the generator to a deterministic state.
func (rng *rngSource) Seed(seed int64) {
rng.tap = 0
rng.feed = rngLen - rngTap
seed = seed % int32max
if seed < 0 {
seed += int32max
}
if seed == 0 {
seed = 89482311
}
x := int32(seed)
for i := -20; i < rngLen; 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 ^= rngCooked[i]
rng.vec[i] = u
}
}
}
|
这个函数就是在设置 seed, 其实就是对 rng.vec 各个位置设置对应的值. rng.vec 的大小是 607.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
// 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 += rngLen
}
rng.feed--
if rng.feed < 0 {
rng.feed += rngLen
}
x := rng.vec[rng.feed] + rng.vec[rng.tap]
rng.vec[rng.feed] = x
return uint64(x)
}
|
我们在使用不管调用 Intn()
, Int31n()
等其他函数, 最终调用到就是这个函数. 可以看到每次调用就是利用 rng.feed
rng.tap
从 rng.vec
中取到两个值相加的结果返回了. 同时还是这个结果又重新放入 rng.vec
.
在这里需要注意使用 rng.go 的 rngSource 时, 由于 rng.vec 在获取随机数时会同时设置 rng.vec 的值, 当多 goroutine 同时调用时就会有数据竞争问题. math/rand 采用在调用 rngSource 时加锁 sync.Mutex 解决.
1
2
3
4
5
6
|
func (r *lockedSource) Uint64() (n uint64) {
r.lk.Lock()
n = r.src.Uint64()
r.lk.Unlock()
return
}
|
另外我们能直接使用rand.Seed()
,rand.Intn(100)
, 是因为 math/rand 初始化了一个全局的 globalRand 变量.
1
2
3
4
5
|
var globalRand = New(&lockedSource{src: NewSource(1).(*rngSource)})
func Seed(seed int64) { globalRand.Seed(seed) }
func Uint32() uint32 { return globalRand.Uint32() }
|
需要注意到由于调用 rngSource 加了锁, 所以直接使用rand.Int32()
会导致全局的 goroutine 锁竞争, 所以在高并发场景时, 当你的程序的性能是卡在这里的话, 你需要考虑利用New(&lockedSource{src: NewSource(1).(*rngSource)})
为不同的模块生成单独的 rand. 不过根据目前的实践来看, 使用全局的 globalRand 锁竞争并没有我们想象中那么激烈. 使用 New 生成新的 rand 里面是有坑的, 开篇的 panic 就是这么产生的, 后面具体再说.
种子(seed)到底起什么作用?
1
2
3
4
5
6
7
|
func main() {
for i := 0; i < 10; i++ {
fmt.Printf("current:%d\n", time.Now().Unix())
rand.Seed(time.Now().Unix())
fmt.Println(rand.Intn(100))
}
}
|
结果:
1
2
3
4
5
6
7
|
current:1613814632
65
current:1613814632
65
current:1613814632
65
...
|
这个例子能得出一个结论: 相同种子, 每次运行的结果都是一样的,是一串相同顺序的随机数. 这是为什么呢?
在使用 math/rand 的时候, 一定需要通过调用 rand.Seed 来设置种子, 其实就是给 rng.vec 的 607 个槽设置对应的值. 通过上面的源码那可以看出来, rand.Seed 会调用一个 seedrand 的函数, 来计算对应槽的值.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
func seedrand(x int32) int32 {
const (
A = 48271
Q = 44488
R = 3399
)
hi := x / Q
lo := x % Q
x = A*lo - R*hi
if x < 0 {
x += int32max
}
return x
}
|
这个函数的计算结果并不是随机的, 而是根据 seed 实际算出来的. 另外这个函数并不是随便写的, 是有相关的数学证明的.
这也导致了相同的 seed, 最终设置到 rng.vec里面的值是相同的, 通过 Intn 取出的也是相同的值.
我遇到的那些坑
rand panic
文章开头的截图就是项目开发中使用别人封装的底层库, 在某天出现的 panic. 大概实现的代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
// random.go
var (
rrRand = rand.New(rand.NewSource(time.Now().Unix()))
)
type Random struct{}
func (r *Random) Balance(sf*service.Service) ([]string, error) {
// .. 通过服务发现获取到一堆ip+port, 然后随机拿到其中的一些ip和port出来
randIndexes := rrRand.Perm(randMax)
// 返回这些ip 和port
}
|
这个 Random 会被并发调用, 由于 rrRand 不是并发安全的, 所以就导致了调用 rrRand.Perm 时偶尔会出现 panic 情况.
在使用 math/rand 的时候, 有些人使用 math.Intn()
, 看了下注释发现是全局共享了一个锁, 担心出现锁竞争, 所以用 rand.New 来初始化一个新的 rand, 但是要注意到 rand.New 初始化出来的 rand 并不是并发安全的.
修复方案: 就是把 rrRand 换成了 globalRand, 在线上高并发场景下, 发现全局锁影响并不大.
获取的都是同一个机器

同样也是底层封装的 rpc 库, 使用 random 的方式来流量分发, 在线上跑了一段时间后, 流量都路由到一台机器上了, 导致服务直接宕机. 大概实现代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
func Call(ctx *gin.Context, method string, service string, data map[string]interface{}) (buf []byte, err error) {
ins, err := ral.GetInstance(ctx, ral.TYPE_HTTP, service)
if err != nil {
// 错误处理
}
defer ins.Release()
if b, e := ins.Request(ctx, method, data, head); e == nil {
// 错误处理
}
// 其他逻辑, 重试等等
}
func GetInstance(ctx *gin.Context, modType string, name string) (*Instance, error) {
// 其他逻辑..
switch res.Strategy {
case WITH_RANDOM:
if res.rand == nil {
res.rand = rand.New(rand.NewSource(time.Now().Unix()))
}
which = res.rand.Intn(res.count)
case 其他负载均衡查了
}
// 返回其中一个ip和port
}
|
引起问题的原因: 可以看出来每次请求到来都是利用 GetInstance 来获取一个 ip 和 port, 如果采用 Random 方式的流量负载均衡, 每次都是重新初始化一个 rand. 我们已经知道当设置相同的种子, 每次运行的结果都是一样的. 当瞬间流量过大时, 并发请求 GetInstance, 由于那一刻 time.Now().Unix() 的值是一样的, 这样就会导致获取到随机数都是一样的, 所以就导致最后获取到的 ip, port 都是一样的, 流量都分发到这台机器上了.
修复方案: 修改成 globalRand 即可.
rand 未来期望
说到这里基本上可以看出来, 为了防止全局锁竞争问题, 在使用 math/rand 的时候, 首先都会想到自定义 rand, 但是就容易整出来莫名其妙的问题.
为什么 math/rand 需要加锁呢?
大家都知道 math/rand 是伪随机的, 但是在设置完 seed 后, rng.vec 数组的值基本上就确定下来了, 这明显就不是随机了, 为了增加随机性, 通过 Uint64() 获取到随机数后, 还会重新去设置 rng.vec. 由于存在并发获取随机数的需求, 也就有了并发设置 rng.vec 的值, 所以需要对 rng.vec 加锁保护.
crypto/rand 包
crypto/rand 包实现了用于加解密的更安全的随机数生成器
主要方法
Variables
Reader是一个全局、共享的密码用强随机数生成器。在Unix类型系统中,会从/dev/urandom读取;而Windows中会调用CryptGenRandom API。
举例说明该如何使用Reader:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
package main
import(
"fmt"
"encoding/base64"
"crypto/rand"
"io"
)
//sessionId函数用来生成一个session ID,即session的唯一标识符
func sessionId() string {
b := make([]byte, 32)
//ReadFull从rand.Reader精确地读取len(b)字节数据填充进b
//rand.Reader是一个全局、共享的密码用强随机数生成器
if _, err := io.ReadFull(rand.Reader, b); err != nil {
return ""
}
fmt.Println(b) //[62 186 123 16 209 19 130 218 146 136 171 211 12 233 45 99 80 200 59 20 56 254 170 110 59 147 223 177 48 136 220 142]
return base64.URLEncoding.EncodeToString(b)//将生成的随机数b编码后返回字符串,该值则作为session ID
}
func main() {
fmt.Println(sessionId()) //Prp7ENETgtqSiKvTDOktY1DIOxQ4_qpuO5PfsTCI3I4=
}
|
func Int
1
|
func Int(rand io.Reader, max *big.Int) (n*big.Int, err error)
|
返回一个在[0, max)区间服从均匀分布的随机值,如果max<=0则会panic。
举例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
package main
import(
"fmt"
"crypto/rand"
"math/big"
)
func main() {
//从128开始,这样就能够将(max.BitLen() % 8) == 0的情况包含在里面
for n := 128; n < 140; n++ {
b := new(big.Int).SetInt64(int64(n)) //将new(big.Int)设为int64(n)并返回new(big.Int)
fmt.Printf("max Int is : %v\n", b)
i, err := rand.Int(rand.Reader, b)
if err != nil {
fmt.Printf("Can't generate random value: %v, %v", i, err)
}
fmt.Printf("rand Int is : %v\n", i)
}
}
|
返回:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
bogon:~ user$ go run testGo.go
max Int is : 128
rand Int is : 25
max Int is : 129
rand Int is : 117
max Int is : 130
rand Int is : 85
max Int is : 131
rand Int is : 62
max Int is : 132
rand Int is : 27
max Int is : 133
rand Int is : 120
max Int is : 134
rand Int is : 10
max Int is : 135
rand Int is : 27
max Int is : 136
rand Int is : 11
max Int is : 137
rand Int is : 119
max Int is : 138
rand Int is : 35
max Int is : 139
rand Int is : 83
|
func Prime
1
|
func Prime(rand io.Reader, bits int) (p *big.Int, err error)
|
返回一个具有指定字位数的数字,该数字具有很高可能性是质数。如果从rand读取时出错,或者bits<2会返回错误。
举例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
package main
import(
"fmt"
"crypto/rand"
)
func main() {
for n := 2; n < 10; n++ {
p, err := rand.Prime(rand.Reader, n) //n代表位数,比如3为2位,127为7位
if err != nil {
fmt.Printf("Can't generate %d-bit prime: %v", n, err)
}
if p.BitLen() != n { //返回p的绝对值的字位数,0的字位数为0
fmt.Printf("%v is not %d-bit", p, n)
}
if !p.ProbablyPrime(32) { //对p进行32次Miller-Rabin质数检测。如果方法返回真则p是质数的几率为1-(1/4)**32;否则p不是质数
fmt.Printf("%v is not prime", p)
}
fmt.Println(p)
}
}
|
返回:
1
2
3
4
5
6
7
8
9
|
bogon:~ user$ go run testGo.go
3
7
13
31
53
109
223
439
|
如果位数小于2的话,会报错:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
package main
import(
"fmt"
"crypto/rand"
"log"
)
func main() {
p, err := rand.Prime(rand.Reader, 1) //n代表位数,比如3为2位,127为7位
if err != nil {
log.Fatal(err)
}
fmt.Println(p)
}
|
返回:
1
2
3
|
bogon:~ user$ go run testGo.go
2019/02/23 12:31:37 crypto/rand: prime size must be at least 2-bit
exit status 1
|
func Read
1
|
func Read(b []byte) (n int, err error)
|
本函数是一个使用io.ReadFull调用Reader.Read的辅助性函数。当且仅当err == nil时,返回值n == len(b)。
因为本函数是一个使用io.ReadFull调用Reader.Read的辅助性函数,所以最上面的那个生成session ID的例子等价于:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
package main
import(
"fmt"
"encoding/base64"
"crypto/rand"
)
//sessionId函数用来生成一个session ID,即session的唯一标识符
func sessionId() string {
b := make([]byte, 32)
//rand.Reader是一个全局、共享的密码用强随机数生成器
n, err := rand.Read(b);
if err != nil {
return ""
}
fmt.Println(b[:n]) //[154 94 244 2 147 96 148 6 13 27 3 52 231 127 160 159 40 47 84 116 79 87 160 217 185 216 47 143 101 107 219 178]
return base64.URLEncoding.EncodeToString(b)//将生成的随机数b编码后返回字符串,该值则作为session ID
}
func main() {
fmt.Println(sessionId()) //ml70ApNglAYNGwM053-gnygvVHRPV6DZudgvj2Vr27I=
}
|
代码示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
package main
import (
"crypto/rand"
"encoding/base64"
"fmt"
"math/big"
)
func main() {
//1、Int
n, err := rand.Int(rand.Reader, big.NewInt(128))
if err == nil {
fmt.Println("rand.Int:", n, n.BitLen())
}
//2、Prime
p, err := rand.Prime(rand.Reader, 5)
if err == nil {
fmt.Println("rand.Prime:", p)
}
//3、Read
b := make([]byte, 32)
m, err := rand.Read(b)
if err == nil {
fmt.Println("rand.Read:", b[:m])
fmt.Println("rand.Read:", base64.URLEncoding.EncodeToString(b))
}
}
|
输出:
1
2
3
4
|
rand.Int: 92 7
rand.Prime: 29
rand.Read: [207 47 241 208 190 84 109 134 86 106 87 223 111 113 203 155 44 118 71 20 186 62 66 130 244 98 97 184 8 179 6 230]
rand.Read: zy_x0L5UbYZWalffb3HLmyx2RxS6PkKC9GJhuAizBuY=
|
参考
golang 随机数 math/rand包 crypto/rand包
Golang: math/rand 和 crypto/rand 区别
go标准库的学习-crypto/rand
深度解密 Go math/rand