Learning Golang: Random numbers

Share on:

This is part 23 of my journey learning Golang.

Random number generators

For some kinds of programs, like simulations, games, or test code, it is useful to be able to generate unpredictable sequences of numbers.

While generating truly random numbers is a hard problem (see RANDOM.ORG), a common alternative is to use a pseudorandom number generator.

Go has a package for generating pseudo-random numbers: rand.

Random integer

The Int() function from the rand package returns a non-negative pseudo-random int.

For example:

 1package main
 2
 3import (
 4  "fmt"
 5  "math/rand"
 6  "time"
 7)
 8
 9func main() {
10  rand.Seed(time.Now().UnixNano())
11  for i := 0; i < 3; i++ {
12    r := rand.Int()
13    fmt.Println(r)
14  }
15}

Source: learning-go/random/random-integer.go

When executed it produces output like this:

1❯ bin/go run ./random/random-integer.go
27844980600027973356
33488658292559818292
43150213459654129095
5
6❯ bin/go run ./random/random-integer.go
78417580983999923480
87072937072426873398
98910705581888374081

The Int() function is being called 3 times, and each time it produces a different value.

Notice that the Seed() function is being used to initialize the pseudo-random generator with an integer value corresponding to the current time. That makes the generator produce a different sequence each time the program is executed. Without that, it would always use the same seed value, and produce the same numbers.

Keep in mind that the pseudo-random number generator is deterministic, not truly random. That’s why if we want different results, we must configure it to start from different starting points.

Cryptographically strong random seed

Using the current time as random seed, like the program above does, is not good enough if the random numbers that will be generated need to be hard to predict or tamper with. See this Stack Overflow answer for a brief explanation of the problem.

The crypto/rand package implements a cryptographically secure random number generator. It is based on secure random number generators provided by the underlying operating system. Read about entropy to learn more.

For instance take a look at this program:

 1package main
 2
 3import (
 4  "encoding/binary"
 5  "fmt"
 6  crypto_rand "crypto/rand"
 7  math_rand "math/rand"
 8)
 9
10func main() {
11  seedRandomizer()
12  for i := 0; i < 3; i++ {
13    r := math_rand.Int()
14    fmt.Println(r)
15  }
16}
17
18// Based on John Leidegren's code at https://stackoverflow.com/a/54491783/376366
19func seedRandomizer() {
20    var b [8]byte
21    _, err := crypto_rand.Read(b[:])
22    if err != nil {
23        panic("cannot seed math/rand package with cryptographically secure random number generator")
24    }
25    math_rand.Seed(int64(binary.LittleEndian.Uint64(b[:])))
26}

Source: learning-go/random/random-integer-secure.go

The seedRandomizer function gets 8 strongly random bytes using crypto/rand's Read function and uses that to seed math/rand's random number generator. That makes it extremely difficult to predict or manipulate the numbers that will be generated, with no impact on the generator’s performace.

Random integer on a range

The Intn function generates a random integer greater than or equal to 0 and less than an upper limit. To generate integers in any given range, just shift the range by adding a padding to it.

For instance:

 1package main
 2
 3import (
 4  "encoding/binary"
 5  "fmt"
 6  crypto_rand "crypto/rand"
 7  math_rand "math/rand"
 8)
 9
10func main() {
11  seedRandomizer()
12  for i := 0; i < 30; i++ {
13    fmt.Printf("%v ", randomInt(1, 10))
14  }
15  fmt.Printf("\n")
16}
17
18func randomInt(min, max int) int {
19  return min + math_rand.Intn(max - min + 1)
20}
21
22// Based on John Leidegren's code at https://stackoverflow.com/a/54491783/376366
23func seedRandomizer() {
24    var b [8]byte
25    _, err := crypto_rand.Read(b[:])
26    if err != nil {
27        panic("cannot seed math/rand package with cryptographically secure random number generator")
28    }
29    math_rand.Seed(int64(binary.LittleEndian.Uint64(b[:])))
30}

Source: learning-go/random/range.go

Notice how the randomInt function adds min to offset the lower limit of Intn (which is 0) and how it sets the upper limit to the number of possible values.

Sample output:

11 9 2 7 10 1 4 9 1 4 5 7 5 4 5 9 8 2 1 9 9 8 5 10 3 8 4 10 5 6

Random string

An easy way to generate a string of random characters is to generate random numbers that correspond to the desired characters in the ASCII chart.

For example this program will print strings composed of random lowercase letters:

 1package main
 2
 3import (
 4  "encoding/binary"
 5  "fmt"
 6  crypto_rand "crypto/rand"
 7  math_rand "math/rand"
 8)
 9
10func main() {
11  seedRandomizer()
12  for i := 0; i < 3; i++ {
13    fmt.Println(randomString(20))
14  }
15}
16
17func randomInt(min, max int) int {
18  return min + math_rand.Intn(max - min + 1)
19}
20
21// Based on Flavio Copes's code at https://flaviocopes.com/go-random/
22func randomString(length int) string {
23  bytes := make([]byte, length)
24  for i := 0; i < length; i++ {
25    bytes[i] = byte(randomInt(97, 122))
26  }
27  return string(bytes)
28}
29
30// Based on John Leidegren's code at https://stackoverflow.com/a/54491783/376366
31func seedRandomizer() {
32  var b [8]byte
33  _, err := crypto_rand.Read(b[:])
34  if err != nil {
35    panic("cannot seed math/rand package with cryptographically secure random number generator")
36  }
37  math_rand.Seed(int64(binary.LittleEndian.Uint64(b[:])))
38}

Source: learning-go/random/strings.go

Sample output:

1awmckozkhqkullghvlga
2mhevzjivybulgfcocbnd
3sekxlbauktzloghkfecz

Random floating point numbers

The Float64 function will return pseudo-random floating point numbers greater than or equal to 0 and less than 1.

For example:

 1package main
 2
 3import (
 4  "encoding/binary"
 5  "fmt"
 6  crypto_rand "crypto/rand"
 7  math_rand "math/rand"
 8)
 9
10func main() {
11  seedRandomizer()
12  for i := 0; i < 3; i++ {
13    r := math_rand.Float64()
14    fmt.Println(r)
15  }
16}
17
18// Based on John Leidegren's code at https://stackoverflow.com/a/54491783/376366
19func seedRandomizer() {
20  var b [8]byte
21  _, err := crypto_rand.Read(b[:])
22  if err != nil {
23    panic("cannot seed math/rand package with cryptographically secure random number generator")
24  }
25  math_rand.Seed(int64(binary.LittleEndian.Uint64(b[:])))
26}

Sample output:

10.5797031300913683
20.1786276285370312
30.3732865775928763

See the rand package documentation for other variations like:

  • Float32 returns a pseudo-random number as a float32.
  • ExpFloat64 returns an exponentially distributed float64.
  • NormFloat64 returns a normally distributed float64.

Takeaways

  • The math/rand package has functions for generating random integer and floating-point numbers and doing related operations like permutations and shuffling.
  • The crypto/rand package implements a cryptographically secure random number generator. It can be used for security-sensitive applications and it is handy for seeding a math/rand generator. See this article for a discussion of the trade-offs between the math and crypto rand packages.
  • Random numbers can easily be used to generate random values of more complex types.

comments powered by Disqus