golang中的随机数rand与rand/v2

一、Go1.18及以前

使用的是固定的随机数种子,因此每一次启动程序看到都是通用的随机数序列。以下是go1.18.windows-amd64\src\math\rand\rand.go的注释

// Seed uses the provided seed value to initialize the default Source to a
// deterministic state. If Seed is not called, the generator behaves as
// if seeded by Seed(1). Seed values that have the same remainder when
// divided by 2³¹-1 generate the same pseudo-random sequence.
// Seed, unlike the Rand.Seed method, is safe for concurrent use.
func Seed(seed int64) { globalRand.Seed(seed) }
1、math/rand

随机数从资源生成。包水平的函数都使用的默认的公共资源。

该资源会在程序每次运行时都产生确定的序列。如果需要每次运行产生不同的序列,应使用Seed函数进行初始化。默认资源可以安全的用于多go程并发。

关于种子seed
程序启动的时候,种子的初始值是一样的,也就是说随机数是一样的,什么意思呢?

package main

import (
   "fmt"
   "math/rand"
)

func main(){
   data := rand.Int63n(100)
   fmt.Println(data)
}

每次运行go run main.go
打印的都是 10

如果我们播放种子

package main

import (
   "fmt"
   "math/rand"
   "time"
)

func main(){
   rand.Seed(time.Now().Unix()) // unix 时间戳,秒
   data := rand.Int63n(100)
   fmt.Println(data)
}

这样每次执行go run main.go
打印的结果就不一样,但是,根据随机数的特性,如果两次执行的时间戳是在同一秒,那么打印的结果是相同的。
以上的随机数相同的情况是发生在程序启动的时候,如果程序启动后,每次生成随机数会怎样呢?

package main

import (
   "fmt"
   "math/rand"
)

func main(){
   for i := 0; i<5; i++ {
      data := rand.Int63n(100)
      fmt.Println(data)
   }
}

运行 go run main.go
打印
10
51
21
51
37

再次运行 go run main.go
打印
10
51
21
51
37

可见每次启动的结果是一样的;但是程序启动后,每次的随机数都不尽相同,是随机的。

如果再加上种子呢?

package main

import (
   "fmt"
   "math/rand"
   "time"
)

func main(){
   for i := 0; i<5; i++ {
      rand.Seed(time.Now().Unix()) // unix 时间戳,秒
      data := rand.Int63n(100)
      fmt.Println(data)
   }
}

运行 go run main.go
打印
86
86
86
86
86

再次运行 go run main.go
打印
72
72
72
72
72

每次启动程序,因为种子不一样,所以随机数不一样;但是程序启动后,每次也都是播放种子,秒级时间戳,如果时间戳一样,就导致种子一样,生成的随机数就一样,所以五次的随机数是一样的。

通过上面的例子。可以知道,播放种子不是必须的,除非要求每次启动程序的时候随机数不一样。

并且,要设置种子的情况下,应该放在整个程序启动的时候,而且只需要设置一次即可。修改上面的例子:

package main

import (
   "fmt"
   "math/rand"
   "time"
)

func main(){
   rand.Seed(time.Now().UnixNano()) // 纳秒时间戳
   for i := 0; i<5; i++ {
      data := rand.Int63n(100)
      fmt.Println(data)
   }
}

运行 go run main.go
打印
3
49
46
83
25

再次运行 go run main.go
打印
39
3
14
42
65

这次就是理想的结果了。使用纳秒时间戳基本就没问题了,因为我们的程序几乎不会在1纳秒时间内多次启动的。

下面来讲讲rand包的具体用法

rand 包提供了两块的内容,一块是基于 Rand 结构体及其方法;另一块是基于 Rand 结构体再封装的可直接调用的方法 rand.xxx,查看源码就知道它们是同样的功能。

所以,生成随机数有两种方式

rander := rand.New(rand.NewSource(time.Now().UnixNano()))
n1 := rander.Intn(100)

rand.Seed(time.Now().UnixNano())
n2 := rand.Intn(100)

使用第一种方法,将 rander 作为包的全局变量,这样就只会设置一次种子。

var Rander = rand.New(rand.NewSource(time.Now().UnixNano()))

随机整数

func (r *Rand) Int() int
func (r *Rand) Int31() int32
func (r *Rand) Int63() int64
func (r *Rand) Uint32() uint32
func (r *Rand) Uint64() uint64
func (r *Rand) Intn(n int) int
func (r *Rand) Int31n(n int32) int32
func (r *Rand) Int63n(n int64) int64

Int, Int31, Int63 生成的数都太大,一般使用 Intn, Int31n, Int63n。得到的范围 [0, n),想要得到 [0, n],就要使用 Intn(n + 1),想要得到 [10, 100] 的随机数,就要使用 Intn(91) + 10。

随机浮点数

func (r *Rand) Float32() float32
func (r *Rand) Float64() float64

得到 [0, 1) 之间的浮点数,32单精度,64双精度。

基于正态分布的随机浮点数

func (r *Rand) NormFloat64() float64

基于指数分布的随机浮点数

func (r *Rand) ExpFloat64() float64

随机序列

func (r *Rand) Perm(n int) []int

返回一个有n个元素的,[0,n)范围内整数的伪随机排列的切片。

Rander.Perm(10) // [1 8 0 4 7 6 3 2 9 5]

总结:

package main

import (
	"fmt"
	"math/rand"
	"strings"
	"time"
)

func main() {
	s := RandString(10)
	fmt.Println(s)
}

var Rander = rand.New(rand.NewSource(time.Now().UnixNano()))

const letterString = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
const numLetterString = "0123456789"

// 随机生成字符串
func RandStr(n int, letter string) string {
	str := []byte(letter)
	res := ""
	for i := 0; i < n; i++ {
		res += fmt.Sprintf("%c", str[Rander.Intn(strings.Count(letter, "") - 1)])
	}
	return res
}

func RandNumStr(n int) string {
	return RandStr(n, numLetterString)
}

func RandString(n int) string {
	return RandStr(n, letterString)
}

func RandOrder(n int) string {
	return time.Now().Format("20060102150405") + RandNumStr(n)
}

// 包含min, max
func RandNum(min , max int) int {
	return Rander.Intn(max - min + 1) + min
}

2、crypto/rand

实现了用于加解密的更安全的随机数生成器。

变量 var Reader io.Reader
是一个全局、共享的密码用强随机生成器。在Unix类型系统中,会从/dev/urandom读取;而windows中会调用RtlGenRandom API。

提供的方法
1、返回一个基于[0, max)的随机数

// Int returns a uniform random value in [0, max). It panics if max <= 0.
func Int(rand io.Reader, max *big.Int) (n *big.Int, err error)

示例

r, err := rand.Int(rand.Reader, big.NewInt(100))
fmt.Println(r.Int64(), err)

2、根据指定的位数bits,返回一个数,大概率是素数,(不知道这个函数是干嘛用的)

// Prime returns a number, p, of the given size, such that p is prime
// with high probability.
// Prime will return error for any error returned by rand.Read or if bits < 2.
func Prime(rand io.Reader, bits int) (p *big.Int, err error)

示例

p, err := rand.Prime(rand.Reader, 8)
fmt.Println(p.Int64(), err)

8个二进制位的最大值为255,此处会随机返回小于255的素数。

3、生成随机的二进制序列

// Read is a helper function that calls Reader.Read using io.ReadFull.
// On return, n == len(b) if and only if err == nil.
func Read(b []byte) (n int, err error) {
	return io.ReadFull(Reader, b)
}

比如,随机生成16个字节的数据

var b [16]byte
n, err := rand.Read(b[:])
fmt.Println(n, err)
fmt.Println(b)

返回值
16 <nil>
[94 184 113 36 224 18 239 52 69 242 14 84 174 113 125 15]

我们可以将其转换成16进制数,也就是32位

buf := make([]byte, 32)
hex.Encode(buf, b[:])
fmt.Println(string(buf))

得到
5eb87124e012ef3445f20e54ae717d0f

通过这个方法可以生成随机的字符串。

3、关于并发安全性

源码里有说明,包级别的函数是并发安全的。

Top-level functions, such as [Float64] and [Int], are safe for concurrent use by multiple goroutines.

而你自己使用 Source 来生成随机数则不是并发安全的。

// A Source represents a source of uniformly-distributed
// pseudo-random int64 values in the range [0, 1<<63).
//
// A Source is not safe for concurrent use by multiple goroutines.
type Source interface

原因就是,生成器是按固定规则生产序列的,如果多个goroutines同时拿到了这个生成器,那得到的随机数就可能一样,这就是并发安全问题。而包级别的函数使用了atomic来控制生成器。

// globalRand returns the generator to use for the top-level convenience
// functions.
func globalRand() *Rand {
	if r := globalRandGenerator.Load(); r != nil {
		return r
	}

	// This is the first call. Initialize based on GODEBUG.
	var r *Rand
	if randautoseed.Value() == "0" {
		randautoseed.IncNonDefault()
		r = New(new(lockedSource))
		r.Seed(1)
	} else {
		r = &Rand{
			src: &runtimeSource{},
			s64: &runtimeSource{},
		}
	}

	if !globalRandGenerator.CompareAndSwap(nil, r) {
		// Two different goroutines called some top-level
		// function at the same time. While the results in
		// that case are unpredictable, if we just use r here,
		// and we are using a seed, we will most likely return
		// the same value for both calls. That doesn't seem ideal.
		// Just use the first one to get in.
		return globalRandGenerator.Load()
	}

	return r
}

二、Go1.20及以后

Go1.20 Release Log 中有说明

The math/rand package now automatically seeds the global random number generator (used by top-level functions like Float64 and Int) with a random value, and the top-level Seed function has been deprecated. Programs that need a reproducible sequence of random numbers should prefer to allocate their own random source, using rand.New(rand.NewSource(seed)).

Programs that need the earlier consistent global seeding behavior can set GODEBUG=randautoseed=0 in their environment.

The top-level Read function has been deprecated. In almost all cases, crypto/rand.Read is more appropriate.

现在每次启动的时候都会使用一个随机的种子来设置generator,这其实是符合正常使用习惯的,因此你无需在手动设置种子,除非你想得到固定的序列。

三、Go1.22及以后

Go1.22 Release Log 提到,增加了 math/rand/v2 包,它是对math/rand的重构,为了保证兼容性,使用了v2子目录,Go是支持带版本号的引入。

包级别的函数

针对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返回一个指数分布的随机数。

三种伪随机数生成器

生成器就是source。

ChaCha8,PCG,Zipf 三个都实现了Source接口。

简单使用

rand.Int32N(100)
rand.New(rand.NewPCG(1, 10000)).Int32N(100)
### Golang 版本间的变化兼容性 #### Go 1.x 系列中的重要变更 Go 语言自发布以来一直致力于保持向后兼容性,这使得大多数旧代码可以在新版本上运行而无需修改。然而,在某些情况下仍会引入一些破坏性的更改或重要的功能增强。 - **垃圾回收器改进** 垃圾收集机制在多个版本中得到了优化,显著降低了停顿时间并提高了性能[^1]。 - **并发模型调整** 协程调度策略不断演进,提升了多核处理器上的执行效率。例如 Goroutine 的栈大小动态调整特性增强了内存利用率[^2]。 - **标准库更新** 随着时间和需求的发展,官方持续维护和完善内置模块的功能集。像 `net/http` 包增加了 HTTP/2 支持;还有提到的新版 `math/rand/v2` 提供了更好的随机数生成功能[^4]。 #### 向前兼容性考量 为了保障应用程序能够顺利迁移至较新的发行版,开发者应关注以下几点: - **API 变动通知** 官方文档通常会在每次大版本迭代时公布可能影响现有项目的改动列表。对于计划升级的项目而言,提前查阅这些资料有助于评估潜在风险。 - **工具链辅助** 利用 go tool fix 或其他第三方插件可以帮助自动修正部分已知模式下的不兼容项。尽管如此,人工审查仍然是不可或缺的一环以确保逻辑正确无误[^3]。 ```go // 使用 go vet 工具检测潜在问题 package main import "fmt" func main() { fmt.Println("Checking compatibility...") } ```
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值