文章目录

  • 1.前言
  • 2.剖析
  • 3.核心函数
  • 4.需要避开的坑
  • 参考文献

1.前言

Go 版本为 go 1.17。

go version go1.17 darwin/amd64

本文以type rand struct 为切入点,看下 Go 伪随机数的实现原理。

// A Rand is a source of random numbers.
type Rand struct {src Sources64 Source64 // non-nil if src is source64// readVal contains remainder of 63-bit integer used for bytes// generation during most recent Read call.// It is saved so next Read call can start where the previous// one finished.readVal int64// readPos indicates the number of low-order bytes of readVal// that are still valid.readPos int8
}

2.剖析

伪随机数如果每次种子相同,那么生成的随机序列也是相同的。下面通过赋予不同的种子创建一个随机数发生器。

r := rand.New(rand.NewSource(time.Now().UnixNano()))r.Intn(62)  // 生成 [0, n) 内的整数

我们以 rand.Intn() 为例,看下随机数的实现。

// Intn returns, as an int, a non-negative pseudo-random number in the half-open interval [0,n).
// It panics if n <= 0.
func (r *Rand) Intn(n int) int {if n <= 0 {panic("invalid argument to Intn")}if n <= 1<<31-1 {return int(r.Int31n(int32(n)))}return int(r.Int63n(int64(n)))
}

我传入的是 62 小于,所以调用的是r.Int31n()

// Int31n returns, as an int32, a non-negative pseudo-random number in the half-open interval [0,n).
// It panics if n <= 0.
func (r *Rand) Int31n(n int32) int32 {if n <= 0 {panic("invalid argument to Int31n")}if n&(n-1) == 0 { // n is power of two, can maskreturn r.Int31() & (n - 1)}max := int32((1 << 31) - 1 - (1<<31)%uint32(n))v := r.Int31()for v > max {v = r.Int31()}return v % n
}

因为我们的 n=62 不是 2 的幂,所以走的是下面的逻辑。其中这个 max 操作需要明白其作用。

max := int32((1 << 31) - 1 - (1<<31)%uint32(n))

max 就是将 int32 范围 [0, (1 << 31) - 1] 内最后取模不能覆盖 [0, n) 的部分去掉,保证 [0, n) 内各个整数出现的概率相同。看下几个具体的值就明白了。

var n int32 = 62
tail := (1<<31) % uint32(n)
max := int32((1 << 31) - 1 - (1<<31)%uint32(n))
fmt.Println(tail)       // 2
fmt.Println(max)        // 2147483645
fmt.Println(max % n)    // 61

再看下真正产生随机数的函数r.Int31()

// Int31 returns a non-negative pseudo-random 31-bit integer as an int32.
func (r *Rand) Int31() int32 { return int32(r.Int63() >> 32) }

其又调用的是r.Int63(),取高 31 位作为 int32 的随机值。

// Int31 returns a non-negative pseudo-random 31-bit integer as an int32.
func (r *Rand) Int31() int32 { return int32(r.Int63() >> 32) }

其又调用的是r.src.Int63()。我们先看下type Source interface的定义。

// A Source represents a source of uniformly-distributed
// pseudo-random int64 values in the range [0, 1<<63).
type Source interface {Int63() int64Seed(seed int64)
}

我们初始化 Rand 的时候,通过rand.New(rand.NewSource(seed))创建,看下rand.New()的实现。

// New returns a new Rand that uses random values from src
// to generate other random values.
func New(src Source) *Rand {s64, _ := src.(Source64)return &Rand{src: src, s64: s64}
}

可见 Rand 使用的是rand.NewSource()传入的 Source,看下rand.NewSource()的实现。

// NewSource returns a new pseudo-random Source seeded with the given value.
// Unlike the default Source used by top-level functions, this source is not
// safe for concurrent use by multiple goroutines.
func NewSource(seed int64) Source {var rng rngSourcerng.Seed(seed)return &rng
}

可见 Source 的实际类型是 rngSource,实现了接口 Source,其定义如下:

type rngSource struct {tap  int           // index into vecfeed int           // index into vecvec  [rngLen]int64 // current feedback register
}

我们再看下rngSource.Int63()的实现。

// Int63 returns a non-negative pseudo-random 63-bit integer as an int64.
func (rng *rngSource) Int63() int64 {return int64(rng.Uint64() & rngMask)
}

其中 rngMask 的定义如下,表示 Int64 的最大值,作用是作为掩码。

rngMax   = 1 << 63
rngMask  = rngMax - 1

至此,我们找到了随机数生成的两个核心函数,一个是根据种子初始化数组 rngSource.vec 的函数rngSource.Seed(),一个是从数组 rngSource.vec 取出随机数的rngSource.Uint64()

3.核心函数

我们看下随机数的真正生成函数rngSource.Uint64()

// Uint64 returns a non-negative pseudo-random 64-bit integer as an uint64.
func (rng *rngSource) Uint64() uint64 {rng.tap--if rng.tap < 0 {rng.tap += rngLen}rng.feed--if rng.feed < 0 {rng.feed += rngLen}x := rng.vec[rng.feed] + rng.vec[rng.tap]rng.vec[rng.feed] = xreturn uint64(x)
}

实际上,我们在调用Intn(), Int31n(), Int63(), Int63n()等其他函数,最终调用到都是函数rngSource.Uint64()可以看到每次调用就是利用 rng.feed, rng.tap 从 rng.vec 中取到两个值相加的结果返回,同时这个结果又重新放入 rng.vec。这么做的目的显而易见,让随机数更加丰富随机,而不是仅局限于 rng.vec 数组中的值。

另外 rng.tap、rng.feed 和 rng.vec 的初始化工作是在函数rngSource.Seed()中完成的。

// Seed uses the provided seed value to initialize the generator to a deterministic state.
func (rng *rngSource) Seed(seed int64) {rng.tap = 0rng.feed = rngLen - rngTapseed = seed % int32maxif seed < 0 {seed += int32max}if seed == 0 {seed = 89482311}x := int32(seed)for i := -20; i < rngLen; i++ {x = seedrand(x)if i >= 0 {var u int64u = int64(x) << 40x = seedrand(x)u ^= int64(x) << 20x = seedrand(x)u ^= int64(x)u ^= rngCooked[i]rng.vec[i] = u}}
}

相关常量定义如下。

const (rngLen   = 607rngTap   = 273rngMax   = 1 << 63rngMask  = rngMax - 1int32max = (1 << 31) - 1
)

其中函数seedrand()也需要关注下,其是完成对 seed 的变换。这也导致了相同的 seed, 最终设置到 rng.vec 里面的值是相同的,通过rngSource.Uint64()取出的也是相同的值。

// seed rng x[n+1] = 48271 * x[n] mod (2**31 - 1)
func seedrand(x int32) int32 {const (A = 48271Q = 44488R = 3399)hi := x / Qlo := x % Qx = A*lo - R*hiif x < 0 {x += int32max}return x
}

至此我们大值了解了 math/rand 的随机数的生成过程。

4.需要避开的坑

通过上面对 math/rand 的分析,我们应该知道使用时需要避开的坑。

(1)相同种子,每次运行的结果是一样的。 因为随机数是从 rng.vec 数组中取出来的,这个数组是根据种子生成的,相同的种子生成的 rng.vec 数组是相同的。

(2)不同种子,每次运行的结果可能一样。 因为根据种子生成 rng.vec 数组时会有一个取模的操作,模后的结果可能相同,导致 rng.vec 数组相同。

(3)rand.New 初始化出来的 rand 不是并发安全的。 因为每次利用 rng.feed, rng.tap 从 rng.vec 中取到随机值后会将随机值重新放入 rng.vec。如果想并发安全,可以使用全局的随机数发生器 rand.globalRand。

(4)不同种子,随机序列发生碰撞的概率高于单个碰撞概率的乘积。 这是因为存在生日问题。

比如我要随机从数字字母集(62个字符)中获取长度为 6 的邀请码,种子使用用户ID,如果生成 100W 个邀请码,假设前 100W 一个都不重复,那么下一个重复的概率是((1/62)^6 * 100W)≈1/5.6W,冲突率已经到了在万分之一的概率,远大于想象中的(1/62)^6


参考文献

CSDN.一文完全掌握 Go math/rand
CSDN.记录使用 Golang math/rand 随机数遇到的坑

Golang math/rand 源码剖析避坑指南相关推荐

  1. 《GDAL源码剖析与开发指南》一一1.9 简单的调用

    本节书摘来自异步社区出版社<GDAL源码剖析与开发指南>一书中的第1章,第1.9节,作者:李民录 更多章节内容可以访问云栖社区"异步社区"公众号查看. 1.9 简单的调 ...

  2. java实现gdal栅格矢量化,《GDAL源码剖析与开发指南》一一1.5 GDAL源码目录

    本节书摘来自异步社区出版社<GDAL源码剖析与开发指南>一书中的第1章,第1.5节,作者:李民录 更多章节内容可以访问云栖社区"异步社区"公众号查看. 1.5 GDAL ...

  3. 《GDAL源码剖析与开发指南》导读

    前言 GDAL源码剖析与开发指南 GDAL全称是Geospatial Data Abstraction Library(地理空间数据抽象库),是一个在X/MIT许可协议下读写空间数据(包括栅格数据和矢 ...

  4. Ubuntu18.04 编译Android 10源码 并烧录源码到pixel3的避坑指南

    Ubuntu18.04 编译Android 10源码 并烧录源码到pixel3的避坑指南 实验环境 下载Android源码树 在pixel3上安装手机驱动版本 编译Android源码 Android ...

  5. mac下编译android源码避坑指南(新)

    截至目前mac环境下android源码编译最新避坑指南 避坑方法 配置(不说配置的都是耍流氓) 下载 编译 烧录 注意事项 避坑方法 源码.SDK.机型版本一定要清楚,有些特殊的版本需要特殊的方法,官 ...

  6. 安卓源码避坑指南3——拨打电话的SIM卡无效导致蓝牙断开连接

    安卓源码避坑指南3--拨打电话的SIM卡无效导致蓝牙断连 它来了.它来了,它带着BUG赶来了,欢迎大家查看本期的安卓源码避坑指南.本期的问题场景比较特殊,电话SIM卡是无效的(欠费过期了,很是贫穷-) ...

  7. 4.2.10 Kafka源码剖析, 阅读环境搭建, broker启动流程, topic创建流程, Producer生产者流程, Consumer消费者流程,

    目录 4.1 Kafka源码剖析之源码阅读环境搭建 4.1.1 安装配置Gradle 4.1.2 Scala的安装和配置 4.1.3 Idea配置 4.1.4 源码操作 4.2 Kafka源码剖析之B ...

  8. ThreadLocal源码剖析

    目录 一.ThreadLocal 1.1源码注释 1.2 源码剖析 散列算法-魔数0x61c88647 set操作 get操作 remove操作 1.3 功能测试 1.4 应用场景 二.变量可继承的T ...

  9. 【Java集合源码剖析】Hashtable源码剖析

    转载请注明出处:http://blog.csdn.net/ns_code/article/details/36191279 Hashtable简介 Hashtable同样是基于哈希表实现的,同样每个元 ...

最新文章

  1. 高逼格UILabel的闪烁动画效果
  2. 基于VMware vSphere 5.0的服务器虚拟化实践(9)
  3. 蓝桥杯-十六进制转八进制(java)
  4. 三十五、深入Python的引用计数
  5. 【转】 Linux内核中读写文件数据的方法--不错
  6. [Hadoop in China 2011] 华为 - NoSQL/NewSQL在传统IT产业的机遇和挑战
  7. c语言中如何防止输入的格式存在错误,C语言如何避免输入
  8. 豆瓣再被约谈处罚150万!一年被罚20次,豆瓣到底怎么了?
  9. Spring Boot使用自定义的properties
  10. java应用uploadify 3.2丢失session
  11. css span 右端对齐_使用 CSS 实现具有方面感知的幽灵按钮
  12. mysql 伪哈希_MySQL技巧--伪哈希索引
  13. 【Java NIO的深入研究2】RandomAccessFile的使用
  14. Java项目:医院门诊收费管理系统(java+html+jdbc+mysql)
  15. 拉普拉斯方程(Laplace's equation)-- 更新中
  16. Win10相机打不开提示:我们找不到你的相机 错误代码0xa00f4244!
  17. C#,数值计算,矩阵的行列式(Determinant)、伴随矩阵(Adjoint)与逆矩阵(Inverse)的算法与源代码
  18. 中国旅行包行业市场供需与战略研究报告
  19. android锁屏密码文件夹,深入理解Android M 锁屏密码存储方式
  20. 车牌识别关键技术-车牌定位

热门文章

  1. 中秋逢国庆 | 盛世华诞 阖家团圆
  2. 奇怪,勒索软件Ryuk 新版本把这些 *UNIX 文件夹列入了黑名单
  3. 这个 WebKit 漏洞助力 Pwn2Own 冠军斩获5.5万美元赏金(详细分析)
  4. HTTP2.0,HTTP1.1,HTTP1.0三者在通性性能上的优化方法
  5. 计算机网络知识点回顾
  6. ExtJS4.2学习(11)——高级组件之Grid
  7. 不能因技术后天的死 而迷茫了今天的“学” 生
  8. 网上一片红色的中国心,我也来跟随潮流,表达对祖国的热爱!
  9. python gzip压缩文件
  10. python程序封装成exe_如何将python脚本封装成exe程序?