quux00
quux00

Reputation: 14604

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

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

Upvotes: 2

Views: 1296

Answers (2)

James O'Doherty
James O'Doherty

Reputation: 2246

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

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 (
    "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
}

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

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

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

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

Upvotes: 4

andybalholm
andybalholm

Reputation: 16130

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.

Upvotes: 4

Related Questions