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.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 作为全局随机生成器。
math/rand/v2介绍
注意,根据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对象的方法而已:
|
|
