Go: How Are Random Numbers Generated?

Vincent Blanchon
Dec 1 · 4 min read
Illustration created for “A Journey With Go”, made from the original Go Gopher, created by Renee French.

ℹ️ This article is based on Go 1.13.

Go implements two packages to generate random numbers:

  • a pseudo-random number generator (PRNG) in the package math/rand
  • cryptographic pseudorandom number generator (CPRNG), implemented in crypto/rand

If both generate random numbers, your choice will be based on a tradeoff between genuinely random numbers and performance.

Deterministic result

Go rand package uses a single source that produces a deterministic sequence of pseudo-random numbers. This source generates a static sequence of numbers that are later used during the execution. Running your program many times will read the exact same sequence and will produce the same result. Let’s try with a simple example:

func main() {
for i := 0; i < 4; i++ {
println(rand.Intn(100))
}
}

Running this program will produce the same result again and again:

81
87
47
59

Since the source is versioned into the Go standard library, any computer that runs this program will get the exact same result. However, since Go keeps only one sequence of generated numbers, we could wonder how Go manages the interval requested by the user. Go actually uses this sequence of numbers for seeding the source that generates this random number, and then gets its modulo of the requested interval. For example, running the same program with a limit to 10 will give the same result modulo 10:

1
7
7
9

Let’s see how to get a different sequence each time we run our program.

Seeding

Go provides a method, Seed(see int64), that allows you to initiate this default sequence. By default, it uses the value 1. Using another value will provide a new sequence but will remain deterministic:

func main() {
rand.Seed(2)
for i := 0; i < 4; i++ {
println(rand.Intn(100))
}
}

Here are the new results:

86
86
92
40

This sequence will remain the same every time you run the program. Here is the workflow to build this sequence:

The sequence is pre-generated at the bootstrap

The solution for getting a brand new sequence is to use a value that changes at the runtime like the current time:

func main() {
rand.Seed(time.Now().UnixNano())
for i := 0; i < 3; i++ {
println(rand.Intn(100))
}
}

This program will use a different sequence every run since the current nanosecond will be different every time. However, if the sequence is now different at every run, the numbers remain pseudo-random. If you are ready to trade performance against a better randomness, Go has another implementation for you.

Random Generator Number

The Go standard library also provides a random number generator that is suitable for cryptographic applications. Therefore, by definition, it cannot be deterministic and must provide a better randomness. Here is an example with this new package crypto/rand:

func main() {
for i := 0; i < 4; i++ {
n, _ := rand.Int(rand.Reader, big.NewInt(100))
println(n.Int64())
}
}

Here are the results:

12
24
56
19

Running the program multiple times will give different results. Internally, Go applies the following rules:

On Linux and FreeBSD, Reader uses getrandom(2) if available, /dev/urandom otherwise.
On OpenBSD, Reader uses getentropy(2).
On other Unix-like systems, Reader reads from /dev/urandom.
On Windows systems, Reader uses the CryptGenRandom API.
On Wasm, Reader uses the Web Crypto API.

However, getting better quality means degraded performance since it has to perform more operations and cannot use a pre-generated sequence.

Performance

To understand the tradeoff between the two different ways to generate random numbers, I ran a benchmark based on the two previous examples. Here are the results:

name    time/op
RandWithCrypto-8 272ns ± 3%

As expected, the crypto package is much slower. However, if you do not have to deal with secure random numbers, the math package is enough and will give you the best performance.

You can also tune the default number generator that is concurrent safe, thanks to an internal mutex. If the generator is not used in a concurrency environment, you can create your generator without lock:

func main() {
gRand := rand.New(rand.NewSource(1).(rand.Source64))
for i := 0; i < 4; i++ {
println(gRand.Intn(100))
}
}

The performance will get better:

name                  time/op
RandWithMathNoLock-8 10.7ns ± 4%

A Journey With Go

A Journey With Go Language Programming

Vincent Blanchon

Written by

French Gopher in Dubai

A Journey With Go

A Journey With Go Language Programming

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade