Go 1.22 就要在龙年春节期间发布了。Go 1.22的新特性包括了新的 math/rand
包。这个包的目标是提供一个更好的伪随机数生成器,它的 API 也更加简单易用。本文将介绍这个新的包的特性。
Go 1.22 release notes[1] 正在编写之中,大家可以关注这个网页以便全面了解Go 1.22的变化,前几天有Gopher制作了一个交互式运行新特性代码的网页[2],也非常好,在reddit上关注度很高。今天这篇文章只关注于于math/rand/v2
这个新的包。
为什么要新的math/rand包
其实大家对math/rand
不是那么满意。
2017年,#20661[3] 中提到math/rand.Read
和crypto/rand.Read
相近,导致本来应该使crypto/rand.Read
的地方使用了math/rand.Read
,导致了安全问题。
2017年,#21835[4] 中 Rob Pike 提议在Go 2中使用PCG Source。
2018年,#26263[5] 中 Josh Bleecher Snyder 提议对math/rand
进行彻底的重构。
2023年6月, Russ Cox基于先前的对math/rand
的吐槽,以及和Rob Pike的讨论,建立了一个讨论(#60751[6]),准备新建一个包math/rand/v2
,重新设计和实现一个新的伪随机数的库讨论也很热烈,最后实现了一个提案#61716[7],这个提案最直接的动机是清理 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](https://tip.golang.org/doc/go1.20#mathrandpkgmathrand "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[8] 的算法。初步基准测试显示,与 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
对象是非线程安全的。
包级别的函数
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
针对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
也是包级别的函数使用的伪随机数生成器。
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
是另外一种伪随机数生成器。
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分布的伪随机数生成器。
type Zipf func NewZipf(r *Rand, s float64, v float64, imax uint64) *Zipf func (z *Zipf) Uint64() uint64
相信后续还会有一些第三方的伪随机数生成器出现。
它们都实现了接口Source
,Source
接口只有一个方法Uint64()
:
type Source interface { Uint64() uint64 }
所有的伪随机数生成器都可以包装成一个Rand
对象,Rand
对象是非线程安全的,所以要注意。
func New(src Source) *Rand
★这和Rust中的实现模式类似。<>第一版把它叫做伴型特性,第二版中不知道为什么把这一节去掉了。
Rust中的
”Rng
类似这里的Go的Source
,可以有多种实现生成器。Rust中的Rand
也类似这里Go的Rand
,基于Uint64() uint64
提供各种类型的随机数。
Rand
提供了各种便利的方法,这些方法其实和包级别的函数是一样的,只是它们是Rand
对象的方法而已:
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]交互式运行新特性代码的网页: 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