golang随机数源码分析及应用

引言

大家刚开始使用随机数的时候可能会这样写, 但是他会产生一个问题,这是什么问题呢

func main() {
	for i := 0; i < 10; i++ {
		rand.Seed(time.Now().Unix())
		fmt.Println(rand.Intn(100))
	}
}

发现打印出来的结果都是相同的

让我们看看用代码分析为什么产生这个问题

首先golang定义一个长度为607个原素的数组,类型是int64

rand/rng.go文件

var (
   // rngCooked used for seeding. See gen_cooked.go for details.
   rngCooked [607]int64 = [...]int64{1395769623340756751...}
)

通过seed方法给以上的 rngCooked 数组初始化每个元素

// Seed uses the provided seed value to initialize the generator to a deterministic state.
func (rng *rngSource) Seed(seed int64) {
   rng.tap = 0
   rng.feed = rngLen - rngTap  // rngLen和rngTap是常量

   seed = seed % int32max
   if seed < 0 {
      seed += int32max
   }
   if seed == 0 {
      seed = 89482311
   }

   x := int32(seed)
   for i := -20; i < rngLen; i++ { // 为什么要冗余20次,我理解是让数据更加随机,你也可以设置100次,当然数据更随机
      x = seedrand(x)
      if i >= 0 {
         var u int64
         u = int64(x) << 40
         x = seedrand(x)
         u ^= int64(x) << 20
         x = seedrand(x)
         u ^= int64(x)
         u ^= rngCooked[i]
         rng.vec[i] = u
      }
   }
}

调用rand.Int31n(100)方法获得100以内的随机数

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 mask
      return 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
}

以上就是获得随机数的基本流程,但是似乎并没有解释 循环读取的随机数总是相同的,让我们再来看一段代码

  这个代码运用了典型的 线性同余法(LCG)来获取随机数,线性同余法作为一种伪随机数生成算法,在许多应用场景中基本满足了需求。
// seed rng x[n+1] = 48271 * x[n] mod (2**31 - 1)
func seedrand(x int32) int32 {
   const (
      A = 48271
      Q = 44488
      R = 3399
   )

   hi := x / Q
   lo := x % Q
   x = A*lo - R*hi
   if x < 0 {
      x += int32max
   }
   return x
}

结论:所以我们可以看出,当X相同的情况下, 生成的随机数序列也是相同的,同一时刻拿到的随机数也就是相同的

安全性

我们在获取随机数的时候是用的排他锁,也就是并发安全的

func (r *lockedSource) Int63() (n int64) {
   r.lk.Lock()
   n = r.source().Int63()
   r.lk.Unlock()
   return
}

非并发安全情况

var (
	rrRand = rand.New(rand.NewSource(time.Now().Unix()))
	rrRand.Intn(100)
	此时并发访问rrRand.Intn(100) 就会出现panic
)
func (r *Rand) Seed(seed int64) {
   if lk, ok := r.src.(*lockedSource); ok {  // 因为rand.NewSource(time.Now().Unix())不是lockedSource,所以不会走这个逻辑
      lk.seedPos(seed, &r.readPos)
      return
   }

   r.src.Seed(seed) // 会走这个逻辑
   r.readPos = 0
}

在什么情况下需要指定不同的seed

比如模拟投掷骰子的实验,每次实验投掷10次骰子,并记录每次投掷的结果。需要运行3次实验,每次实验使用不同的种子值。总之,在业务中,或者在大多数情况下我们不需要指定种子值(go 1.20以后)

应用

  1. 在go 1.20以前,需要在程序启动时手动加上 rand.Seed(time.Now().UnixNano()) 种子值,但是在go 1.20以后就直接使用rand.IntN(100) 获取随机数就可以了 ,如下代码
func (r *lockedSource) source() *rngSource {
   if r.s == nil {
      var seed int64
      if randautoseed.Value() == "0" {
         seed = 1 // 环境变量为0,seed默认1,每次获得的随机数序列是一样的
      } else {
         seed = int64(fastrand64())
      }
      r.s = newSource(seed)
   }
   return r.s
}
  1. 如果需要特定的随机序列 请单独使用 rrRand = rand.New(rand.NewSource(time.Now().UnixNano())),这样就不会影响其他使用方使用全局随机数,但是请记住这个不是并发安全的,在并发的情况下需要加排他锁
  2. math.rand包产生的随机数是伪随机数,也就是会存在周期性重复,

对于公式 RandSeed = (A * RandSeed + B) % M 要达到周期重复需满足以下条件

  • B,M互质;
  • M的所有质因数都能整除A-1;
  • 若M是4的倍数,A-1也是;
  • A,B,N[0]都比M小;
  • A,B是正整数。
  1. 如果需要真正意义的随机数,请使用encrypt/rand包 ,由于crypto/rand包使用的是基于密码学的安全随机数生成器(CSPRNG),因此生成的随机数序列是不可预测的、不可重复的。这意味着,即使使用相同的种子值,每次生成的随机数序列也应该是不同的。
posted @   AGopher  阅读(202)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek “源神”启动!「GitHub 热点速览」
· 我与微信审核的“相爱相杀”看个人小程序副业
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· C# 集成 DeepSeek 模型实现 AI 私有化(本地部署与 API 调用教程)
· spring官宣接入deepseek,真的太香了~
点击右上角即可分享
微信分享提示