How to properly seed a random number generator in Go: performance secrets

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

Randomness is too serious to leave to chance

\n\n

Anyone who has ever written code in Go has encountered random number generation. Random number generator (RNG) is a tricky thing. It seems simple: call rand.Intn() and get a number. But if you don't pay attention to proper seeding, your "randomness" will turn into a predictable sequence that will break tests, fail in cryptography, or simply surprise you with identical results on every run.

\n\n

On Stack Overflow, the question "How to properly seed random number generator" has gathered thousands of views. People get confused with time.Now().UnixNano(), use crypto/rand where it's not needed, and suffer from low performance. Today we'll break down this topic using Go as an example — no fluff, with real benchmarks and ready-made solutions.

\n\n

Why is seed not just a formality?

\n\n

Seed is the initial state of the generator. The same seed produces the same sequence of numbers. This is convenient for debugging but deadly for production if you expect unpredictability.

\n\n

In Go, the standard math/rand package uses a deterministic algorithm. If you forget to call rand.Seed(), then... Before Go 1.20, the default generator used a fixed seed (usually 1). Starting with Go 1.20, math/rand is automatically seeded with a random value at program startup. However, relying on this in critical places is bad practice.

\n\n

The main problem for beginners is trying to seed the generator for every random number:

\n\n
// ❌ BAD: recreating the generator every time\nfunc getRandom() int {\n    r := rand.New(rand.NewSource(time.Now().UnixNano()))\n    return r.Intn(100)\n}
\n\n

Why is this bad? time.Now().UnixNano() gives nanoseconds, but in reality the precision depends on the OS. In Windows, for example, it can be ~15 ms. If you call the function twice in a row, you'll get identical numbers. Plus, each call to rand.NewSource creates a new object, putting load on the garbage collector.

\n\n

Top 3 approaches to seeding in Go

\n\n

1. Classic approach: global seed once

\n\n

The simplest and most correct option for most applications is to set the seed once at program startup.

\n\n
package main\n\nimport (\n    "fmt"\n    "math/rand"\n    "time"\n)\n\nfunc main() {\n    // ✅ GOOD: seed once\n    rand.Seed(time.Now().UnixNano())\n\n    for i := 0; i < 5; i++ {\n        fmt.Println(rand.Intn(100))\n    }\n}
\n\n

Important: time.Now().UnixNano() gives a unique value, but if the program runs multiple times within the same nanosecond (unlikely but theoretically possible), the seed will be identical. For 99% of applications, this is sufficient.

\n\n

2. Local generator for isolation

\n\n

When you have multiple goroutines or don't want to affect the global state, create your own rand.Rand instance.

\n\n
func NewSeededGenerator() *rand.Rand {\n    seed := time.Now().UnixNano()\n    return rand.New(rand.NewSource(seed))\n}\n\n// Usage:\nrng := NewSeededGenerator()\nfor i := 0; i < 3; i++ {\n    fmt.Println(rng.Intn(100))\n}
\n\n

This approach gives full control: different goroutines can have their own generators, and they won't block each other (the global rand uses a mutex).

\n\n

3. Cryptographically secure seed for critical tasks

\n\n

If you need true randomness (tokens, passwords, cryptography) — forget about math/rand. Use crypto/rand:

\n\n
import (\n    "crypto/rand"\n    "encoding/binary"\n    "math/rand"\n)\n\nfunc CryptoSeed() int64 {\n    var buf [8]byte\n    _, err := rand.Read(buf[:])\n    if err != nil {\n                

Blogs

Book Recommendations