Go標(biāo)準(zhǔn)庫(kù)的新 math/rand,你看明白了嗎?
Go 1.22 就要在龍年春節(jié)期間發(fā)布了。Go 1.22的新特性包括了新的 math/rand 包。這個(gè)包的目標(biāo)是提供一個(gè)更好的偽隨機(jī)數(shù)生成器,它的 API 也更加簡(jiǎn)單易用。本文將介紹這個(gè)新的包的特性。
Go 1.22 release notes[1] 正在編寫之中,大家可以關(guān)注這個(gè)網(wǎng)頁(yè)以便全面了解Go 1.22的變化,前幾天有Gopher制作了一個(gè)交互式運(yùn)行新特性代碼的網(wǎng)頁(yè)[2],也非常好,在reddit上關(guān)注度很高。今天這篇文章只關(guān)注于于math/rand/v2這個(gè)新的包。
為什么要新的math/rand包
其實(shí)大家對(duì)math/rand不是那么滿意。
2017年,#20661[3] 中提到math/rand.Read和crypto/rand.Read相近,導(dǎo)致本來(lái)應(yīng)該使crypto/rand.Read的地方使用了math/rand.Read,導(dǎo)致了安全問題。
2017年,#21835[4] 中 Rob Pike 提議在Go 2中使用PCG Source。
2018年,#26263[5] 中 Josh Bleecher Snyder 提議對(duì)math/rand進(jìn)行徹底的重構(gòu)。
2023年6月, Russ Cox基于先前的對(duì)math/rand的吐槽,以及和Rob Pike的討論,建立了一個(gè)討論(#60751[6]),準(zhǔn)備新建一個(gè)包math/rand/v2,重新設(shè)計(jì)和實(shí)現(xiàn)一個(gè)新的偽隨機(jī)數(shù)的庫(kù)討論也很熱烈,最后實(shí)現(xiàn)了一個(gè)提案#61716[7],這個(gè)提案最直接的動(dòng)機(jī)是清理 math/rand 并解決其中許多懸而未決的問題,特別是使用過時(shí)生成器、緩慢的算法,以及與 crypto/rand.Read 的不幸沖突。
由于go module的支持版本v2、v3、..., Go 1.22中將會(huì)有一個(gè)新的包math/rand/v2,這個(gè)包將會(huì)是一個(gè)新的包,而不是math/rand的升級(jí)版本。這個(gè)包的目標(biāo)是提供一個(gè)更好的偽隨機(jī)數(shù)生成器,它的 API 也更加簡(jiǎn)單易用,同時(shí)一些檢查工具也能支持這個(gè)包,不會(huì)報(bào)錯(cuò)。
看樣子,math/rand/v2將會(huì)是第一個(gè)在標(biāo)準(zhǔn)庫(kù)中建立v2版本的包,如果大家能夠接受,將來(lái)會(huì)有更多的包加入進(jìn)來(lái),比如sync/v2、encoding/json/v2等等。
提案的主要內(nèi)容
math/rand/v2 API 以 math/rand 為起點(diǎn),進(jìn)行以下不兼容的更改:
1、 移除 Rand.Read 和頂層的 Read。假裝偽隨機(jī)生成器是任意長(zhǎng)字節(jié)序列的良好來(lái)源幾乎總是錯(cuò)誤的。math/rand 適用于模擬和非確定性算法,幾乎從不需要字節(jié)序列。Read 是 math/rand 和 crypto/rand 之間唯一共享的 API 部分,代碼應(yīng)該基本上總是使用 crypto/rand.Read。(math/rand.Read 和 crypto/rand.Read 存在問題,因?yàn)樗鼈兙哂邢嗤暮灻? math/rand.Int 和 crypto/rand.Int 也都存在,但具有不同的簽名,這意味著代碼永遠(yuǎn)不會(huì)意外地將一個(gè)錯(cuò)認(rèn)為是另一個(gè)。)
2、 移除 Source.Seed、Rand.Seed 和頂層的 Seed。頂層的 Seed 已在 Go 1.20 中廢棄。Source.Seed 和 Rand.Seed 假定底層源可以由單個(gè) int64 作為種子,這只對(duì)有限數(shù)量的源是真實(shí)的。具體的源實(shí)現(xiàn)可以提供具有適當(dāng)簽名的 Seed 方法,或者對(duì)于不能重新設(shè)置種子的生成器根本不提供;簡(jiǎn)單來(lái)說(shuō)使用一個(gè)int64 作為種子沒有普適性,不適合定義一個(gè)通用的接口。
注意,移除頂層 Seed 意味著頂層函數(shù)如 Int 將始終以隨機(jī)方式而不是確定性方式生成。math/rand/v2 將不關(guān)注 math/rand 所關(guān)注的 [randautoseed](https://tip.golang.org/doc/go1.20#mathrandpkgmathrand "randautoseed") GODEBUG 設(shè)置;頂層函數(shù)的自動(dòng)設(shè)置哦隨機(jī)種子是唯一的模式。這反過來(lái)意味著頂層函數(shù)使用的具體 PRNG 算法是未指定的,可以在發(fā)布之間更改而不破壞任何現(xiàn)有代碼。
3、 將 Source 接口更改為具有單個(gè) Uint64() uint64 方法,取代 Int63() int64。后者過于擬合原始的 Mitchell & Reeds LFSR 生成器?,F(xiàn)代生成器可以提供 uint64。
4、 移除 Source64,現(xiàn)在不再需要,因?yàn)?nbsp;Source 提供了 Uint64 方法。
5、 在 Float32 和 Float64 中使用更直觀的實(shí)現(xiàn)。以 Float64 為例,它最初使用 float64(r.Int63()) / (1<<63),但這存在問題,偶爾會(huì)四舍五入為 1.0。我們嘗試將其更改為 float64(r.Int63n(1<<53) / (1<<53),避免了四舍五入的問題。
6、 修復(fù) ExpFloat64 和 NormFloat64 中的偏差問題。
7、 使用 Rand.Shuffle 實(shí)現(xiàn) Rand.Perm。
8、 將 Intn、Int31、Int31n、Int63、Int64n 重命名為 IntN、Int32、Int32N、Int64、Int64N。原來(lái)的名稱中的 31 和 63 是令人困惑的,而大寫 N 在 Go 中作為名稱的第二個(gè)“單詞”更為習(xí)慣。
9、 添加 Uint32、Uint32N、Uint64、Uint64N、Uint、UintN,既作為頂層函數(shù),也作為 Rand 的方法。
10、在 N、IntN、UintN 等中使用 Lemire[8] 的算法。初步基準(zhǔn)測(cè)試顯示,與 v1 Int31n 相比,節(jié)省了 40%,與 v1 Int63n 相比,節(jié)省了 75%。
11、添加一個(gè)通用的頂層函數(shù) N,類似于 Int64N 或 Uint64N,但適用于任何整數(shù)類型。特別是這允許使用 rand.N(1*time.Minute) 來(lái)獲取范圍在 [0, 1*time.Minute) 內(nèi)的隨機(jī)持續(xù)時(shí)間。
12、添加一個(gè)新的 Source 實(shí)現(xiàn),PCG-DXSM。PCG 是一個(gè)簡(jiǎn)單、高效的算法,具有良好的統(tǒng)計(jì)隨機(jī)性質(zhì)。DXSM 變體是作者專門為糾正原始 (PCG-XSLRR) 中的一種罕見、隱晦的缺陷而引入的,并且現(xiàn)在是 Numpy 中的默認(rèn)生成器。
13、移除 Mitchell & Reeds LFSR 生成器和 NewSource。
14、添加一個(gè)新的 Source 實(shí)現(xiàn),ChaCha8。ChaCha8 是從 ChaCha8 流密碼派生的具有強(qiáng)密碼學(xué)隨機(jī)性質(zhì)的隨機(jī)數(shù)生成器。它提供與 ChaCha8 加密等效的安全性。
15、在 math/rand/v2 和 math/rand(未設(shè)置種子時(shí))中使用每個(gè) OS 線程的 ChaCha8 作為全局隨機(jī)生成器。
math/rand/v2介紹
注意,根據(jù)go module的定義,v2只是版本號(hào),新的包名還是叫做rand。
rand 包實(shí)現(xiàn)了適用于模擬(simulation)等任務(wù)的偽隨機(jī)數(shù)生成器,但不應(yīng)用于對(duì)安全性敏感的工作。
隨機(jī)數(shù)由 Source生成,通常包裝在 Rand 中。這兩種類型應(yīng)該一次由單個(gè) goroutine 使用:在多個(gè) goroutine 之間共享需要某種形式的同步。
頂層函數(shù),如 Float64 和 Int,對(duì)于多個(gè) goroutine 的并發(fā)使用是安全的。
該包的輸出可能在設(shè)置種子的方式不同的情況下很容易可預(yù)測(cè)。對(duì)于適用于對(duì)安全性敏感的工作的隨機(jī)數(shù),請(qǐng)參閱 crypto/rand 包。
簡(jiǎn)單綜述:所以你考慮到安全避免被人預(yù)測(cè)的場(chǎng)景下,還是要使用crypto/rand 包。 包級(jí)別的函數(shù)比如Int是線程安全的,但是如果你自己生成一個(gè)Rand對(duì)象,那么就要注意了,因?yàn)镽and對(duì)象是非線程安全的。
包級(jí)別的函數(shù)
func ExpFloat64() float64
func Float32() float32
func Float64() float64
func Int() int
func Int32() int32
func Int32N(n int32) int32
func Int64() int64
func Int64N(n int64) int64
func IntN(n int) int
func N[Int intType](n Int "Int intType") Int
func NormFloat64() float64
func Perm(n int) []int
func Shuffle(n int, swap func(i, j int))
func Uint32() uint32
func Uint32N(n uint32) uint32
func Uint64() uint64
func Uint64N(n uint64) uint64
func UintN(n uint) uint
針對(duì)int32、int64、uint32、uint64,分別有Xxxxx()和XxxxxN()兩種函數(shù),前者返回一個(gè)隨機(jī)數(shù),后者返回一個(gè)范圍在[0,n)的隨機(jī)數(shù)。
Float32和Float64返回范圍在[0.0, 1.0)的隨機(jī)浮點(diǎn)數(shù)。
IntN返回一個(gè)范圍在[0,n)的隨機(jī)數(shù),數(shù)據(jù)類型是int類型。
N是一個(gè)泛型的函數(shù),返回一個(gè)范圍在[0,n)的隨機(jī)數(shù),底層數(shù)據(jù)是int類型的,特別適合time.Duration這樣的類型。
Perm返回一個(gè)長(zhǎng)度為n的隨機(jī)排列的int數(shù)組。
Shuffle洗牌算法
NormFloat64返回一個(gè)標(biāo)準(zhǔn)正態(tài)分布的隨機(jī)數(shù)。
ExpFloat64返回一個(gè)指數(shù)分布的隨機(jī)數(shù)。
三種偽隨機(jī)數(shù)生成器
ChaCha8 也是包級(jí)別的函數(shù)使用的偽隨機(jī)數(shù)生成器。
type ChaCha8
func NewChaCha8(seed [32]byte) *ChaCha8
func (c *ChaCha8) MarshalBinary() ([]byte, error)
func (c *ChaCha8) Seed(seed [32]byte)
func (c *ChaCha8) Uint64() uint64
func (c *ChaCha8) UnmarshalBinary(data []byte) error
PCG 是另外一種偽隨機(jī)數(shù)生成器。
type PCG
func NewPCG(seed1, seed2 uint64) *PCG
func (p *PCG) MarshalBinary() ([]byte, error)
func (p *PCG) Seed(seed1, seed2 uint64)
func (p *PCG) Uint64() uint64
func (p *PCG) UnmarshalBinary(data []byte) error
Zipf是生成Zipf分布的偽隨機(jī)數(shù)生成器。
type Zipf
func NewZipf(r *Rand, s float64, v float64, imax uint64) *Zipf
func (z *Zipf) Uint64() uint64
相信后續(xù)還會(huì)有一些第三方的偽隨機(jī)數(shù)生成器出現(xiàn)。
它們都實(shí)現(xiàn)了接口Source,Source接口只有一個(gè)方法Uint64():
type Source interface {
Uint64() uint64
}
所有的偽隨機(jī)數(shù)生成器都可以包裝成一個(gè)Rand對(duì)象,Rand對(duì)象是非線程安全的,所以要注意。
func New(src Source) *Rand
這和Rust中的實(shí)現(xiàn)模式類似。<>第一版把它叫做伴型特性,第二版中不知道為什么把這一節(jié)去掉了。
Rust中的Rng類似這里的Go的Source,可以有多種實(shí)現(xiàn)生成器。Rust中的Rand也類似這里Go的Rand,基于Uint64() uint64提供各種類型的隨機(jī)數(shù)。
Rand提供了各種便利的方法,這些方法其實(shí)和包級(jí)別的函數(shù)是一樣的,只是它們是Rand對(duì)象的方法而已:
func (r *Rand) Float32() float32
func (r *Rand) Float64() float64
func (r *Rand) Int() int
func (r *Rand) Int32() int32
func (r *Rand) Int32N(n int32) int32
func (r *Rand) Int64() int64
func (r *Rand) Int64N(n int64) int64
func (r *Rand) IntN(n int) int
func (r *Rand) NormFloat64() float64
func (r *Rand) Perm(n int) []int
func (r *Rand) Shuffle(n int, swap func(i, j int))
func (r *Rand) Uint32() uint32
func (r *Rand) Uint32N(n uint32) uint32
func (r *Rand) Uint64() uint64
func (r *Rand) Uint64N(n uint64) uint64
func (r *Rand) UintN(n uint) uint
參考資料
[1]Go 1.22 release notes: https://tip.golang.org/doc/go1.22
[2]交互式運(yùn)行新特性代碼的網(wǎng)頁(yè): https://antonz.org/go-1-22/
[3]#20661: https://github.com/golang/go/issues/20661
[4]#21835: https://github.com/golang/go/issues/21835
[5]#26263: https://github.com/golang/go/issues/26263
[6]#60751: https://github.com/golang/go/discussions/60751
[7]#61716: https://github.com/golang/go/issues/61716
[8]Lemire: https://lemire.me/blog/2016/06/27/a-fast-alternative-to-the-modulo-reduction