Illustration created for “A Journey With Go”, made from the original Go Gopher, created by Renee French.
這篇文章基于 Go 1.13 版本
Go 實現了兩個包來產生隨機數:
- 在包 math/rand 的一個偽隨機數生成器( PRNG )
- 在包 crypto/rand 中實現的加密偽隨機數生成器( CPRNG )
如果這兩個包都產生了隨機數,你需要在真正的隨機數和性能之間尋找平衡。
確定的結果
Go 的 rand 包會使用相同的源來產生一個確定的偽隨機數序列。這個源會產生一個不變的數列,稍后在執行期間使用。將你的程序運行多次將會讀到一個完全相同的序列并產生相同的結果。讓我們用一個簡單的例子來嘗試一下:
func main() {
for i := 0; i < 4; i++ {
println(rand.Intn(100))
}
}
多次運行這個程序將會產生相同的結果:
81
87
47
59
由于源代碼已經發布到 Go 的官方標準庫中,因此任何運行此程序的計算機都會得到相同的結果。但是,由于 Go 僅保留一個生成的數字序列,我們可能想知道 Go 是如何管理用戶請求的時間間隔的。Go 實際上使用此數字序列來播種一個產生這個隨機數的源,然后獲取其請求間隔的模。例如,運行相同的程序,最大值為 10,則模 10 的結果相同。
1
7
7
9
讓我們來看一下如何在每次運行我們的程序時得到不同的序列。
播種
Go 提供一個方法, Seed(see int64) ,該方法能讓你初始化這個默認序列。默認情況下,它會使用變量 1。使用另一個變量將會提供一個新的序列,但會保持確定性:
func main() {
rand.Seed(2)
for i := 0; i < 4; i++ {
println(rand.Intn(100))
}
}
這些是新的結果:
86
86
92
40
在你每次運行這個程序時,這個序列將會保持不變。這是構建此序列的工作流:

The sequence is pre-generated at the bootstrap
獲取一個全新序列的解決方案是使用一個在運行時能改變的變量,比如當前時間:
func main() {
rand.Seed(time.Now().UnixNano())
for i := 0; i < 3; i++ {
println(rand.Intn(100))
}
}
由于當前納秒數在任何時刻都是不同的,因此這個程序每次運行都會使用一個不同的序列。然而,盡管這個序列在每次運行都是不同的,可這些數字仍是偽隨機數。如果你準備犧牲性能來獲得更好的隨機性,那么 Go 已經為你提供了另一種實現方式。
隨機數生成器
Go 的標準庫也提供了一個適用于加密應用的隨機數生成器。因此,理所當然的,生成的隨機數并不固定,并且一定會提供更好的隨機性。這有一個例子使用了這個新包 cryto/rand :
func main() {
for i := 0; i < 4; i++ {
n, _ := rand.Int(rand.Reader, big.NewInt(100))
println(n.Int64())
}
}
這是結果:
12
24
56
19
多次運行這個程序將會得到不同的結果。在內部,Go 應用了如下規則:
在 linux 和 FreeBSD 系統上,Reader 會使用 getrandom(2) (如果可用的話),否則使用 /dev/urandom。
在 OpenBSD 上,Reader 會使用 getentropy(2)。
在其他的類 Unix 系統上,Reader 會讀取 /dev/urandom。
在 windows 系統上,Reader 會使用 CryptGenRandom API.
在 Wasm 上,Reader 會使用 Web Cryto API。
但是,獲得更好的質量意味著性能降低,因為它必須執行更多的操作并且不能使用預生成的序列。
性能
為了理解生成隨機數的兩種不同方式之間的折衷,我基于先前的兩個例子運行了一個基準測試。結果如下:
name time/op
RandWithCrypto-8 272ns ± 3%
name time/op
RandWithMath-8 22.8ns ± 4%
不出所料,crypto 包更慢一些。但是,如果你不用去處理安全的隨機數,那么 math 包就足夠了并且它將會給你提供最好的性能。
你也可以調整默認數字生成器,由于內部互斥鎖的存在,它是并發安全的。如果生成器并不在并發環境下使用,那么你就可以在不使用鎖的情況下創建你自己的生成器:
func main() {
gRand := rand.New(rand.NewSource(1).(rand.Source64))
for i := 0; i < 4; i++ {
println(gRand.Intn(100))
}
}
性能會更好:
name time/op
RandWithMathNoLock-8 10.7ns ± 4%
via:https://medium.com/a-journey-with-go/go-how-are-random-numbers-generated-e58ee8696999
作者:Vincent Blanchon[1]譯者:sh1luo[2]校對:lxbwolf[3]
本文由 GCTT[4] 原創編譯,Go 中文網[5] 榮譽推出
參考資料
[1]
Vincent Blanchon: https://medium.com/@blanchon.vincent
[2]
sh1luo: https://github.com/sh1luo
[3]
lxbwolf: https://github.com/lxbwolf
[4]
GCTT: https://github.com/studygolang/GCTT
[5]
Go 中文網: https://studygolang.com/