Go标准库的新 math/rand

Go 1.22 就要在龙年春节期间发布了。Go 1.22的新特性包括了新的 math/rand 包。这个包的目标是提供一个更好的伪随机数生成器,它的 API 也更加简单易用。本文将介绍这个新的包的特性。

Go 1.22 release notes 正在编写之中,大家可以关注这个网页以便全面了解Go 1.22的变化,前几天有Gopher制作了一个交互式运行新特性代码的网页,也非常好,在reddit上关注度很高。今天这篇文章只关注于于math/rand/v2这个新的包。

为什么要新的math/rand包

其实大家对math/rand不是那么满意。
2017年,#20661 中提到math/rand.Readcrypto/rand.Read相近,导致本来应该使用crypto/rand.Read的地方使用了math/rand.Read,导致了安全问题。
2017年,#21835 中 Rob Pike 提议在Go 2中使用PCG Source。
2018年,#26263 中 Josh Bleecher Snyder 提议对math/rand进行彻底的重构。
2023年6月, Russ Cox基于先前的对math/rand的吐槽,以及和Rob Pike的讨论,建立了一个讨论(#60751),准备新建一个包math/rand/v2,重新设计和实现一个新的伪随机数的库讨论也很热烈,最后实现了一个提案#61716,这个提案最直接的动机是清理 math/rand 并解决其中许多悬而未决的问题,特别是使用过时生成器、缓慢的算法,以及与 crypto/rand.Read 的不幸冲突。

由于go module的支持版本v2、v3、..., Go 1.22中将会有一个新的包math/rand/v2,这个包将会是一个新的包,而不是math/rand的升级版本。这个包的目标是提供一个更好的伪随机数生成器,它的 API 也更加简单易用,同时一些检查工具也能支持这个包,不会报错。

看样子,math/rand/v2将会是第一个在标准库中建立v2版本的包,如果大家能够接受,将来会有更多的包加入进来,比如sync/v2encoding/json/v2等等。

提案的主要内容

math/rand/v2 API 以 math/rand 为起点,进行以下不兼容的更改

1、 移除 Rand.Read 和顶层的 Read。假装伪随机生成器是任意长字节序列的良好来源几乎总是错误的。math/rand 适用于模拟和非确定性算法,几乎从不需要字节序列。Readmath/randcrypto/rand 之间唯一共享的 API 部分,代码应该基本上总是使用 crypto/rand.Read。(math/rand.Readcrypto/rand.Read 存在问题,因为它们具有相同的签名; math/rand.Intcrypto/rand.Int 也都存在,但具有不同的签名,这意味着代码永远不会意外地将一个错认为是另一个。)

2、 移除 Source.SeedRand.Seed 和顶层的 Seed。顶层的 Seed 已在 Go 1.20 中废弃。Source.SeedRand.Seed 假定底层源可以由单个 int64 作为种子,这只对有限数量的源是真实的。具体的源实现可以提供具有适当签名的 Seed 方法,或者对于不能重新设置种子的生成器根本不提供;简单来说使用一个int64 作为种子没有普适性,不适合定义一个通用的接口。

注意,移除顶层 Seed 意味着顶层函数如 Int 将始终以随机方式而不是确定性方式生成。math/rand/v2 将不关注 math/rand 所关注的 randautoseed GODEBUG 设置;顶层函数的自动设置哦随机种子是唯一的模式。这反过来意味着顶层函数使用的具体 PRNG 算法是未指定的,可以在发布之间更改而不破坏任何现有代码。

3、 将 Source 接口更改为具有单个 Uint64() uint64 方法,取代 Int63() int64。后者过于拟合原始的 Mitchell & Reeds LFSR 生成器。现代生成器可以提供 uint64

4、 移除 Source64,现在不再需要,因为 Source 提供了 Uint64 方法。

5、 在 Float32Float64 中使用更直观的实现。以 Float64 为例,它最初使用 float64(r.Int63()) / (1<<63),但这存在问题,偶尔会四舍五入为 1.0。我们尝试将其更改为 float64(r.Int63n(1<<53) / (1<<53),避免了四舍五入的问题。

6、 修复 ExpFloat64NormFloat64 中的偏差问题。

7、 使用 Rand.Shuffle 实现 Rand.Perm

8、 将 IntnInt31Int31nInt63Int64n 重命名为 IntNInt32Int32NInt64Int64N。原来的名称中的 3163 是令人困惑的,而大写 N 在 Go 中作为名称的第二个“单词”更为习惯。

9、 添加 Uint32Uint32NUint64Uint64NUintUintN,既作为顶层函数,也作为 Rand 的方法。

10、在 NIntNUintN 等中使用 Lemire 的算法。初步基准测试显示,与 v1 Int31n 相比,节省了 40%,与 v1 Int63n 相比,节省了 75%。

11、添加一个通用的顶层函数 N,类似于 Int64NUint64N,但适用于任何整数类型。特别是这允许使用 rand.N(1*time.Minute) 来获取范围在 [0, 1*time.Minute) 内的随机持续时间。

12、添加一个新的 Source 实现,PCG-DXSM。PCG 是一个简单、高效的算法,具有良好的统计随机性质。DXSM 变体是作者专门为纠正原始 (PCG-XSLRR) 中的一种罕见、隐晦的缺陷而引入的,并且现在是 Numpy 中的默认生成器。

13、移除 Mitchell & Reeds LFSR 生成器和 NewSource。

14、添加一个新的 Source 实现,ChaCha8。ChaCha8 是从 ChaCha8 流密码派生的具有强密码学随机性质的随机数生成器。它提供与 ChaCha8 加密等效的安全性。

15、在 math/rand/v2math/rand(未设置种子时)中使用每个 OS 线程的 ChaCha8 作为全局随机生成器。

math/rand/v2介绍

注意,根据go module的定义,v2只是版本号,新的包名还是叫做rand

rand 包实现了适用于模拟(simulation)等任务的伪随机数生成器,但不应用于对安全性敏感的工作。

随机数由 Source生成,通常包装在 Rand 中。这两种类型应该一次由单个 goroutine 使用:在多个 goroutine 之间共享需要某种形式的同步

顶层函数,如 Float64Int,对于多个 goroutine 的并发使用是安全的。

该包的输出可能在设置种子的方式不同的情况下很容易可预测。对于适用于对安全性敏感的工作的随机数,请参阅 crypto/rand 包。

简单综述:所以你考虑到安全避免被人预测的场景下,还是要使用crypto/rand 包。 包级别的函数比如Int是线程安全的,但是如果你自己生成一个Rand对象,那么就要注意了,因为Rand对象是非线程安全的。

包级别的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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
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

针对int32int64uint32uint64,分别有Xxxxx()XxxxxN()两种函数,前者返回一个随机数,后者返回一个范围在[0,n)的随机数。
Float32Float64返回范围在[0.0, 1.0)的随机浮点数。
IntN返回一个范围在[0,n)的随机数,数据类型是int类型。
N是一个泛型的函数,返回一个范围在[0,n)的随机数,底层数据是int类型的,特别适合time.Duration这样的类型。

Perm返回一个长度为n的随机排列的int数组。
Shuffle洗牌算法

NormFloat64返回一个标准正态分布的随机数。
ExpFloat64返回一个指数分布的随机数。

三种伪随机数生成器

ChaCha8 也是包级别的函数使用的伪随机数生成器。

1
2
3
4
5
6
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 是另外一种伪随机数生成器。

1
2
3
4
5
6
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分布的伪随机数生成器。

1
2
3
type Zipf
func NewZipf(r *Rand, s float64, v float64, imax uint64) *Zipf
func (z *Zipf) Uint64() uint64

相信后续还会有一些第三方的伪随机数生成器出现。

它们都实现了接口Source,Source接口只有一个方法Uint64():

1
2
3
type Source interface {
Uint64() uint64
}

所有的伪随机数生成器都可以包装成一个Rand对象,Rand对象是非线程安全的,所以要注意。

1
func New(src Source) *Rand

这和Rust中的实现模式类似。<>第一版把它叫做伴型特性,第二版中不知道为什么把这一节去掉了。
Rust中的Rng类似这里的Go的Source,可以有多种实现生成器。Rust中的Rand也类似这里Go的Rand,基于Uint64() uint64提供各种类型的随机数。

Rand提供了各种便利的方法,这些方法其实和包级别的函数是一样的,只是它们是Rand对象的方法而已:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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