How should Go library code initialize and use random number generation?

huangapple go评论121阅读模式
英文:

How should Go library code initialize and use random number generation?

问题

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

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

import (
    "math/rand"
    "time"
)

// 在init函数中进行初始种子设置
func init() {
    // 设置全局种子并使用全局函数
    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.New(src)创建自己的私有Rand对象并使用它?

我没有特别的理由认为这是不安全的,但我对密码学和伪随机数生成器有足够的了解,知道如果不知道自己在做什么,很容易出错。

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

英文:

When writing a Go library that needs to use random numbers, what is the best way to initialize and consume random numbers?

I know that the std way to do this in an application is:

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

So when I'm writing library code (not in the main package), should I just do the same:

package libfoo

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

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

The application using my library might also do its own random number seeding and use rand.Intn, so my question really is - is there any downside to having a library seed the random number generator and some app code (or another library) do so as well?

Also is there any issue with the library using the "global" rand.Intn or rand.Int or should a library create it's own private Rand object via rand.New(src) and use that instead?

I don't have any particular reason for thinking this is unsafe, but I know enough about crypto and PRNGs to know that it is easy to get something wrong if you don't know what you're doing.

For example, here's a simple library for the Knuth (Fisher-Yates) shuffle that needs randomness: https://gist.github.com/quux00/8258425

答案1

得分: 4

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

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

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

英文:

Don't seed the global random number generator. That should be left to package main.

If you care what your seed is, you should create your own private Rand object. If you don't care, you can just use the global source.

If you care about your numbers actually being random, you should use crypto/rand instead of math/rand.

答案2

得分: 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)
}

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

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

在这种情况下,我们算法结果的质量取决于所使用的伪随机数生成器的类型,因此我们需要提供一种方法,让用户可以替换一个生成器为另一个。在这里,我们定义了一个不透明类型,在其构造函数中接受一个随机数源。通过使他们的随机数生成器满足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而意外重新种子化你的包使用的随机数生成器。

英文:

What's best really just depends on the type of application you're writing and the type of library you want to create. If we're not sure, we can get the most flexibility by using a form of dependency injection through Go interfaces.

Consider the following naive Monte Carlo integrator that takes advantage of the rand.Source interface:

package monte
 
import (
	&quot;math/rand&quot;
)
 
const (
	DEFAULT_STEPS = 100000
)
 
type Naive struct {
	rand *rand.Rand
	steps int
}
 
func NewNaive(source rand.Source) *Naive {
	return &amp;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 &lt; m.steps; i++ {
		x := (b-a) * m.rand.Float64() 
		sum += fn(x)
	}
	return (b-a)*sum/float64(m.steps)
}

We can then use this package to calculate the value of 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)
}

In this case, the quality of our algorithm's results depend on the type of pseudorandom number generator used, so we need to provide a way for users to swap out one generator for another. Here we've defined an opaque type that takes a random number source in its constructor. By having their random number generator satisfy the rand.Source interface, our application writer can then swap out random number generators as needed.

However, there are many cases where this is exactly what we don't want to do. Consider a random password or key generator. In that case, what we really want is a high entropy source of truly random data, so we should just use the crypto/rand package internally and hide the details from our application writers:

package keygen

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

Hopefully that helps you make a decision. If the code is for your own applications or applications within a specific company rather than industry wide or public use, lean towards the library design that exposes the fewest internals and creates the fewest dependencies rather than the most general design, since that will ease maintenance and shorten implementation time.

Basically, if it feels like overkill, it probably is.

In the case of the Knuth Shuffle, the requirements are simply a decent psuedo-random number generator, so you could simply use an internally seeded rand.Rand object that's private to your package like so:

package shuffle

import (
	&quot;math/rand&quot;
	&quot;time&quot;
)

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

Then the application doesn't have to worry about how it works:

package main

import (
	&quot;shuffle&quot;
	&quot;fmt&quot;
)

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

This prevents the application from accidentally reseeding the random number generator used by your package by calling rand.Seed.

huangapple
  • 本文由 发表于 2014年1月5日 00:11:25
  • 转载请务必保留本文链接:https://go.coder-hub.com/20923341.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定