Alright, let’s talk about the new hotness: math/rand/v2. It landed in Go 1.22, and frankly, it’s about time. The old math/rand was like that reliable but slightly cranky old car that started most days. The new one is that car’s sleeker, more efficient descendant, with a few of the weird quirks finally fixed. We’re going to focus on two of the biggest upgrades: the end of the tedious seeding dance and the fantastic new buffet of distributions.

First, the elephant in the room. In the old package, if you just called rand.Intn(100) without seeding, you’d get the same sequence of “random” numbers every single time. This was the classic rookie mistake, leading to a generation of programmers thinking, “Wow, my random enemy movements are so predictable!” This happened because the default source was deterministic, seeded with 1 to give you reproducible results for things like testing. It was a design choice, but one that constantly bit people.

The End of Manual Seeding (Mostly)

math/rand/v2 fixes this by automatically seeding the global generator with a random value at startup. Thank the cosmos.

package main

import (
    "fmt"
    "math/rand/v2" // Note the v2!
)

func main() {
    // No more rand.Seed! It just works.
    for i := 0; i < 5; i++ {
        fmt.Println(rand.IntN(100)) // Note the new name: IntN, not Intn
    }
}

Run that a few times. You’ll get different numbers. It’s a small miracle. Now, the automatic seeding uses a cryptographically random seed if possible, which is great. But—and this is a big but—the global generator is now safe from this pitfall, but it’s still slow because it uses a mutex for safety. For high-throughput code, you’ll want your own generator.

Creating and Seeding Your Own Generators

This is where the new package shines. You can create a local generator, which is much faster, and seed it yourself. The key improvement here is the rand.New function, which takes a rand.Source. And the sources are now much better.

func main() {
    // The new recommended way: create a source from a random seed.
    // The time.Now().UnixNano() dance is officially passé.
    source := rand.NewPCG(1, 2) // You can provide your own deterministic seeds
    rng := rand.New(source)

    // Or, for the common case of "just give me randomness":
    // This uses a random seed from the crypto rand reader under the hood.
    rng := rand.New(rand.NewPCG(1, 2))

    for i := 0; i < 5; i++ {
        fmt.Println(rng.IntN(100))
    }
}

Why NewPCG? The old package used a linear congruential generator (LCG), which was fast but had… well, bad statistical properties. The new default is a PCG generator, which is significantly better—faster, smaller, and actually produces decent pseudo-randomness. It’s a real upgrade.

Beyond Uniform Integers: A Distribution Buffet

The old math/rand gave you a decent set of tools, but v2 goes all out. It’s like going from a corner shop to a full supermarket. Need a random value from a normal (Gaussian) distribution? Forgeddaboutit.

func main() {
    rng := rand.New(rand.NewPCG(1, 2))

    // Uniform distribution in [0.0, 1.0). Old faithful.
    fmt.Println("Uniform:", rng.Float64())

    // Normal (Gaussian) distribution: mean=0, standard deviation=1.
    fmt.Println("Normal:", rng.NormFloat64())

    // Exponential distribution: rate parameter lambda=1.
    fmt.Println("Exponential:", rng.ExpFloat64())
}

This is huge for simulations, games, machine learning, or any domain where the real world isn’t uniformly distributed. The implementation details matter here. These aren’t just hacks built on top of Float64(); they’re carefully implemented transformations (like the ziggurat algorithm for the normal distribution) that are both efficient and statistically sound.

Best Practices and Pitfalls

  1. Don’t Use the Global Generator for Performance: I can’t stress this enough. If you’re in a hot path, like generating millions of random numbers per second in a goroutine, the mutex on the global generator will murder your performance. Create a local rand.New(rand.NewPCG(1, 2)) for each goroutine. Seeding each with rand.NewPCG(1, 2) is perfectly fine and thread-safe.

  2. Reproducibility vs. Randomness: The auto-seeded global generator is great for “I need random stuff now.” But if you need reproducible results—for a simulation, a test, or a game level—you must create your own generator with a fixed seed. rand.New(rand.NewPCG(42, 0)) will give you the same sequence every time, which is exactly what you want in that case.

  3. Naming Conventions: Watch out for the subtle naming changes. It’s IntN(100) now, not Intn(100). It’s a small change, but it’s the kind of thing that will break your code and make you curse for a solid ten minutes until you notice it. Consider it a rite of passage.

So, in summary, math/rand/v2 is a genuine improvement. It fixes the glaring foot-gun of seeding, provides a faster and better algorithm under the hood, and gives you a proper toolkit of distributions. It respects your time while giving you more power. That’s a win in my book. Now go use it. The old math/rand is officially on notice.