Go库代码应如何初始化并使用随机数生成?

时间:2014-01-04 16:11:26

标签: go

在编写需要使用随机数的Go 时,初始化和使用随机数的最佳方法是什么?

我知道在应用程序中执行此操作的标准方法是:

import (
    "math/rand"
    "time"
)

// do the initial seeding in an init fn
func init() {
    // set the global seed and use the global fns
    rand.Seed(time.Now().UTC().UnixNano())
}

func main() {
    fmt.Println(rand.Int())
    fmt.Println(rand.Intn(200))
}

因此,当我编写库代码(不在主程序包中)时,我应该这样做:

package libfoo

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

func AwesomeFoo() {
    r :=  rand.Intn(1000)
    // ...
}

使用我的库的应用程序也可以使用自己的随机数种子并使用rand.Intn,所以我的问题确实是 - 有一个库种子随机数生成器和一些应用程序代码(或其他图书馆)也这样做?

使用“全局”rand.Intnrand.Int的库也存在任何问题,或者库是否应通过Rand创建自己的私有rand.New(src)对象并使用该库代替?

我没有任何特别的理由认为这是不安全的,但我对加密和PRNG足够了解,知道如果你不知道你在做什么就很容易出错。

例如,这是一个需要随机性的Knuth(Fisher-Yates)shuffle的简单库:https://gist.github.com/quux00/8258425

2 个答案:

答案 0 :(得分:4)

不要为全局随机数生成器设定种子。这应该留给包装主。

如果你关心你的种子是什么,你应该创建自己的私人Rand对象。如果你不在乎,你可以使用全局来源。

如果你关心你的数字实际上是随机的,你应该使用crypto / rand而不是math / rand。

答案 1 :(得分:4)

什么是最好的只取决于您正在编写的应用程序类型以及您要创建的库类型。如果我们不确定,我们可以通过Go接口使用一种依赖注入形式来获得最大的灵活性。

考虑以下使用rand.Source接口的朴素蒙特卡洛积分器:

package monte

import (
    "math/rand"
)

const (
    DEFAULT_STEPS = 100000
)

type Naive struct {
    rand *rand.Rand
    steps int
}

func NewNaive(source rand.Source) *Naive {
    return &Naive{rand.New(source), DEFAULT_STEPS}
}

func (m *Naive) SetSteps(steps int) {
    m.steps = steps
}

func (m *Naive) Integrate1D(fn func(float64) float64, a, b float64) float64 {
    var sum float64
    for i := 0; i < m.steps; i++ {
        x := (b-a) * m.rand.Float64() 
        sum += fn(x)
    }
    return (b-a)*sum/float64(m.steps)
}

然后我们可以使用这个包计算pi的值:

func main() {
    m := monte.NewNaive(rand.NewSource(200))
    pi := 4*m.Integrate1D(func (t float64) float64 {
        return math.Sqrt(1-t*t)
    }, 0, 1)
    fmt.Println(pi)
}

在这种情况下,算法结果的质量取决于所使用的伪随机数生成器的类型,因此我们需要为用户提供一种方法将一个生成器换成另一个生成器。这里我们定义了一个opaque类型,它在构造函数中采用随机数源。通过使随机数生成器满足rand.Source接口,我们的应用程序编写器可以根据需要交换随机数生成器。

然而,在许多情况下,这正是我们不想做的事情。考虑随机密码或密钥生成器。在这种情况下,我们真正想要的是真正随机数据的高熵源,因此我们应该在内部使用crypto/rand包并隐藏应用程序编写者的详细信息:

package keygen

import (
    "crypto/rand"
    "encoding/base32"
)

func GenKey() (string, error) {
    b := make([]byte, 20)
    if _, err := rand.Read(b); err != nil {
        return "", err
    }
    enc := base32.NewEncoding("ABCDEFGHIJKLMNOPQRSTUVWXYZ346789")
    return enc.EncodeToString(b), nil
}

希望这有助于您做出决定。如果代码是针对您自己的应用程序或特定公司内的应用程序而不是行业范围或公共用途,那么倾向于使用最少内部的库设计并创建最少的依赖项而不是最通用的设计,因为这将简化维护和缩短实施时间。

基本上,如果感觉有点矫枉过正,那可能就是。

在Knuth Shuffle的情况下,要求只是一个不错的伪随机数生成器,所以你可以简单地使用一个内部种子rand.Rand对象,它是你的包私有的,如下所示:

package shuffle

import (
    "math/rand"
    "time"
)

var r *rand.Rand

func init() {
    r = rand.New(rand.NewSource(time.Now().UTC().UnixNano()))
}

func ShuffleStrings(arr []string) {
    last := len(arr)-1
    for i := range arr {
        j := r.Intn(last)
        arr[i], arr[j] = arr[j], arr[i]
    }
}

然后应用程序不必担心它是如何工作的:

package main

import (
    "shuffle"
    "fmt"
)

func main() {
    arr := []string{"a","set","of","words"}
    fmt.Printf("Shuffling words: %v\n", arr)
    for i := 0; i<10; i++ {
        shuffle.ShuffleStrings(arr)
        fmt.Printf("Shuffled words: %v\n", arr)
    }
}

这可以防止应用程序通过调用rand.Seed来意外重新播种程序包使用的随机数生成器。