Question

How to Seed a Random Number Generator Properly in Go

gorandom

Question

I am trying to generate a random string in Go, and this is the code I have so far:

package main

import (
    "bytes"
    "fmt"
    "math/rand"
    "time"
)

func main() {
    fmt.Println(randomString(10))
}

func randomString(l int) string {
    var result bytes.Buffer
    var temp string

    for i := 0; i < l; {
        if string(randInt(65, 90)) != temp {
            temp = string(randInt(65, 90))
            result.WriteString(temp)
            i++
        }
    }

    return result.String()
}

func randInt(min int, max int) int {
    rand.Seed(time.Now().UTC().UnixNano())
    return min + rand.Intn(max-min)
}

This implementation is very slow. Because I seed using the current time on every call, I sometimes get the same random number repeatedly for a short period, which causes the loop to repeat many times. How can I improve this code and seed the random number generator properly in Go?

Short Answer

By the end of this page, you will understand how Go's pseudo-random number generator works, why seeding should usually happen only once, and how to generate random strings more efficiently. You will also see better patterns for reusable randomness in real Go programs.

Concept

Go's math/rand package generates pseudo-random numbers. That means the numbers are not truly random; they come from a deterministic sequence.

A seed is the starting point for that sequence.

  • If you use the same seed, you get the same sequence.
  • If you use a different seed, you get a different sequence.

The key idea is:

  • Seed once when your program starts.
  • Do not seed before every random number.

In your code, rand.Seed(time.Now().UTC().UnixNano()) runs inside randInt(), so it is called every time you want a number. That causes two problems:

  1. Performance problem: seeding repeatedly is unnecessary work.
  2. Randomness problem: if two calls happen very close together, they may get the same seed or nearly identical seeds, producing repeated values.

For most programs using math/rand, the usual pattern is:

rand.Seed(time.Now().UnixNano())

and then call rand.Intn(...) as many times as needed.

Also, your current function calls randInt() twice per loop iteration:

if string(randInt(65, 90)) != temp {
    temp = string(randInt(65, 90))

That means the value checked in the if condition is not necessarily the same as the value stored in temp, because a second random number is generated. So the logic is doing extra work and can behave unexpectedly.

Another important point: if you need secure randomness, such as passwords, tokens, or API keys, math/rand is not the right package. You should use crypto/rand instead. For simple random-looking strings in learning examples, test data, or non-security use cases, math/rand is fine.

Mental Model

Think of a pseudo-random number generator like a shuffled playlist machine.

  • The seed is the initial shuffle setup.
  • Once the playlist is prepared, you just keep asking for the next song.
  • If you keep rebuilding the playlist before every song, you waste time and may keep starting from almost the same point.

So instead of saying, "shuffle again" every second, you should:

  • shuffle once
  • keep taking the next item from the sequence

That is what seeding once does in Go.

Take Quiz

Syntax and Examples

The common pattern in Go is to seed once, then generate values many times.

package main

import (
    "fmt"
    "math/rand"
    "time"
)

func main() {
    rand.Seed(time.Now().UnixNano())

    fmt.Println(rand.Intn(10)) // 0 to 9
    fmt.Println(rand.Intn(10))
    fmt.Println(rand.Intn(10))
}

rand.Intn(10) returns a number in the range:

  • minimum: 0
  • maximum: 9

If you want uppercase letters, you can map random indexes into a character set.

package main

import (
    "fmt"
    "math/rand"
    "time"
)

const letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"

func randomString(n int) string {
    b := make([]byte, n)
    for i := range b {
        b[i] = letters[rand.Intn(len(letters))]
    }
    return string(b)
}

func main() {
    rand.Seed(time.Now().UnixNano())
    fmt.Println(randomString(10))
}

This version is simpler and faster because:

  • it seeds once
  • it does not use a bytes.Buffer unnecessarily
  • it builds the string directly from bytes

If you want to avoid consecutive repeated letters, generate one random letter per loop and compare it with the previous one:

package main

import (
    "fmt"
    "math/rand"
    "time"
)

const letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"

func randomStringNoRepeat(n int) string {
    if n <= 0 {
        return ""
    }

    b := make([]byte, n)
    b[0] = letters[rand.Intn(len(letters))]

    for i := 1; i < n; i++ {
        for {
            ch := letters[rand.Intn(len(letters))]
            if ch != b[i-1] {
                b[i] = ch
                break
            }
        }
    }

    return string(b)
}

func main() {
    rand.Seed(time.Now().UnixNano())
    fmt.Println(randomStringNoRepeat(10))
}

This keeps the "no two adjacent characters are the same" rule, but does it correctly.

Step by Step Execution

Consider this example:

package main

import (
    "fmt"
    "math/rand"
    "time"
)

const letters = "ABC"

func randomString(n int) string {
    b := make([]byte, n)
    for i := range b {
        b[i] = letters[rand.Intn(len(letters))]
    }
    return string(b)
}

func main() {
    rand.Seed(time.Now().UnixNano())
    fmt.Println(randomString(5))
}

Possible execution:

  1. rand.Seed(time.Now().UnixNano())

    • The random generator is initialized once.
  2. randomString(5) is called.

    • A byte slice of length 5 is created.
  3. First loop iteration:

    • rand.Intn(3) returns 2
    • letters[2] is 'C'
    • b[0] = 'C'
  4. Second loop iteration:

    • rand.Intn(3) returns 0
    • letters[0] is 'A'
    • b[1] = 'A'
  5. Third loop iteration:

    • rand.Intn(3) returns 1
    • letters[1] is 'B'
    • b[2] = 'B'
  6. The loop continues until all 5 positions are filled.

  7. string(b) converts the byte slice into a Go string.

Possible output:

CABAC

The important part is that the generator state moves forward naturally on each call. It is not reset each time.

Real World Use Cases

This concept appears in many practical situations:

  • Generating test data

    • random usernames
    • sample order IDs
    • fake labels for demos
  • Game development

    • random enemy movement
    • loot drops
    • map variation
  • Simulations

    • random sampling
    • trial runs
    • probabilistic models
  • Load testing and scripting

    • creating varied request payloads
    • generating temporary values for mock APIs
  • Shuffling or selecting items

    • picking a random record
    • randomizing quiz questions
    • sampling input data

In all of these, the same rule applies: initialize the random source once, then reuse it.

Take Quiz

Real Codebase Usage

In real Go codebases, developers usually avoid calling the global seed repeatedly. Common patterns include:

Seed once in main

func main() {
    rand.Seed(time.Now().UnixNano())
    // start app
}

This is fine for small programs.

Use a dedicated rand.Rand

For better control, create your own generator:

src := rand.NewSource(time.Now().UnixNano())
r := rand.New(src)

fmt.Println(r.Intn(100))

This pattern is useful when:

  • you want separate random generators
  • you want predictable tests with a fixed seed
  • you want to pass randomness into functions or structs

Fixed seed for tests

r := rand.New(rand.NewSource(42))

This makes random behavior reproducible in tests.

Validation and guard clauses

Developers often validate input before generating data:

func randomString(n int) string {
    if n <= 0 {
        return ""
    }
    // generate string
}

This avoids edge-case bugs.

Secure values use crypto/rand

In production code, if the string is used for:

  • passwords
  • session IDs
  • reset tokens
  • API secrets

then crypto/rand is the standard choice, not math/rand.

Common Mistakes

1. Seeding on every call

Broken code:

func randInt(min int, max int) int {
    rand.Seed(time.Now().UnixNano())
    return min + rand.Intn(max-min)
}

Why it is a problem:

  • repeated seeding is slow
  • calls close together may repeat the same sequence

Better:

func main() {
    rand.Seed(time.Now().UnixNano())
}

2. Calling random generation twice when you only need one value

Broken code:

if string(randInt(65, 90)) != temp {
    temp = string(randInt(65, 90))
}

Why it is a problem:

  • the if check uses one random value
  • the assignment uses another random value
  • the comparison does not match what you actually store

Better:

ch := byte(randInt(65, 91))
if ch != temp {
    temp = ch
}

3. Off-by-one range errors

This code:

rand.Intn(max-min)

returns values from 0 up to max-min-1.

So:

min + rand.Intn(max-min)

returns min to max-1.

If you want ASCII A through Z, use:

randInt(65, 91)

because 91 is exclusive.

4. Using string(int) without understanding it

In Go, string(65) becomes "A" because it converts a code point to a string.

That works, but if you are building character data, using byte or rune is often clearer.

5. Using math/rand for security-sensitive values

Broken choice:

  • password generator
  • token generator
  • auth secret generator

Use crypto/rand instead.

6. Ignoring invalid input

If n is negative or zero, your function should handle it clearly.

Better:

if n <= 0 {
    return ""
}

Comparisons

ConceptBest forNotes
math/randFast pseudo-random valuesGood for simulations, games, test data
crypto/randSecure random valuesUse for passwords, tokens, secrets
Seeding onceNormal application flowCorrect and efficient
Seeding every callAlmost never correctSlower and can reduce randomness quality
Global rand functionsSmall/simple programsEasy to use
rand.New(...) with custom sourceLarger apps and testsMore control and easier to test

Another useful comparison:

ApproachExampleGood?Why
Seed inside helperrand.Seed(...) in randInt()NoResets generator too often
Seed in mainrand.Seed(...) onceYesStandard simple pattern
Dedicated generatorr := rand.New(...)YesBest when you want explicit control
Take Quiz

Cheat Sheet

import (
    "math/rand"
    "time"
)

Seed once:

rand.Seed(time.Now().UnixNano())

Random int from 0 to n-1:

rand.Intn(n)

Random int from min to max-1:

min + rand.Intn(max-min)

Uppercase letters:

const letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
ch := letters[rand.Intn(len(letters))]

Random string:

func randomString(n int) string {
    b := make([]byte, n)
    for i := range b {
        b[i] = letters[rand.Intn(len(letters))]
    }
    return string(b)
}

Rules:

  • Seed once, not per function call
  • Intn(n) never returns n
  • Use crypto/rand for secure values
  • Use rand.New(rand.NewSource(seed)) when you want your own generator

Edge case:

  • if n <= 0, return "" or handle it explicitly

FAQ

Why should I seed math/rand only once in Go?

Because seeding resets the pseudo-random sequence. If you do it repeatedly, especially in a tight loop, you can get repeated or low-quality results and unnecessary overhead.

Why do I get repeated random values when using time.Now()?

If calls happen very close together, the time-based seed may be the same or too similar, which can restart the generator at nearly the same point.

Is math/rand good enough for passwords or tokens?

No. Use crypto/rand for anything security-sensitive.

What does rand.Intn(n) return?

It returns an integer from 0 up to n-1.

How do I include Z when generating uppercase letters?

Use an exclusive upper bound of 91, because ASCII Z is 90.

Should I use rand.Seed in every package?

Usually no. Seed once at program startup, or create a dedicated rand.Rand instance and pass it where needed.

What is the advantage of rand.NewSource and rand.New?

They let you create a separate random generator with its own state, which is useful for testing and cleaner design.

Take Quiz

Mini Project

Description

Build a small Go program that generates random invitation codes made of uppercase letters. This project demonstrates proper random seeding, efficient string building, and optional prevention of consecutive duplicate characters.

Goal

Create a function that generates uppercase random codes of a requested length, using a correctly seeded random number generator.

Requirements

  • Seed the random number generator only once
  • Generate a string containing only uppercase A-Z letters
  • Return an empty string if the requested length is 0 or less
  • Avoid consecutive duplicate letters in the generated code
Take Quiz

Keep learning

Related questions

View more