Generate Random Numbers in Go Correctly With math rand v2
Use math/rand/v2 for dice, shuffles and simulations, seed a generator for reproducible tests, and use crypto/rand for tokens. See the API and the old traps.

Random numbers in Go have a history that makes old answers misleading. Before Go 1.20, a program that called rand.Intn without seeding printed the same "random" numbers on every run. Go 1.20 started seeding the global generator randomly, and Go 1.22 introduced math/rand/v2, a cleaned-up API, with the old package left in place and its Seed function deprecated. If you search for how to generate a random number you will find advice from all three eras. This how-to uses the current package, shows its output, explains reproducible generators for tests, and draws the line with crypto/rand. It is part of the Go basics path and follows sorting in using the newer standard library helpers.
Tested with Go 1.25.6, macOS on Apple Silicon (arm64)
The everyday functions
math/rand/v2 has top-level functions that use a global generator. They are safe to call from many goroutines and need no setup:
package main
import (
"fmt"
"math/rand/v2"
"time"
)
func main() {
roll := rand.IntN(6) + 1
wait := rand.N(10 * time.Second)
coin := rand.IntN(2) == 0
ratio := rand.Float64()
fmt.Println(roll >= 1 && roll <= 6)
fmt.Println(wait >= 0 && wait < 10*time.Second)
fmt.Println(coin || !coin, ratio >= 0 && ratio < 1)
words := []string{"a", "b", "c", "d", "e"}
rand.Shuffle(len(words), func(i, j int) { words[i], words[j] = words[j], words[i] })
fmt.Println(len(words), len(rand.Perm(5)))
}true
true
true true
5 5The output is all true and counts because the values change on every run, so the program checks the ranges instead of printing the numbers. IntN(6) returns 0 to 5, hence the + 1 for a die. rand.N is the generic form that works with any integer type: given a time.Duration limit it returns a Duration below it, which is exactly what a jittered sleep needs, as in retry with backoff and jitter. Float64 returns [0, 1). Shuffle reorders a slice in place, given its length and a swap function, and Perm(n) returns a random permutation of 0 to n-1, useful for visiting items in random order without touching the slice.
The names changed from the old package: Intn became IntN, Int31n became Int32N, and the generic N is new. The trap in both is the argument: a limit of zero or less panics.
package main
import (
"fmt"
"math/rand/v2"
)
func main() {
fmt.Println(rand.IntN(0))
}panic: invalid argument to IntN
...The panic is invalid argument to IntN. It usually arises from rand.IntN(len(xs)) on an empty slice, so guard the length before picking a random element, or use slices helpers that handle emptiness.
Reproducible sequences for tests and simulations
Sometimes you want randomness that is the same every time: a test that must not flake, a simulation whose result you want to reproduce, a fixture generator. Build your own generator with a seed. Two sources are provided: rand.NewPCG(seed1, seed2) is small and fast, and rand.NewChaCha8(seed) takes a 32 byte seed and is the cryptographically derived one:
package main
import (
"fmt"
"math/rand/v2"
)
func main() {
r := rand.New(rand.NewPCG(1, 2))
fmt.Println(r.IntN(100), r.IntN(100), r.IntN(100), r.Float64())
again := rand.New(rand.NewPCG(1, 2))
fmt.Println(again.IntN(100), again.IntN(100), again.IntN(100))
var seed [32]byte
copy(seed[:], "chacha8 seed for this example!!!")
c := rand.New(rand.NewChaCha8(seed))
fmt.Println(c.IntN(1000), c.IntN(1000), c.Uint64()%10)
counts := [6]int{}
dice := rand.New(rand.NewPCG(7, 7))
for range 60000 {
counts[dice.IntN(6)]++
}
fmt.Println(counts)
}76 61 78 0.4297927436037299
76 61 78
257 779 3
[10036 10172 9834 9951 10018 9989]Two generators with the same PCG seed produced the same numbers, 76 61 78, which is the whole point. The last block rolls a die sixty thousand times and counts the faces: each came out near 10,000, from 9,834 to 10,172, which is the uniform spread you expect. A seeded generator makes that statistical check reproducible too.
A generator made with rand.New is not safe for use by several goroutines at once, and the top-level functions are. Give each goroutine its own generator, or guard one with a mutex. A seeded generator is fine for tests because it repeats within one Go release. I would not hard-code the exact numbers it produced here in a test that has to survive upgrades, since the way methods such as IntN turn a stream into values is not frozen forever. Assert properties of the result, or regenerate the expected values when you upgrade. Do not seed a generator from the clock for tests, since that defeats reproducibility, and do not use the clock for production seeding either, because the default global generator is already seeded better than you will do by hand.
The old package, if you maintain old code
Code written for math/rand (version 1) is still valid. The things to know: since Go 1.20 the global generator is seeded randomly unless the GODEBUG setting randautoseed=0 is set, so a call to rand.Seed(time.Now().UnixNano()), which every tutorial from before 2023 includes, is no longer needed and is deprecated. Calling rand.Seed still works and makes the global generator deterministic, which is rarely what you want. If you are starting a new file, import math/rand/v2 and let the version 1 package fade. A mixed code base can use both under different names, "math/rand/v2" is imported as rand and mrand "math/rand" for the old one.
When math/rand is the wrong package
Anything an attacker must not predict needs crypto/rand: session identifiers, password reset tokens, API keys, nonces, salts, anything you would be upset to see guessed. math/rand generators are fast and statistically good and they are not secure, since their output reveals their state. The secure source reads from the operating system:
token := rand.Text() // crypto/rand, Go 1.24 and later: 26 base32 characters
buf := make([]byte, 16)
if _, err := rand.Read(buf); err != nil {
return err
}On my machine crypto/rand.Text() returned a string of 26 characters, and a 16 byte Read hex-encoded to 32. Import it under a different name when you also use math/rand/v2, such as crand "crypto/rand". Choose the length from the strength you need: 16 random bytes, 128 bits, is plenty for an unguessable token. For a UUID, the UUID generator shows the format, and version 4 UUIDs are made from crypto/rand bytes. Picking a random number in a range securely is crypto/rand.Int(rand.Reader, big.NewInt(n)), which is clumsier and correct, and avoids the modulo bias you would introduce by taking value % n of random bytes.
Common mistakes
Taking rand.Int() % n instead of rand.IntN(n) skews the distribution slightly when n does not divide the range, and the IntN function removes the bias. Seeding in a loop, with a new generator each iteration from the same seed, gives the same number every time. Sharing one rand.New generator across goroutines without a lock is a data race. Using a random choice in a test without a seed makes it flaky in exactly the way that nobody can reproduce. And using math/rand for security is the mistake with consequences. The package documentation covers the sources and their guarantees in math/rand/v2, and the Go blog's evolving the Go standard library with math/rand/v2 explains why a second version was needed.


