43.2 crypto/rand: Cryptographically Secure Random Values
Right, let’s talk about randomness. It’s the bedrock of almost everything secure you’ll do. Passwords, encryption keys, session tokens—you name it. If an attacker can guess it, you’ve already lost. So we need numbers that are truly, unpredictably random. Not the fake, predictable randomness you get from math/rand for shuffling your game’s card deck. We need the cryptographic-grade stuff. That’s what crypto/rand is for.
Think of math/rand as a clever magician doing a card trick: it looks random to you, but it’s following a secret script (a seed). Anyone who knows the script knows the trick. crypto/rand, on the other hand, is pulling cards from a giant, chaotic deck being constantly shuffled by cosmic noise from your operating system. It’s fundamentally unpredictable.
The Absolute Basics: Reading Your Bytes
The primary interface is the Reader, a global, shared instance of a cryptographically secure random number generator. You don’t create it; you just use it. Its job is to fill a byte slice ([]byte) with random goodness.
package main
import (
"crypto/rand"
"encoding/hex"
"fmt"
"log"
)
func main() {
// Let's generate a 16-byte (128-bit) key. This is a common size.
key := make([]byte, 16)
// This is the most important call in this chapter.
_, err := rand.Read(key)
if err != nil {
log.Fatalf("Failed to generate random key: %v", err) // This is actually a big deal.
}
// Let's see what we got, in hex for readability.
fmt.Printf("Random key: %s\n", hex.EncodeToString(key))
// Example output: Random key: 9a7f3d... (you'll get something different, obviously)
}
The crucial part here is checking that error. On most modern systems, rand.Read will never fail. But on some obscure or locked-down environment, it might not have access to a randomness source. If it fails, you absolutely cannot proceed. Your encryption would be built on a foundation of predictability, which is worse than no encryption at all.
Generating Random Integers Without Shooting Yourself in the Foot
You’ll often need a random number within a range, say, for a random default port or a random backoff duration. This is where everyone, and I mean everyone, gets tempted to use math/rand and then seed it with crypto/rand. Don’t. It’s a trap. You’ll get the seeding right but then use a non-crypto generator afterward, which is pointless. Instead, use the rand.Int function from crypto/rand.
func generateRandomPortAbove1024() (int, error) {
// This generates a random integer in [0, max). Since we want a port above 1024,
// let's choose a range between 1025 and 9999.
const minPort = 1025
const maxPort = 10000
// rand.Int returns (*big.Int, error)
n, err := rand.Int(rand.Reader, big.NewInt(maxPort-minPort))
if err != nil {
return 0, err
}
// Convert the big.Int to an int64 and add our minimum offset.
return int(n.Int64()) + minPort, nil
}
Yes, it’s verbose. Yes, dealing with *big.Int feels like using a sledgehammer to crack a nut. I don’t know why the Go team didn’t include a simpler helper function for this common case—a truly baffling omission. But this verbosity is the price of doing it right. Writing a wrapper function is your first best practice.
The One Joke We’re Allowed to Make
There’s an old, infamous comment in the libc source code about their RNG: “This is not the best random number generator, but it is the fastest one we could find.” The Go team, thankfully, did not make this trade-off. crypto/rand is a direct interface to your OS’s battle-tested entropy source. On Linux, that’s getrandom(2) syscall; on Windows, it’s BCryptGenRandom; on Unixes, it reads from /dev/urandom. These are the correct, secure sources. The designers got this part unequivocally right.
The io.Reader Interface is Your Superpower
Because rand.Reader is just an io.Reader, it composes beautifully with the rest of the Go standard library. Need to generate a random UUID? Use it with io.ReadFull. Need to create a random temporary file? Use it with io.Copy. This design is a stroke of genius.
func generateRandomString(length int) (string, error) {
// This is a more efficient way to generate a random identifier
// than multiple calls to rand.Int.
const letters = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
bytes := make([]byte, length)
// We're going to generate random bytes and then map them to our letter set.
if _, err := rand.Read(bytes); err != nil {
return "", err
}
for i := range bytes {
bytes[i] = letters[bytes[i]%byte(len(letters))]
}
return string(bytes), nil
}
Heads-up: This method, while common, introduces a tiny, usually negligible bias because the length of the letters slice (62) doesn’t divide evenly into 256. For a session token, it’s fine. For a nuclear launch code, you’d want to use a more complex rejection sampling method. Know your threat model.
The final word: never, ever skip the error check. The one time you do is the time your production system ends up on a platform where /dev/urandom is mysteriously blocked, and you’ll be generating the same “random” key for every single customer. And I will point and laugh from the sidelines. Don’t let that happen.