How to Generate Random Numbers and Bytes in Go (crypto/rand)

Generate secure random bytes and integers in Go using the crypto/rand package for cryptographic safety.

You are generating a session token for a user login

You reach for math/rand because you have used it in Python or JavaScript before. You get a number. It looks random. The problem is that the number is predictable. If an attacker knows the seed or the time you started the program, they can reproduce the sequence. Your session token is compromised. This is why Go separates random number generation into two packages. math/rand is for games and simulations. crypto/rand is for security.

Secure randomness versus predictable noise

Think of math/rand as a magic 8-ball. You shake it, it gives you an answer. If you could record the exact shake, you could predict the answer. It is deterministic. crypto/rand is different. It reads from the operating system's entropy pool. This pool gathers noise from hardware events: mouse movements, disk timing, network packet arrival times. It is like listening to the static between radio stations. The static is chaotic and impossible to reproduce. crypto/rand taps into that chaos.

The OS maintains an entropy pool that mixes these hardware events into a stream of unpredictable bytes. On Linux, this is backed by /dev/urandom. On Windows, it uses CryptGenRandom. Go abstracts these details behind a single API. When you call crypto/rand, you are making a system call to read from this pool. The result is cryptographically secure, meaning no efficient algorithm can distinguish the output from true randomness.

crypto/rand is your shield against predictability.

Minimal example

Here is the simplest way to get random bytes and a random integer using the secure package.

package main

import (
	"crypto/rand"
	"fmt"
	"log"
	"math/big"
)

func main() {
	// Allocate a slice to hold 16 bytes of random data
	b := make([]byte, 16)
	// Read fills the slice with random bytes from the OS entropy source
	_, err := rand.Read(b)
	// Always check the error; Read can fail if the OS runs out of entropy
	if err != nil {
		log.Fatalf("failed to read random bytes: %v", err)
	}
	// Print the bytes as hex for readability
	fmt.Printf("Random bytes: %x\n", b)

	// Create a big.Int representing the upper bound (exclusive)
	limit := big.NewInt(101)
	// Int returns a random number in the range [0, limit)
	n, err := rand.Int(rand.Reader, limit)
	if err != nil {
		log.Fatalf("failed to generate random int: %v", err)
	}
	fmt.Printf("Random int: %d\n", n)
}

Walkthrough

rand.Read writes random bytes into a slice. It returns the number of bytes written and an error. The error is rare on modern systems but possible if the entropy pool is exhausted. The Read method follows the io.Reader interface contract. It fills the buffer and returns the count. If the count is less than the buffer length, it returns an error. This means you never get a partial fill without an error. You can rely on the slice being fully populated if err is nil.

rand.Int generates a random integer. It takes an io.Reader and a *big.Int limit. The limit is exclusive. If you pass 101, you get 0 to 100. rand.Reader is a global variable that implements io.Reader. It is the bridge to the OS entropy source. You pass rand.Reader to rand.Int so the function can draw bytes as needed.

The community accepts the verbose if err != nil pattern because it makes the unhappy path visible. If the entropy source fails, your security is broken. You need to know. Do not suppress the error with _.

Why big.Int and arbitrary precision

Go's standard integer types are fixed size. int is 32 or 64 bits depending on the architecture. Cryptographic keys often require 2048 bits or more. big.Int represents integers of arbitrary precision. It stores the number as a slice of words. This allows crypto/rand to generate numbers of any size without overflow.

When you call rand.Int, you pass the limit as a big.Int. The function generates a random number with the same bit length as the limit, then reduces it modulo the limit using rejection sampling. This ensures the result is uniformly distributed. If you try to pass a plain integer, the compiler rejects the program with cannot use 100 (untyped int constant) as *big.Int value in argument. You must wrap the limit in big.NewInt.

big.Int is the standard way to handle large numbers in Go. It is used throughout the crypto package for RSA, ECC, and other algorithms. Learning to use big.Int pays off when you work with cryptographic protocols.

Realistic example: secure token

In a real application, you rarely print hex bytes. You usually need a URL-safe token for a password reset link or a session ID. Here is how to generate a secure token and encode it for use in a URL.

package main

import (
	"crypto/rand"
	"encoding/base64"
	"fmt"
	"log"
)

// GenerateToken creates a URL-safe random token of the specified byte length
func GenerateToken(length int) string {
	b := make([]byte, length) // Allocate buffer for random bytes
	_, err := rand.Read(b)    // Fill buffer with secure random data
	if err != nil {
		// Fail hard if the OS cannot provide randomness
		log.Fatalf("entropy error: %v", err)
	}
	return base64.URLEncoding.EncodeToString(b) // Encode for URL safety
}

func main() {
	fmt.Println(GenerateToken(32)) // Print a 256-bit token
}

Realistic example: random string

Developers often need random strings for passwords or codes. You can build one by picking random characters from a charset. Here is how to do it without bias.

package main

import (
	"crypto/rand"
	"fmt"
	"log"
	"math/big"
)

// RandomString generates a random alphanumeric string of the given length
func RandomString(length int) string {
	const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
	b := make([]byte, length)
	for i := range b {
		// Generate a random index within the charset length
		idx, err := rand.Int(rand.Reader, big.NewInt(int64(len(charset))))
		if err != nil {
			log.Fatalf("random error: %v", err)
		}
		b[i] = charset[idx.Int64()] // Pick the character at the random index
	}
	return string(b)
}

func main() {
	fmt.Println(RandomString(16)) // Print a random 16-character string
}

Pitfalls and errors

Modulo bias is a subtle trap. If you generate a random byte and take the remainder modulo 10, the numbers 0 through 5 appear slightly more often than 6 through 9. This happens because 256 is not evenly divisible by 10. In a game, this does not matter. In a cryptographic protocol, this leak can be exploited. crypto/rand.Int implements rejection sampling to eliminate this bias. It discards values that would skew the distribution and draws again until the result is perfectly uniform. Never use byte % n for security-sensitive ranges.

Using math/rand for security is the biggest pitfall. math/rand is deterministic. If you seed it with time.Now().UnixNano(), an attacker can guess the seed within a small window. Go 1.22 introduced math/rand/v2. It replaces the old math/rand with a faster, cleaner API. It uses a PCG64 generator. It is still not secure. It is for performance and convenience. The split between secure and non-secure remains.

If you pass a nil slice to rand.Read, you get a panic. The runtime stops with panic: runtime error: invalid memory address or nil pointer dereference. Always allocate the slice before calling Read.

Never use math/rand for secrets. The compiler will not stop you, but an attacker will exploit it.

Performance trade-offs

crypto/rand involves a system call to read from the OS entropy pool. It is slower than math/rand, which generates numbers in memory. If you need a million random numbers for a simulation, crypto/rand will bottleneck. math/rand is the right tool for high-throughput, non-secure workloads.

For most applications, the performance difference is negligible. Generating a session token or a nonce happens once per request. The cost is tiny compared to network I/O or database queries. Only profile your code before optimizing randomness. Premature optimization can lead to security bugs.

Trust the entropy pool. It is designed to be fast enough for security use cases.

Decision matrix

Use crypto/rand when you are generating tokens, nonces, salts, or keys for any security-sensitive operation. Use crypto/rand.Read when you need a raw byte slice, such as for a session ID or a random nonce. Use crypto/rand.Int when you need a random integer within a specific range, especially for cryptographic protocols requiring large numbers. Use math/rand when you are writing a game, a simulation, or a shuffle algorithm where predictability does not matter. Use math/rand/v2 when you need fast, non-secure random numbers in modern Go code; it is faster and has a better API than the old math/rand. Use a deterministic seed with math/rand when you need reproducible results for testing or debugging.

Security is not a feature you add later. Pick the right random source from the start.

Where to go next