Introduction
In the 27th post of the series, we will be looking into random number generation in golang. We will be exploring how to create a random number, generate random numbers within a range, shuffle slices/arrays, and generate random strings, float, and bytes.
There are two types of random number generation processes in software pseudo-random numbers and cryptographically secure pseudo-random number generation.
The math/rand package in Golang provides a number of functions for generating pseudorandom numbers. These functions are suitable for a variety of applications, such as games, simulations, and to some extent in cryptography.
The crypto/rand package in Golang generates cryptographically secure pseudorandom numbers. This means that the numbers are generated in a way that is very difficult to predict or reproduce. However, they are not truly random, as they are generated by a computer algorithm.
Creating a Random Source
We need to first create a source/seed to generate a random number. If we do not add a new source each time we run the program or generate a random number, it will pick up the same source leading to the generation of the same pattern of the random numbers.
We use the rand.NewSource method to generate a new source of the random number, by initializing it with the current time in nanoseconds. Further, we need to create a Rand object for accessing the methods and properties attached to the struct type in the rand package for generating random numbers.
package main
import (
"fmt"
"math/rand"
"time"
)
func main() {
source := rand.NewSource(time.Now().UnixNano())
rand_source := rand.New(source)
for i := 0; i < 5; i++ {
rand_num := rand_source.Int()
fmt.Println(rand_num)
}
}
$ go run rand_source.go
2651653079934875120
5510445616427469234
3817011159463415912
5708754224362255659
7511308401304127761
In the above example, we use the NewSource
and the New
method to generate the Source
and the Rand
object respectively. Further, for demonstration, we use the Intmethod to generate a 64-bit random integer 5 times with a for loop.
As we can see it generates a 5 random number, we will see why we need the random source initialized to the current time in the upcoming section.
Generating Random Numbers
Random number as the name suggest are used to get an unpredictable number, however, using software we can only mimic the actual randomness. The process is called pseudo-random number generation. There is a particular pattern in the numbers, however, it is sufficient for trivial tasks in games, and simulations to some extent. However, actual cryptographic random numbers should be used for security tasks, crypto arithmetic, and other sensitive tasks.
Golang provides a built-in package for both generating pseudo-random numbers called math/rand and cryptographic numbers with crypto/rand packages. This package contains a number of functions for generating random integers, floats, and strings.
Random Numbers
To simply generate a random number we can use the rand.Int
method from the match/rand package to get a single random integer.
package main
import (
"fmt"
"math/rand"
)
func main() {
fmt.Println(rand.Int())
num := rand.Int()
fmt.Printf("%d", num)
}
$ go run main.go
5577006791947779410
int 8674665223082153551
$ go run main.go
5577006791947779410
int 8674665223082153551
In the above code, we have generated a couple of random numbers with the rand.Int
method. The method returns a random 64-bit integer. If you run the program a few times, you can see the numbers are the same, so how exactly are they random?
They are not random yet, we need to create a new seed/source each time we run the program in order to generate a new pattern of digits each time to generate a pseudo-random number.
package main
import (
"fmt"
"math/rand"
"time"
)
func main() {
r := rand.New(rand.NewSoure(time.Now().UnixNano()))
fmt.Println(r.Int())
num := r.Int()
fmt.Printf("%d", num)
}
$ go run main.go
5577006791947779410
int 8674665223082153551
7524437893560534176
int 5023070266853767708
$ go run main.go
5577006791947779410
int 8674665223082153551
8935404877937414882
int 209789266380754935
No, we can see that after the rand.New(rand.NewSource(time.Now().UnixNano()))
function call, the number generated is random each time we run the program. This is because we initialize the source of the random number generator package to the current time in nanoseconds.
Random Numbers in a Range
The above numbers are too big, what if we want the random numbers to be in a specific range? This is quite a common thing to do, and hence there is a function like rand.Intn where we can specify the bound to which the function should generate the random numbers.
package main
import (
"fmt"
"math/rand"
"time"
)
func main() {
r := rand.New(rand.NewSoure(time.Now().UnixNano()))
for i := 0; i < 10; i++ {
// generate an integer between 0 and 5
dice_throw := r.Intn(6)
// Move the Offset of 0
fmt.Println(dice_throw + 1)
}
}
$ go run main.go
1
2
5
6
6
3
6
1
4
2
In the above code, we have used the r.Intn(upper_range int)
method to generate a random number between 0 and the provided range, so if we give a parameter to the method r.Intn(6)
, it would generate the numbers between 0 and 5. so the range is not inclusive 0, 6). Thereby the numbers generated will be either 0, 1, 2, 3, 4, or 5. So to remove the offset of 0, we add 1.
Hence we get some pseudo-random numbers between 1 and 6. I have used a for loop that generates 10 such numbers.
Cryptographic Random Numbers
The above method was a pseudo-random number generator, however, for more robust random number generations, we can use the crypto/rand package that is more secure and powerful for complex operations.
package main
import (
crypto_rand "crypto/rand"
"fmt"
"math/big"
)
func Handle_error(err error) {
if err != nil {
panic(err)
}
}
func main() {
// Cryptographic random numbers
var max *big.Int = big.NewInt(6)
// big is a package for high-precision arithmetic
for i := 0; i < 10; i++ {
crypt_rand_num, err := crypto_rand.Int(crypto_rand.Reader, max)
Handle_error(err)
// Move the Offset of 0 by adding 1
crypt_num := crypt_rand_num.Add(crypt_rand_num, big.NewInt(1))
fmt.Println(crypt_num)
}
}
$ go run main.go
3
5
5
1
5
5
4
2
3
6
In the above example, we have used the math/big package to store the random number generated by the Int method in the crypto/rand
package. We create a new integer from the big
package. The NewInt function returns a pointer to the integer. So, we parse the integer 6 which will create a memory location storing 6 as a big.Int type. We use the max variable name as it denotes the maximum number to be generated in the next step.
Then we can use the crypto/rand package to generate cryptographic random numbers. The package has Int method to generate a big.Int
type of number in a given range. However, it also takes in a Reader object that is global in the package used as a shared instance of a cryptographically secure random number generator. This means it can be used as the platform's default random number generator in the program.
So, the crypto_rand.Int
method takes in two parameters, the Reader
object which will be the platform-specific random number generator/api, and the next parameter is the max range to generate the random number. So, this method returns a big.Int
type. This is the cryptographic random number.
However, we have the range from 0 to 5 again, so we just add 1 to the big.Int
type by using the Add method associated to the big.Int
type as crypto_rand_num
variable and then parse two parameters as x and y, i.e. the two numbers to add. So, we just pass the crypto_rand_num
and a new integer 1
. This adds the numbers and we store it in the crypto_num
variable. This is how we generate a cryptographic random number between 1 and 6.
Random Float
We can even generate random types like float. There are a quite a few variations like Float32, Float64, ExpFloat64, and NormFloat64.
package main
import (
"fmt"
"math/rand"
"time"
)
func main() {
r := rand.New(rand.NewSoure(time.Now().UnixNano()))
rand_float32 := r.Float32()
fmt.Println(rand_float32)
rand_float64 := r.Float64()
fmt.Println(rand_float64)
rand_exp_float := r.ExpFloat64()
fmt.Println(rand_exp_float)
rand_norm_float := r.NormFloat64()
fmt.Println(rand_norm_float)
for i := 0; i < 5; i++ {
rand_float := r.Float32()
fmt.Println(rand_float)
}
$ go run main.go
0.08891088
0.9218221078616824
1.8237338579299396
-0.30238778160373464
0.65474856
0.65964687
0.39930198
0.8043338
0.17894344
We have used 4 types for generating a random float. We have two variations of the float depending on the size, 32 or 64-bit number. We get 32-bit and 64-bit random floats from the r.Float32
and r.Float64
respectively.
The r.ExpFloat64
function returns an exponentially distributed float64 with a range from 0 to +math.MaxFloat64 with rate parameter and mean as 1. If you want to change the distribution's rate parameter, it can be done by dividing the number by the desired rate parameter.
The r.NormFlaot64
function returns a normally distributed float64 with a range from -math.MaxFloat64 to +math.MaxFloat64 with mean as 0 and standard deviation as 1. This can also be changed by multiplying the generated number by the desired standard deviation and then adding the desired mean.
Generating Random Strings
We can generate a random string of a specific length. We can generate a random number between 0 and 25 and then add 97 for lowercase ASCII characters and add 65 for uppercase characters. So, we generate a random number between 97 and 122 which can be cast to string/rune to get the string equivalent of the numbers.
This get's us a single character which would be random and thereby we use for loop to generate a fixed length random string for upper case and lower case characters similarly.
package main
import (
"fmt"
"math/rand"
"time"
)
func main() {
r := rand.New(rand.NewSoure(time.Now().UnixNano()))
//Random string
randomLowerCase := make([]rune, 6)
randomUpperCase := make([]rune, 6)
for i := range randomLowerCase {
randomLowerCase[i] = rune(r.Intn(26) + 97)
randomUpperCase[i] = rune(r.Intn(26) + 65)
}
randomLowerCaseStr := string(randomLowerCase)
randomUpperCaseStr := string(randomUpperCase)
fmt.Println(randomLowerCase)
fmt.Println(randomLowerCaseStr)
fmt.Println(randomUpperCase)
fmt.Println(randomUpperCaseStr)
}
$ go run main.go
[100 113 122 97 107 101]
dqzake
[86 81 88 76 66 74]
VQXLBJ
$ go run main.go
[116 115 120 97 100 111]
tsxado
[80 74 66 83 77 66]
PJBSMB
We first create an empty or 0 initialized rune slice with length 6, it can be any length as per your requirement. Then we create a for loop iterating over that slice and set each rune to the r.Intn
method with range 26 and add 97 for lower case letters and add 65 for upper case letters. This generates an integer between 97 to 122 that is typecast to a rune to represent the slice. Similarly, it generates an integer between 65 and 90 which is typecast to rune.
Shuffling Arrays
We can use the rand.Shuffle and the rand.Perm to shuffle and create a random permutation of a particular list/slice of elements.
package main
import (
"fmt"
"math/rand"
"time"
)
func main() {
r := rand.New(rand.NewSoure(time.Now().UnixNano()))
fmt.Println(r.Perm(10))
arr := []int{1, 2, 3, 4, 5, 6}
fmt.Println("Before shuffle:", arr)
r.Shuffle(len(arr), func(i, j int) {
arr[i], arr[j] = arr[j], arr[i]
})
fmt.Println("After shuffle:", arr)
}
$ go run main.go
[8 1 9 3 7 2 0 6 5 4]
Before shuffle: [1 2 3 4 5 6]
After shuffle: [4 6 5 3 2 1]
In the above example, we have used the rand.Perm
method to create a permutation of the n number passed as a parameter. So, it would generate a permutation of numbers from 0 to 9 if we pass 10 as the parameter to the method. It would return a slice of int.
We also have used the rand.Shuffle
method to shuffle an already existing slice of elements. This is not restricted to int, it can be any type of slice. The method takes in two parameters, the length of the slice/array and the swap function which is an anonymous function.
In the example, I have created an arr slice with 6 elements, it could be any number, for demonstration, I have initialized the slice with 1 to 6 numbers. The rand.Shuffle
method has been parsed with the length of the arr
as len(arr)
and an anonymous function that takes in two integers as the indices of the array and inside the function, we swap the elements of the array. This can be modified as per the requirement, but this is the base swap function for the shuffle method.
So, in the output, we print the shuffled array, which now looks like a random array of numbers.
We can use the Perm
method to generate a random list of indices of an array, and then assign the index to the string, to generate a random shuffled string.
package main
import (
"fmt"
"math/rand"
"time"
)
func main() {
r := rand.New(rand.NewSoure(time.Now().UnixNano()))
letters := "abcdefghijklmnopqrstuvwxyz"
shuffled := r.Perm(len(letters))
result := make([]byte, len(letters))
for i, randIndex := range shuffled {
result[i] = letters[randIndex]
}
rand_str := string(result)
fmt.Println(rand_str)
// random string of length 10
fmt.Println(rand_str[:10]
}
$ go run main.go
yeinvkdbfqomacrzhtswgxulpj
yeinvkdbfq
$ go run main.go
tvakbgnjprwiofquxlzecdshym
tvakbgnjpr
In the above example, we have first created the string with all the alphabets and then created a random permutation with the length of that array i.e. 26. This would create a random permutation of numbers from 0 to 25. This is now a list of numbers, which can be used to assign the index of the string, to make it feel like a shuffled string.
We create a for loop for the string iteration and assign the index with the random operation array index. This will basically jumble the order of the elements/characters in the string. We can then truncate or slice the string to any length as per the requirement.
Random Read Bytes
There is another way to generate a slice of bytes/string with the Read method. We have used the Read
method in the cryptographic random number generation part. The Read method generates a random byte for the given length of a slice of bytes.
package main
import (
crypto_rand "crypto/rand"
"fmt"
"math/big"
"math/rand"
"time"
)
func Handle_error(err error) {
if err != nil {
panic(err)
}
}
func main() {
r := rand.New(rand.NewSoure(time.Now().UnixNano()))
rand_byte := make([]byte, 5)
fmt.Println(rand_byte)
_, err = r.Read(rand_byte)
Handle_error(err)
fmt.Println(rand_byte)
fmt.Printf("%c \n", rand_byte)
crypt_rand_byte := make([]byte, 5)
fmt.Println(crypt_rand_byte)
_, err = crypto_rand.Read(crypt_rand_byte)
Handle_error(err)
fmt.Println(crypt_rand_byte)
fmt.Printf("%c \n", crypto_rand_byte)
}
$ go run main.go
[0 0 0 0 0]
[88 53 113 116 251]
[X 5 q t รป]
[0 0 0 0 0]
[37 90 42 93 96]
[% Z * ] `]
We have demonstrated usage of both the packages i.e. math/rand and crypto/rand for the generation of random bytes. In the example above, we initialize a slice of byte rand_byte
and use the Read
method that will take in the slice of byte as the parameter and return two things, the number of bytes it read and the error object if there is any or nil. So, we do not care how many bytes it read right now, so we do the _
for the read bytes. It mutates/modifies the byte slice and the slice elements are then random byte values.
We can print the slice of byte as a string with %s
, or each character in the bytes using the %c
format specifier. So, the generated bytes are between 0 and 255, which include Unicode and ASCII characters.
Similarly, for the crypto/rand package, we create a slice of bytes with size 5 and use the crypto/rand package provided Read method directly to modify the slice of bytes to random bytes.
That's it from the 27th part of the series, all the source code for the examples are linked in the GitHub on the 100 days of Golang repository.
Conclusion
From this part of the series, we were able to explore the random number generation in golang, packages like math/rand, mah/big, and crypto/rand were used for the examples and generation of random numbers, cryptographic secure random numbers, random strings and shuffling of arrays.
So, hopefully, the article might have found useful to you, if you have any queries, questions, feedback, or mistakes in the article, you can let me know in the discussion or on my social handles. Happy Coding :)
Top comments (1)
GENERA holds a distinguished status as a frontrunner in the national market, with an impressive legacy of 35 years dedicated finiquitos de trabajo chile to pioneering advanced technologies tailored for efficient people management. This extensive experience positions the company as a trusted authority in the realm of workforce optimization.