Go 1.22 就要在龙年春节期间发布了。Go 1.22的新特性包括了新的 math/rand
包。这个包的目标是提供一个更好的伪随机数生成器,它的 API 也更加简单易用。本文将介绍这个新的包的特性。
Go 1.22 release notes 正在编写之中,大家可以关注这个网页以便全面了解Go 1.22的变化,前几天有Gopher制作了一个交互式运行新特性代码的网页,也非常好,在reddit上关注度很高。今天这篇文章只关注于于math/rand/v2
这个新的包。
其实大家对math/rand
不是那么满意。
2017年,#20661 中提到math/rand.Read
和crypto/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/v2
、encoding/json/v2
等等。
math/rand/v2
API 以 math/rand
为起点,进行以下不兼容的更改:
1、 移除 Rand.Read
和顶层的 Read
。假装伪随机生成器是任意长字节序列的良好来源几乎总是错误的。math/rand
适用于模拟和非确定性算法,几乎从不需要字节序列。Read
是 math/rand
和 crypto/rand
之间唯一共享的 API 部分,代码应该基本上总是使用 crypto/rand.Read
。(math/rand.Read
和 crypto/rand.Read
存在问题,因为它们具有相同的签名; math/rand.Int
和 crypto/rand.Int
也都存在,但具有不同的签名,这意味着代码永远不会意外地将一个错认为是另一个。)
2、 移除 Source.Seed
、Rand.Seed
和顶层的 Seed
。顶层的 Seed
已在 Go 1.20 中废弃。Source.Seed
和 Rand.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、 在 Float32
和 Float64
中使用更直观的实现。以 Float64
为例,它最初使用 float64(r.Int63()) / (1<<63)
,但这存在问题,偶尔会四舍五入为 1.0
。我们尝试将其更改为 float64(r.Int63n(1<<53) / (1<<53)
,避免了四舍五入的问题。
6、 修复 ExpFloat64
和 NormFloat64
中的偏差问题。
7、 使用 Rand.Shuffle
实现 Rand.Perm
。
8、 将 Intn
、Int31
、Int31n
、Int63
、Int64n
重命名为 IntN
、Int32
、Int32N
、Int64
、Int64N
。原来的名称中的 31
和 63
是令人困惑的,而大写 N
在 Go 中作为名称的第二个“单词”更为习惯。
9、 添加 Uint32
、Uint32N
、Uint64
、Uint64N
、Uint
、UintN
,既作为顶层函数,也作为 Rand
的方法。
10、在 N
、IntN
、UintN
等中使用 Lemire 的算法。初步基准测试显示,与 v1 Int31n
相比,节省了 40%,与 v1 Int63n
相比,节省了 75%。
11、添加一个通用的顶层函数 N
,类似于 Int64N
或 Uint64N
,但适用于任何整数类型。特别是这允许使用 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/v2
和 math/rand
(未设置种子时)中使用每个 OS 线程的 ChaCha8 作为全局随机生成器。
注意,根据go module的定义,v2
只是版本号,新的包名还是叫做rand
。
rand
包实现了适用于模拟(simulation
)等任务的伪随机数生成器,但不应用于对安全性敏感的工作。
随机数由 Source
生成,通常包装在 Rand
中。这两种类型应该一次由单个 goroutine 使用:在多个 goroutine 之间共享需要某种形式的同步。
顶层函数,如 Float64
和 Int
,对于多个 goroutine 的并发使用是安全的。
该包的输出可能在设置种子的方式不同的情况下很容易可预测。对于适用于对安全性敏感的工作的随机数,请参阅 crypto/rand
包。
简单综述:所以你考虑到安全避免被人预测的场景下,还是要使用crypto/rand
包。 包级别的函数比如Int
是线程安全的,但是如果你自己生成一个Rand
对象,那么就要注意了,因为Rand
对象是非线程安全的。
|
|
针对int32
、int64
、uint32
、uint64
,分别有Xxxxx()
和XxxxxN()
两种函数,前者返回一个随机数,后者返回一个范围在[0,n)
的随机数。Float32
和Float64
返回范围在[0.0, 1.0)
的随机浮点数。IntN
返回一个范围在[0,n)
的随机数,数据类型是int
类型。N
是一个泛型的函数,返回一个范围在[0,n)
的随机数,底层数据是int类型的,特别适合time.Duration
这样的类型。
Perm
返回一个长度为n
的随机排列的int
数组。Shuffle
洗牌算法
NormFloat64
返回一个标准正态分布的随机数。ExpFloat64
返回一个指数分布的随机数。
ChaCha8
也是包级别的函数使用的伪随机数生成器。
|
|
PCG
是另外一种伪随机数生成器。
|
|
Zipf
是生成Zipf分布的伪随机数生成器。
|
|
相信后续还会有一些第三方的伪随机数生成器出现。
它们都实现了接口Source
,Source
接口只有一个方法Uint64()
:
|
|
所有的伪随机数生成器都可以包装成一个Rand
对象,Rand
对象是非线程安全的,所以要注意。
|
|
这和Rust中的实现模式类似。<>第一版把它叫做伴型特性,第二版中不知道为什么把这一节去掉了。
Rust中的Rng
类似这里的Go的Source
,可以有多种实现生成器。Rust中的Rand
也类似这里Go的Rand
,基于Uint64() uint64
提供各种类型的随机数。
Rand
提供了各种便利的方法,这些方法其实和包级别的函数是一样的,只是它们是Rand
对象的方法而已:
|
|