AlgoMaster Logo

math/rand & crypto/rand

Low Priority20 min readUpdated June 6, 2026

Go ships two packages for generating random values, and the choice between them is one of the most consequential decisions in the standard library. math/rand is fast and deterministic, designed for simulations, sampling, shuffling, and anywhere repeatability or speed matters more than secrecy. crypto/rand is slower and pulls from the operating system's entropy pool, designed for anything an attacker might try to guess. Mixing them up is a security bug. This chapter walks through both packages, the seeding history, and the decision rule that keeps you out of trouble.

Two Random Number Generators, Two Different Jobs

There are two kinds of randomness software needs, and they have almost nothing to do with each other.

The first kind is statistical randomness. You want numbers that look random in aggregate so that simulations, A/B tests, shuffles, and Monte Carlo methods give unbiased results. You also often want the numbers to be reproducible, so a failing test or a flaky simulation can be replayed. Speed matters a lot because you may generate millions of values per second. Predictability is fine because nobody is attacking the output.

The second kind is unpredictable randomness. You want numbers that an attacker cannot guess, even if they see other numbers from the same generator, even if they know how the generator works, and even if they can time your code. Speed matters less because you only generate a few values per request. Predictability is unacceptable because the attacker is the whole point.

math/rand does the first job. crypto/rand does the second. Confusing them is the bug.

If an attacker guessing the value causes a problem (account takeover, leaked data, broken authentication), use crypto/rand. Otherwise, math/rand is faster and more flexible.

math/rand v1: The Original Pseudorandom Generator

math/rand is a deterministic pseudorandom number generator (PRNG). Given a starting state called the seed, it produces a sequence of numbers that look random but are completely determined by the seed. Same seed, same sequence, every time. That property is a feature, not a bug, because it's what makes tests reproducible.

Here's the simplest possible use, picking the product of the day from a catalog. Don't worry about seeding yet; we'll get to that.

The output you see will depend on your Go version. On Go 1.20 and later, the global generator is auto-seeded with a random value at program start, so you'll get a different product each run. On Go 1.19 and earlier, the program would print "Wireless Headphones" every single time because the unseeded default seed is 1, which is the historical quirk that bit a generation of Go developers.

The handful of functions you'll use most:

FunctionWhat it returns
rand.Intn(n)A non-negative int in [0, n)
rand.Int63n(n)A non-negative int64 in [0, n)
rand.Float64()A float64 in [0.0, 1.0)
rand.Shuffle(n, swap)Shuffles n elements using a swap function
rand.Perm(n)Returns a random permutation of [0, n)

A discount roll, the kind of thing you'd run when a customer checks out:

rand.Intn(21) returns a number in [0, 21), and adding 5 shifts it into [5, 26), which is the inclusive range [5, 25]. The off-by-one is easy to get wrong; remember Intn(n) is exclusive at the top.

Seeding history and the Go 1.20 auto-seed change

Before Go 1.20, math/rand was seeded with the value 1 unless you called rand.Seed yourself. That meant code like the product-of-the-day picker above would print the same product every run, which is rarely what anyone wants.

The historical workaround was to seed at program start, usually from the current time:

Then Go 1.20 changed the default: the global source is now seeded with a random value at program start, so a fresh program produces a fresh sequence without any work from you. rand.Seed still works, but it's deprecated in favor of constructing your own *rand.Rand when you need a controlled seed.

Why did this take so long? Because changing the default broke any test that relied on the deterministic sequence the old default produced. The Go team accepted the breakage in 1.20 because the cost of confusing newcomers was higher than the cost of fixing a few tests.

If you need a reproducible sequence (for tests, simulations, or anywhere repeatability matters), construct a Rand with an explicit seed:

Run this program a hundred times and you'll get the same three numbers every time, because rand.New(rand.NewSource(42)) builds a generator with a fixed starting state. This is the behavior you want when a test needs to assert "the shuffle produced this order" or when a simulation needs to be reproducible from a logged seed.

math/rand/v2: The Cleaner API (Go 1.22+)

Go 1.22 introduced math/rand/v2, a redesigned API that fixes the warts in the original package. New code should prefer it. The v1 package still exists for backwards compatibility, but the v2 package has cleaner naming, better default algorithms, and removes the deprecated functions.

What changed:

Concernmath/rand (v1)math/rand/v2
Function name for [0, n) intIntnIntN
Generic [0, n) helpernonerand.N[T] for any integer type
Default generator algorithmLagged FibonacciPCG (better statistical properties)
Seed functionexists, deprecatedremoved entirely
Auto-seeded globalsince Go 1.20from day one

Here's the v2 version of the same product-of-the-day picker:

The only visible change is IntN (capital N) instead of Intn. The capital is a deliberate signal that this is the v2 API. The behavior is the same, but the global source is always auto-seeded and the underlying algorithm is PCG, which has better statistical properties than the older Lagged Fibonacci generator.

The new rand.N function is the v2 highlight. It's a generic helper that returns a random value in [0, n) for any integer type:

The first call works directly with time.Duration (which is an int64 internally). The second uses uint32 and shows the type-parameter syntax. In v1 you'd have to write time.Duration(rand.Int63n(int64(500 * time.Millisecond))), which is ugly enough that people skipped jitter altogether.

For reproducible sequences in v2, use rand.New(rand.NewPCG(seed1, seed2)):

PCG (Permuted Congruential Generator) takes two 64-bit seeds. Same seeds, same sequence, every time. The algorithm is faster than the v1 default and has stronger statistical guarantees, which is why the Go team made it the v2 default.

Goroutine Safety and Local Generators

The top-level functions in both math/rand and math/rand/v2 are safe for concurrent use, but they pay for that safety with a sync.Mutex on the global generator. For most code, that mutex is invisible. For code that calls rand.Intn from thousands of goroutines per second, the mutex becomes a real bottleneck.

The fix is to give each goroutine its own *rand.Rand. A *rand.Rand instance is not safe for concurrent use, which is exactly what you want: no internal locking, so calls are as fast as possible, with the constraint that you don't share the instance between goroutines.

Each worker generates its own sequence with no contention on a shared lock. The trade is that you have to thread the *rand.Rand through your code instead of just calling rand.IntN from anywhere.

For most applications, the global generator is fine. The mutex contention only becomes visible at very high call rates, and pre-optimizing for it is a waste. Use local generators when profiling shows the mutex is hot, or when you're writing a library that may be called in tight loops.

Shuffling and Permutations

Shuffling the order of recommended products on a homepage so different users see different orderings is a common use case. rand.Shuffle handles this directly.

Shuffle uses the Fisher-Yates algorithm, which produces a uniformly random permutation in O(n) time. The callback receives two indices and is expected to swap whatever data structure you're shuffling. That callback-based API is what makes it work for any sliceable type without generics.

If you just want a random permutation of integers (useful for sampling without replacement), rand.Perm(n) returns a slice with the values 0 through n-1 in random order:

Slicing the result gives you a random sample without duplicates. For larger catalogs this is wasteful because you generate the full permutation even when you only want a few, but for samples up to a few thousand items it's idiomatic and fast enough.

crypto/rand: Unpredictable Randomness

crypto/rand is a completely different package with a completely different job. It reads from the operating system's entropy source (/dev/urandom on Linux and macOS, BCryptGenRandom on Windows), which means the output is unpredictable even to an attacker who can see other output from the same process.

The core function is rand.Read, which fills a byte slice with random bytes:

The call returns an error. crypto/rand.Read almost never fails on modern systems because the OS entropy pool is always available, but check the error regardless. If it does fail, something is deeply wrong with the system, and continuing with non-random bytes would be worse than crashing. Raw bytes are rarely the output format you want; tokens and IDs are usually hex or base64 strings.

Unlike math/rand, crypto/rand.Read is safe for concurrent use without any extra locking. The package's Reader is backed by the OS, which handles concurrency internally.

Encoding crypto/rand Output: Hex and Base64

Raw random bytes aren't useful as-is in URLs, headers, or database columns. You need to encode them into a string. The standard choices are hex and base64.

For an order confirmation code that goes into an email and a URL:

8 random bytes encode to 16 hex characters, which is short enough for an email but long enough that guessing one is computationally infeasible (2^64 possibilities). Hex is the most readable encoding; every byte becomes exactly two characters, all in the alphabet 0-9a-f.

For a password reset token that goes into a URL, base64 is more compact:

32 random bytes (256 bits) is the standard size for a strong token. RawURLEncoding is base64 with URL-safe characters (- and _ instead of + and /) and no = padding, which makes it safe to drop straight into a URL without further encoding.

EncodingBytes per charURL-safe?Padding?Use for
hex2 hex chars per byteyesn/ashort codes, anything case-insensitive
base64.StdEncoding~1.33 chars per byteno (+, /)yes (=)binary in JSON, opaque blobs
base64.URLEncoding~1.33 chars per byteyes (-, _)yes (=)URL-safe with padding
base64.RawURLEncoding~1.33 chars per byteyes (-, _)noURL-safe tokens, JWT components

For most session IDs and tokens, base64.RawURLEncoding of 32 bytes is the right default. It's compact, URL-safe, and gives you 256 bits of entropy.

Session ID generation in a typical web app:

256 bits of entropy from the OS, encoded into a 43-character URL-safe string. Storing this server-side as the session key is the standard pattern for cookie-based authentication.

crypto/rand.Int for Uniform Random Numbers

When you need a uniformly random integer (not just random bytes), crypto/rand.Int is the function. It takes a *big.Int upper bound and returns a value in [0, max):

This is the right way to generate a cryptographically random verification code. The naive approach of taking 4 random bytes and computing int(b) % 1_000_000 introduces a small but real bias because 2^32 % 1_000_000 is not zero, which means some values are slightly more likely than others. crypto/rand.Int uses rejection sampling internally to give a uniform distribution.

The *big.Int return type is awkward for small numbers, but n.Int64() extracts the value as an int64 when you need it. The big.Int exists because crypto/rand.Int can generate values up to arbitrary sizes, which is needed for RSA key generation and similar cryptographic primitives.

The Security Bug: Using math/rand for Tokens

Here is the dangerous pattern:

What's wrong with this code?

It looks fine. The token is 32 characters long, randomly chosen from a 36-character alphabet, which is 36^32 possible tokens. That's an enormous number, and brute-forcing one looks infeasible.

The bug is that math/rand is a PRNG with a small state. The default generator in v1 has 64 bits of internal state, the v2 PCG has 128 bits. An attacker who can observe a handful of tokens generated by your service can recover the internal state of the generator and predict every future token. There's published research and open-source tooling for this exact attack. The token length and alphabet size are irrelevant because the underlying entropy is the generator's state, not the output space.

Fix:

crypto/rand.Read produces bytes that come from the OS entropy pool, which an attacker cannot predict from observing other outputs. The token is now genuinely 256 bits of entropy.

The same bug applies to session IDs, API keys, CSRF tokens, salts, IVs, and anything else that must be unguessable. If an attacker guessing the value causes a security problem, use crypto/rand. There is no scenario where math/rand is the right choice for a security-sensitive value.

A/B Test Bucketing: A Brief Aside

Bucketing users for A/B tests is a case where math/rand would technically work, but a better pattern exists. The problem is that the same user might land in different buckets across sessions, which makes test results noisy.

The idiomatic pattern is stable hashing: hash the user ID, take the result modulo the bucket count, and you get a stable bucket assignment that doesn't change between sessions.

Same user, same bucket, every time. This isn't really random in the usual sense; it's a deterministic distribution that looks random across users. For A/B testing, that property matters more than statistical randomness, so reach for hashing instead of math/rand. When you genuinely want a random per-event assignment (logging sampling, for example), math/rand is fine.

The Decision Table

When choosing between the two packages, this table summarizes the decision.

Use casePackageWhy
Daily product of the daymath/rand/v2No attacker advantage in guessing
Random discount rollmath/rand/v2Not security-sensitive
Shuffle product recommendationsmath/rand/v2Speed and ease of use
Generate fake data for testsmath/rand/v2Reproducibility (seed it)
Simulation jittermath/rand/v2Speed; rand.N(time.Duration) is clean
Game outcome / dice rollmath/rand/v2Unless it's gambling, then crypto/rand
Order confirmation codecrypto/randGuessable codes enable order takeover
Session IDcrypto/randGuessable session IDs = account takeover
Password reset tokencrypto/randThe classic security bug
API keycrypto/randUnpredictable, high entropy
Salt for password hashingcrypto/randPredictable salts defeat hashing
IV for AEScrypto/randPredictable IVs break the cipher
CSRF tokencrypto/randPredictable tokens allow forgery
A/B test bucket assignmenthash of user IDStability across sessions matters more than randomness

The middle column is the entire decision rule, condensed.

The diagram puts numbers on the speed gap. math/rand/v2 is roughly an order of magnitude faster than crypto/rand, which matters in tight loops but is invisible for token generation, where you generate a handful of values per request.

Quiz

math/rand Quiz

10 quizzes