Skip to content

Latest commit

 

History

History
201 lines (150 loc) · 5.3 KB

randutil.md

File metadata and controls

201 lines (150 loc) · 5.3 KB

pion/randutil

这是一个基础工具库,创建一些数学上的随机数和密码学的随机数

准备分4部分来学习这个库.

数学随机数

对外暴露的是一个接口类型和一个构造数学随机数的函数.

type MathRandomGenerator interface {
  Intn(n int) int
  Uint32() uint32
  Uint64() uint64
  GenerateString(n int, runes string) string
}

一个实现接口的类型

type mathRandomGenerator struct {
  r  *mrand.Rand
  mu sync.Mutex
}

func (g *mathRandomGenerator) Intn(n int) int {
  g.mu.Lock()
  v := g.r.Intn(n)
  g.mu.Unlock()
  return v
}

func (g *mathRandomGenerator) Uint32() uint32 {
  g.mu.Lock()
  v := g.r.Uint32()
  g.mu.Unlock()
  return v
}

func (g *mathRandomGenerator) Uint64() uint64 {
  g.mu.Lock()
  v := g.r.Uint64()
  g.mu.Unlock()
  return v
}

func (g *mathRandomGenerator) GenerateString(n int, runes string) string {
  letters := []rune(runes)
  b := make([]rune, n)
  for i := range b {
    b[i] = letters[g.Intn(len(letters))]
  }
  return string(b)
}

从结构上看,并不复杂.相对于标准库来说,少了一个默认处理:

  • 构造一个默认的mathRandomGenerator对象,这个对象是不暴露的
  • 将接口的4个方法,通过4个函数对外暴露

文档说明:

  • 这里的随机数,可用于非加密的唯一id,或随机端口号

这儿的随机都是利用math/rand包完成的,先看下math/rand包.

math/rand

rand包是伪随机数生成器.

生成器需要有个源,用Seed函数可以生成随机数的源. 源一定,生成的随机数序列也是一定的.

源source和随机数种子seed不是一个概念,分析源码可知.

func NewSource(seed int64) Source
func New(src Source) *
func (r *Rand) Intn(n int) int
func (r *Rand) Uint32() uint32
func (r *Rand) Uint64() uint64

从上面的部分接口可以看出,从随机数种子生成源,从源生成随机数生成器, 最后通过生成器生成各种数值.

mathRandomGenerator分析

mathRandomGenerator结构体包含一个生成器和一个互斥量. 她的4个方法都很好理解.

看下构造函数.

func NewMathRandomGenerator() MathRandomGenerator {
  seed, err := CryptoUint64()
  if err != nil {
    seed = uint64(time.Now().UnixNano())
  }

  return &mathRandomGenerator{r: mrand.New(mrand.NewSource(int64(seed)))}
}

用CryptoUint64生成一个种子,如果不成功,还是选用时间为种子.

整体来说还是蛮简单的,不复杂.

数学随机数的测试

测试例子做了一个撞库测试, MathRandom.gen是从字母序列中随机选取10个字母组成一个字符串. 每次生成100个字符串,判断相互之间是否重复,整个过程重复100次.

const runesAlpha = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"

func TestRandomGeneratorCollision(t *testing.T) {
  g := NewMathRandomGenerator()

  testCases := map[string]struct {
    gen func(t *testing.T) string
  }{
    "MathRandom": {
      gen: func(t *testing.T) string {
        return g.GenerateString(10, runesAlpha)
      },
    },
    "CryptoRandom": {
      gen: func(t *testing.T) string {
        s, err := GenerateCryptoRandomString(10, runesAlpha)
        if err != nil {
          t.Fatal(err)
        }
        return s
      },
    },
  }

  const N = 100
  const iteration = 100

  for name, testCase := range testCases {
    testCase := testCase
    t.Run(name, func(t *testing.T) {
      for iter := 0; iter < iteration; iter++ {
        var wg sync.WaitGroup
        var mu sync.Mutex

        rands := make([]string, 0, N)

        for i := 0; i < N; i++ {
          wg.Add(1)
          go func() {
            r := testCase.gen(t)
            mu.Lock()
            rands = append(rands, r)
            mu.Unlock()
            wg.Done()
          }()
        }
        wg.Wait()

        if len(rands) != N {
          t.Fatal("Failed to generate randoms")
        }

        for i := 0; i < N; i++ {
          for j := i + 1; j < N; j++ {
            if rands[i] == rands[j] {
              t.Fatalf("generateRandString caused collision: %s == %s",
                rands[i], rands[j])
            }
          }
        }
      }
    })
  }
}

整个撞库测试并不复杂,但是可以拆的细一点,函数太长了.

密码学随机数

crypto.go,对外暴露了两个函数:

  • GenerateCryptoRandomString 利用crypto库随机数种子来随机生成字符串
  • CryptoUint64 随机出长正整形

这两个是功能性函数,并没有和特定的类型做绑定.

密码学随机数的测试

TestCryptoRandomGenerator, 随机字符串,之后用正则表达式来判断是否是字符.

TestCryptoUint64, 随机一万次,记录随机的最大最小值,之后判断随机值的落点范围, 能取的最大值是0xFFFFFFFFFFFFFFFF(uint64,8字节),判断的范围是: (0x1000000000000000,0xF000000000000000).

这两个测试函数对最后的结果做了测试,并没有对过程进行测试.

这种写法也非常值得学习:

  • 对字符串,用正则表达式来测试
  • 对数字,测试其范围