【Golang】使用 - 生成随机数

Posted by 西维蜀黍 on 2020-07-06, Last Modified on 2021-09-21

GoLang 中的伪随机数

GoLang 中,我们可以通过 math/rand 包里的方法来生成一个伪随机数:

package main

import (
  "fmt"
  "math/rand"
)

func main() {
  fmt.Println(rand.Int())   // => 5577006791947779410
}

上面的代码中,我们通过 rand.Int() 方法来生成一个伪随机数。看起来好像没什么问题嘛,人家也很 OK 啦。

但是细心的你会发现。无论你运行多少次,它都会生成一样的结果。

我们知道 JavaScript 中的 Math.random() 每次都会返回一个不一样的数字,但是 GoLang 中的伪随机数生成器默认情况下竟然会返回相同的数值,这还不反了天了?

都是伪随机数生成器,为什么差别就这么大呢?这里我们就要了解一下“随机种子”的概念啦。

随机种子

我们知道,伪随机数,是使用一个确定性的算法计算出来的似乎是随机的数序,因此伪随机数实际上并不随机。

那么自然,在计算伪随机数时假如使用的开始值不变的话,那么算法计算出的伪随机数的数序自然也是不变的咯。

这个“开始值”,就被称为随机种子(seed)。

Int returns a non-negative pseudo-random int from the default Source.

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.

查阅文档,我们得知,Int() 函数是从 default Source(默认源)中产生的伪随机数。

而这个 default Source,我们从 Seed 部分可以看到,如果你没有设置随机种子,那么默认初始种子总是从 1 开始。

既然随机种子一样,那自然其结果也是一样的。

package main

import (
    "fmt"
    "math/rand"
)

func main() {

    rand.Seed(20)
    fmt.Printf("%d ", rand.Intn(100))
    fmt.Printf("%d ", rand.Intn(100))
    fmt.Printf("%d \n", rand.Intn(100))

    rand.Seed(20)
    fmt.Printf("%d ", rand.Intn(100))
    fmt.Printf("%d ", rand.Intn(100))
    fmt.Printf("%d \n", rand.Intn(100))

    fmt.Println()
}

Output:

$ go run same_seed.go 
30 48 40 
30 48 40 

随机的伪随机数(Pseudo-random Number)

我们已经知道了默认随机种子是从 1 开始,那么我们只要在每次生成随机数之前先设置一个不一样的种子,那么其结果自然也就不一样了。

我们要保证每次伪随机数生成器工作时使用的是不同的种子,通常的做法是采用当前时间作为种子。

package main

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

func main() {
  rand.Seed(int64(time.Now().UnixNano()))

  fmt.Println(rand.Int())
  // Intn returns a random int n, 0 <= n < 100.
  fmt.Print(rand.Intn(100))
  // rand.Float64 returns a float64 f, 0.0 <= f < 1.0.
  fmt.Println(rand.Float64())
}

这样,由于种子不同,我们每次运行的结果也就不一样。我们就能达到获取伪随机数的目的啦。

真随机数

如果我们的应用对安全性(security-sensitive)要求比较高,需要使用真随机数的话,那么可以使用 crypto/rand 包中的方法。

package main

import (
  "crypto/rand"
  "fmt"
  "math/big"
)

func main() {
  // 生成 20 个 [0, 100) 范围的真随机数。
  for i := 0; i < 20; i++ {
    result, _ := rand.Int(rand.Reader, big.NewInt(100))
    fmt.Println(result)
  }
}

上面的程序每次运行的结果都是不一样的,会真正随机的生成随机数。

Usage

Top-level Functions

Top-level functions, such as Float64 and Int, use a default shared Source that produces a deterministic sequence of values each time a program is run. Use the Seed function to initialize the default Source if different behavior is required for each run.

The default Source is safe for concurrent use by multiple goroutines, but Sources created by NewSource are not.

最简单的usage,就是不创建自己的 Rand struct,也不更改随机种子(seed)。

package main

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

func main() {
  fmt.Println(rand.Int())
  // Intn returns a random int n, 0 <= n < 100.
  fmt.Print(rand.Intn(100))
  // rand.Float64 returns a float64 f, 0.0 <= f < 1.0.
  fmt.Println(rand.Float64())
}

这样,由于种子相同,我们每次运行的结果也就一定相同了。

Source Code

// /usr/local/go/src/math/rand/rand.go

var globalRand = New(&lockedSource{src: NewSource(1).(*rngSource)})

// Intn returns, as an int, a non-negative pseudo-random number in [0,n)
// from the default Source.
// It panics if n <= 0.
func Intn(n int) int { return globalRand.Intn(n) }

// Float64 returns, as a float64, a pseudo-random number in [0.0,1.0)
// from the default Source.
func Float64() float64 { return globalRand.Float64() }

// Intn returns, as an int, a non-negative pseudo-random number in [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)))
}

Create Your Own Seed

我们要保证每次伪随机数生成器工作时使用的是不同的种子,通常的做法是采用当前时间作为种子。

package main

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

func init() {
    rand.Seed(time.Now().UnixNano())
}

func main() {
  fmt.Println(rand.Int())
  // Intn returns a random int n, 0 <= n < 100.
  fmt.Print(rand.Intn(100))
  // rand.Float64 returns a float64 f, 0.0 <= f < 1.0.
  fmt.Println(rand.Float64())
}

Create Your Own Rand Struct

The default number generator is deterministic, so it’ll produce the same sequence of numbers each time by default. To produce varying sequences, give it a seed that changes.

s1 := rand.NewSource(time.Now().UnixNano())
r1 := rand.New(s1)
fmt.Print(r1.Intn(100), ",")

Source Code

// /usr/local/go/src/math/rand/rand.go

// 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 rngSource
	rng.Seed(seed)
	return &rng
}

// 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}
}

值得注意的是,NewSource 不应该被 concurrently的call。

Benchmark

math/rand is much faster than crypto/rand, but it returns only a pseudo random number.

// Package random compares math/rand with crypto/rand.
// math/rand is much faster than crypto/rand, but it
// returns only a pseudo random number.
package random

import (
	crand "crypto/rand"
	"encoding/base64"
	"math/big"
	mrand "math/rand"
	"testing"
)

func BenchmarkMathRand(b *testing.B) {
	for n := 0; n < b.N; n++ {
		mrand.Int63n(0xFFFF)
	}
}

func BenchmarkCryptoRand(b *testing.B) {
	for n := 0; n < b.N; n++ {
		_, err := crand.Int(crand.Reader, big.NewInt(0xFFFF))
		if err != nil {
			panic(err)
		}
	}
}

func BenchmarkCryptoRandString(b *testing.B) {
	for n := 0; n < b.N; n++ {
		_, err := GenerateRandomString(32)
		if err != nil {
			panic(err)
		}
	}
}

func GenerateRandomBytes(n int) ([]byte, error) {
	b := make([]byte, n)
	_, err := mrand.Read(b)
	if err != nil {
		return nil, err
	}

	return b, nil
}

func GenerateRandomString(s int) (string, error) {
	b, err := GenerateRandomBytes(s)
	return base64.URLEncoding.EncodeToString(b), err
}

Output:

$ go test -bench=.       
goos: darwin
goarch: amd64
pkg: awesomeProject/fibo
BenchmarkMathRand-32                    213111542                5.68 ns/op
BenchmarkCryptoRand-32                  11499055               106 ns/op
BenchmarkCryptoRandString-32            10829665               108 ns/op
PASS
ok      awesomeProject/random     5.899s

Reference