Генерация псевдослучайных чисел: пакет math/rand

Краткое описание

Псевдослучайные числа (PRNG — Pseudo-Random Number Generator) — числа, которые выглядят случайными, но генерируются по детерминированному математическому алгоритму. В Go для работы с ними используется пакет math/rand. Начиная с Go 1.20, глобальный генератор автоматически инициализируется случайным значением при старте программы, что означает разные последовательности чисел при каждом запуске. Для тестирования и отладки можно создать локальный генератор с фиксированным seed (начальным значением) — тогда последовательность станет детерминированной и будет повторяться при каждом запуске. Пакет предоставляет функции для генерации целых (rand.Int(), rand.Intn(n)) и вещественных (rand.Float64(), rand.Float32()) чисел, а также позволяет создавать числа в произвольных диапазонах через математические формулы.


Что такое псевдослучайные числа

Определение

Псевдослучайные числа — числа, которые кажутся случайными, но на самом деле генерируются по заранее известному алгоритму.

Ключевые характеристики

  • Детерминированность — при одинаковом начальном значении (seed) последовательность повторяется
  • Предсказуемость — зная алгоритм и seed, можно предсказать все числа
  • Статистическая случайность — проходят тесты на случайность, но не являются истинно случайными
  • НЕ криптографически стойкие — не подходят для задач безопасности

Применение

Псевдослучайные числа используются для:

  • Симуляций и моделирования
  • Тестирования программ
  • Игр и развлечений
  • Генерации тестовых данных
  • Алгоритмов с элементом случайности

Важно: Для криптографии используйте crypto/rand, а не math/rand.


Импорт пакета math/rand

Синтаксис

import (
    "math/rand"
)

Базовое использование

После импорта доступны функции для генерации случайных чисел:

package main

import (
    "fmt"
    "math/rand"
)

func main() {
    randomNumber := rand.Int()
    fmt.Println(randomNumber)
}

rand.Int(): большое случайное целое число

Синтаксис

randomInt := rand.Int()

Описание

Функция rand.Int() возвращает неотрицательное псевдослучайное целое число типа int (обычно 63-битное).

Пример

var randomInt1 int = rand.Int()
randomInt2 := rand.Int()
randomInt3 := rand.Int()

fmt.Printf("Случайное число 1: %d\n", randomInt1)
fmt.Printf("Случайное число 2: %d\n", randomInt2)
fmt.Printf("Случайное число 3: %d\n", randomInt3)

Пошаговое выполнение

Шаг 1: Вызов rand.Int() → генерация первого числа (например, 5577006791947779410)

Шаг 2: Присваивание randomInt1 = 5577006791947779410

Шаг 3: Вызов rand.Int() → генерация второго числа (например, 8674665223082153551)

Шаг 4: Присваивание randomInt2 = 8674665223082153551

Шаг 5: Вызов rand.Int() → генерация третьего числа (например, 6129484611666145821)

Шаг 6: Присваивание randomInt3 = 6129484611666145821

Шаг 7: Вывод всех трёх чисел

Результат (пример):

Случайное число 1: 5577006791947779410
Случайное число 2: 8674665223082153551
Случайное число 3: 6129484611666145821

Диапазон значений

  • Минимум: 0
  • Максимум: 2^63 - 1 (для 64-битных систем)
  • Тип: int (знаковый, но всегда неотрицательный)

Когда использовать

  • Нужно очень большое случайное число
  • Точный диапазон не важен
  • Дальше будете применять математические операции

rand.Intn(n): число от 0 до n-1

Синтаксис

randomNum := rand.Intn(n)  // Вернёт число из [0, n)

Описание

Функция rand.Intn(n) возвращает псевдослучайное число в полуинтервале [0, n) — от 0 включительно до n не включая.

Математическая нотация

[0, n) означает:

  • 0включено (может выпасть)
  • nне включено (не может выпасть)

Пример: числа от 0 до 9

fmt.Println("rand.Intn(10) вернёт число от 0 до 9")

num1 := rand.Intn(10)
num2 := rand.Intn(10)
num3 := rand.Intn(10)
num4 := rand.Intn(10)
num5 := rand.Intn(10)

fmt.Printf("rand.Intn(10): %d\n", num1)
fmt.Printf("rand.Intn(10): %d\n", num2)
fmt.Printf("rand.Intn(10): %d\n", num3)
fmt.Printf("rand.Intn(10): %d\n", num4)
fmt.Printf("rand.Intn(10): %d\n", num5)

Результат (пример):

rand.Intn(10) вернёт число от 0 до 9
rand.Intn(10): 1
rand.Intn(10): 7
rand.Intn(10): 7
rand.Intn(10): 9
rand.Intn(10): 1

Важные моменты

  1. Нижняя граница: всегда 0
  2. Верхняя граница: n - 1 (не сам n)
  3. Паника: если n <= 0, программа упадёт

Таблица примеров

Вызов Диапазон Возможные значения
rand.Intn(10) [0, 10) 0, 1, 2, 3, 4, 5, 6, 7, 8, 9
rand.Intn(5) [0, 5) 0, 1, 2, 3, 4
rand.Intn(100) [0, 100) 0, 1, 2, …, 98, 99
rand.Intn(2) [0, 2) 0, 1

Практический пример: игральная кость (1-6)

Задача

Сгенерировать случайное число от 1 до 6 включительно (имитация броска игральной кости).

Проблема

rand.Intn(6) возвращает числа от 0 до 5, но нам нужны 1 до 6.

Решение: добавить 1

Формула:

dice := rand.Intn(6) + 1

Логика

  1. rand.Intn(6) → возвращает 0, 1, 2, 3, 4, 5
  2. + 1 → сдвигаем диапазон на 1 вправо
  3. Результат: 1, 2, 3, 4, 5, 6

Код

fmt.Println("--- Игральная кость (1-6) ---")
fmt.Println("Формула: rand.Intn(6) + 1")

dice1 := rand.Intn(6) + 1
dice2 := rand.Intn(6) + 1
dice3 := rand.Intn(6) + 1

fmt.Printf("Бросок 1: %d\n", dice1)
fmt.Printf("Бросок 2: %d\n", dice2)
fmt.Printf("Бросок 3: %d\n", dice3)

Результат (пример):

--- Игральная кость (1-6) ---
Формула: rand.Intn(6) + 1
Бросок 1: 4
Бросок 2: 2
Бросок 3: 6

Пошаговое выполнение (dice1)

Шаг 1: Вызов rand.Intn(6) → возвращает, например, 3

Шаг 2: Добавление + 13 + 1 = 4

Шаг 3: Присваивание dice1 = 4

Шаг 4: Вывод “Бросок 1: 4”

Обобщённая формула: диапазон [1, n]

// Для диапазона от 1 до n включительно:
result := rand.Intn(n) + 1

Примеры:

  • rand.Intn(6) + 1 → (игральная кость)
  • rand.Intn(10) + 1 → (числа 1-10)
  • rand.Intn(100) + 1 → (проценты 1-100)

Генерация чисел в произвольном диапазоне [min, max]

Формула для целых чисел

result := min + rand.Intn(max - min + 1)

Логика формулы

  1. max - min — размер диапазона (без границ)
  2. + 1 — включение верхней границы
  3. rand.Intn(...) — случайное число от 0 до размера диапазона
  4. min + — сдвиг начала диапазона на min

Пример: возраст 18-65 лет

age := rand.Intn(65-18+1) + 18
fmt.Printf("Возраст (18-65): %d\n", age)

Пошаговый расчёт:

Шаг 1: Вычисление размера диапазона: 65 - 18 + 1 = 48

Шаг 2: rand.Intn(48) → возвращает число от 0 до 47

Шаг 3: Предположим, вернулось 25

Шаг 4: Добавление min: 25 + 18 = 43

Шаг 5: Вывод “Возраст (18-65): 43”

Проверка границ

Минимальное значение:

  • rand.Intn(48)0
  • 0 + 18 = 18

Максимальное значение:

  • rand.Intn(48)47
  • 47 + 18 = 65

Читаемость формулы

❌ Плохо (неясно):

age := rand.Intn(48) + 18  // Откуда 48?

✅ Хорошо (явно указаны границы):

age := rand.Intn(65-18+1) + 18  // Ясно: от 18 до 65

Причина: Код должен быть самодокументируемым. Любой читающий сразу понимает границы диапазона.

Примеры других диапазонов

// Диапазон [10, 20]
num := 10 + rand.Intn(20-10+1)

// Диапазон [100, 200]
num := 100 + rand.Intn(200-100+1)

// Диапазон [-10, 10]
num := -10 + rand.Intn(10-(-10)+1)

Практические примеры использования rand.Intn()

Пример 1: Подбрасывание монеты

coin := rand.Intn(2)
fmt.Printf("Монета (0-решка, 1-орёл): %d\n", coin)

Логика:

  • rand.Intn(2)0 или 1
  • 0 = решка
  • 1 = орёл

Использование с if:

coin := rand.Intn(2)
if coin == 0 {
    fmt.Println("Решка")
} else {
    fmt.Println("Орёл")
}

Пример 2: Процент (0-100)

percentage := rand.Intn(101)  // [0, 100]
fmt.Printf("Процент (0-100): %d%%\n", percentage)

Важно: rand.Intn(101), а не 100, чтобы включить 100.

Пример 3: День недели (1-7)

dayOfWeek := rand.Intn(7) + 1  // [1, 7]
fmt.Printf("День недели (1-7): %d\n", dayOfWeek)

Сопоставление:

  • 1 = Понедельник
  • 2 = Вторник
  • 7 = Воскресенье

Пример 4: HTTP статус-код

// Случайный код ошибки клиента [400, 499]
statusCode := 400 + rand.Intn(499-400+1)
fmt.Printf("HTTP Status: %d\n", statusCode)

Пример 5: Температура [-30, 40]

temperature := -30 + rand.Intn(40-(-30)+1)
fmt.Printf("Температура: %d°C\n", temperature)

Go 1.20+: автоматическая инициализация глобального генератора

Что изменилось в Go 1.20

До Go 1.20: Глобальный генератор использовал фиксированный seed (по умолчанию 1), что приводило к одинаковым числам при каждом запуске.

С Go 1.20: Глобальный генератор автоматически инициализируется случайным значением при старте программы → разные последовательности при каждом запуске.

Старый способ (до Go 1.20)

import (
    "math/rand"
    "time"
)

func main() {
    // Обязательная инициализация
    rand.Seed(time.Now().UnixNano())
    
    fmt.Println(rand.Intn(100))
}

Проблема старого подхода:

  • Легко забыть вызвать rand.Seed()
  • Программы выдавали одинаковые числа
  • Нужен импорт time

Новый способ (Go 1.20+)

import "math/rand"

func main() {
    // Инициализация НЕ НУЖНА - происходит автоматически
    fmt.Println(rand.Intn(100))  // Всегда разные числа
}

Преимущества:

  • ✅ Автоматическая инициализация
  • ✅ Не нужно помнить про Seed()
  • ✅ Разные числа при каждом запуске “из коробки”

rand.Seed() помечен как deprecated

Важно: Функция rand.Seed() устарела (deprecated) в Go 1.20+.

// ❌ Устаревший код
rand.Seed(time.Now().UnixNano())  // deprecated

Когда увидите rand.Seed() в коде:

  1. Код писался для Go < 1.20 — устаревшая версия
  2. Код нужно обновить — удалить rand.Seed()
  3. Исключение: Явная потребность в детерминированности (используйте локальный генератор)

Восстановление старого поведения

Если по каким-то причинам нужно старое детерминированное поведение глобального генератора:

Способ 1: Явный вызов Seed

rand.Seed(1)  // Фиксированная последовательность

Способ 2: Переменная окружения

GODEBUG=randautoseed=0 ./myprogram

Локальный генератор с фиксированным seed

Зачем нужен локальный генератор

Детерминированность — при одинаковом seed последовательность чисел повторяется при каждом запуске программы.

Применение:

  • Тестирование — воспроизводимые тесты
  • Отладка — воспроизведение ошибок
  • Симуляции — повторяемые результаты
  • Демонстрация — одинаковое поведение на разных машинах

Создание локального генератора

rng := rand.New(rand.NewSource(seed))
  • rand.NewSource(seed) — создаёт источник случайных чисел с заданным seed
  • rand.New(...) — создаёт генератор на основе источника
  • seed — начальное значение (любое int64)

Пример: фиксированная последовательность

// Создаём локальный генератор с seed = 42
rng := rand.New(rand.NewSource(42))

value1 := rng.Intn(100)
value2 := rng.Intn(100)
value3 := rng.Intn(100)
value4 := rng.Intn(100)
value5 := rng.Intn(100)

fmt.Printf("1) %d\n", value1)
fmt.Printf("2) %d\n", value2)
fmt.Printf("3) %d\n", value3)
fmt.Printf("4) %d\n", value4)
fmt.Printf("5) %d\n", value5)

Результат при первом запуске:

1) 39
2) 61
3) 27
4) 80
5) 67

Результат при втором запуске:

1) 39
2) 61
3) 27
4) 80
5) 67

Вывод: Последовательность идентична при каждом запуске!

Сравнение: глобальный vs локальный

Глобальный генератор (Go 1.20+):

fmt.Println(rand.Intn(100))  // Каждый запуск — разные числа

Локальный генератор (фиксированный seed):

rng := rand.New(rand.NewSource(42))
fmt.Println(rng.Intn(100))  // Каждый запуск — одинаковые числа

Использование локального генератора

Все функции работают аналогично:

rng := rand.New(rand.NewSource(100))

// Целые числа
num1 := rng.Int()
num2 := rng.Intn(50)

// Вещественные числа
float1 := rng.Float64()
float2 := rng.Float32()

// Диапазоны
age := 18 + rng.Intn(65-18+1)

Выбор значения seed

Фиксированное число:

rng := rand.New(rand.NewSource(42))  // Всегда одинаково

Текущее время (для псевдослучайности):

rng := rand.New(rand.NewSource(time.Now().UnixNano()))

Любое число:

rng := rand.New(rand.NewSource(123456789))

Генерация вещественных чисел

rand.Float64(): число от 0.0 до 1.0

Синтаксис:

randomFloat := rand.Float64()

Диапазон: [0.0, 1.0) — от 0.0 включительно до 1.0 не включая.

Пример:

float1 := rand.Float64()
float2 := rand.Float64()
float3 := rand.Float64()

fmt.Printf("Float 1: %.6f\n", float1)
fmt.Printf("Float 2: %.6f\n", float2)
fmt.Printf("Float 3: %.6f\n", float3)

Результат (пример):

Float 1: 0.605545
Float 2: 0.890594
Float 3: 0.014248

rand.Float32(): вещественное 32-bit

Синтаксис:

randomFloat32 := rand.Float32()

Диапазон: [0.0, 1.0) (меньшая точность, чем Float64).

Пример:

rng32 := rand.New(rand.NewSource(100))

f32_1 := rng32.Float32()
f32_2 := rng32.Float32()

fmt.Printf("Float32 #1: %.6f\n", f32_1)
fmt.Printf("Float32 #2: %.6f\n", f32_2)

Результат (пример):

Float32 #1: 0.240826
Float32 #2: 0.936471

Сравнение Float64 vs Float32

Характеристика Float64 Float32
Точность Двойная (64 бита) Одинарная (32 бита)
Диапазон [0.0, 1.0) [0.0, 1.0)
Десятичные знаки ~15-17 ~6-9
Размер 8 байт 4 байт
Использование Основной выбор Экономия памяти

Вещественные числа в произвольном диапазоне

Формула для диапазона [min, max]

result := min + rand.Float64() * (max - min)

Логика формулы

  1. rand.Float64() → число от 0.0 до 1.0
  2. (max - min) → размер диапазона
  3. Умножение → масштабирование от 0 до размера диапазона
  4. min + → сдвиг начала диапазона

Пример: температура [5.0, 15.0]

fMin := 5.0
fMax := 15.0

rngFloat := rand.New(rand.NewSource(2))

temperature := fMin + rngFloat.Float64()*(fMax-fMin)
fmt.Printf("Температура: %.2f°C\n", temperature)

Пошаговый расчёт (предположим, Float64() = 0.73):

Шаг 1: rand.Float64()0.73

Шаг 2: Размер диапазона: 15.0 - 5.0 = 10.0

Шаг 3: Масштабирование: 0.73 * 10.0 = 7.3

Шаг 4: Сдвиг: 5.0 + 7.3 = 12.3

Шаг 5: Вывод “Температура: 12.30°C”

Проверка границ

Минимум (Float64() = 0.0):

5.0 + 0.0 * 10.0 = 5.0 ✅

Максимум (Float64() ≈ 0.9999…):

5.0 + 0.9999 * 10.0 ≈ 14.999 ✅

Важно: 15.0 не включается (как и в rand.Intn()).

Генерация вещественных чисел “вручную”

Способ 1: Через целые числа (низкая точность)

// Диапазон [0.00, 0.99]
floatNum := float64(rand.Intn(100)) / 100.0
fmt.Printf("Число: %.2f\n", floatNum)

Логика:

  • rand.Intn(100)0 до 99
  • Деление на 100.00.00 до 0.99

Способ 2: Больше точности

// Диапазон [0.000, 0.999]
floatNum := float64(rand.Intn(1000)) / 1000.0
fmt.Printf("Число: %.3f\n", floatNum)

Способ 3: С целой частью

// Диапазон [0.00, 4.99]
floatNum := float64(rand.Intn(5)) + float64(rand.Intn(100))/100.0
fmt.Printf("Число: %.2f\n", floatNum)

Логика:

  1. rand.Intn(5) → целая часть [0, 4]
  2. rand.Intn(100)/100.0 → дробная часть [0.00, 0.99]
  3. Сложение → [0.00, 4.99]

Важно: Использование rand.Float64() проще и точнее, чем ручные манипуляции с целыми числами!


Примеры: целые числа в диапазонах

Пример 1: Диапазон

min := 10
max := 20

rngInt := rand.New(rand.NewSource(1))

fmt.Printf("Диапазон [%d, %d]:\n", min, max)

rangeNum1 := min + rngInt.Intn(max-min+1)
rangeNum2 := min + rngInt.Intn(max-min+1)
rangeNum3 := min + rngInt.Intn(max-min+1)

fmt.Printf("  %d\n", rangeNum1)
fmt.Printf("  %d\n", rangeNum2)
fmt.Printf("  %d\n", rangeNum3)

Результат (seed = 1, детерминированный):

Диапазон [10, 20]:
  18
  15
  19

Пример 2: Случайный день месяца

day := 1 + rand.Intn(31)
fmt.Printf("Случайный день месяца: %d\n", day)

Пример 3: Процент скидки

discount := 5 + rand.Intn(50-5+1)
fmt.Printf("Скидка: %d%%\n", discount)

Пример 4: Отрицательные числа [-100, 100]

num := -100 + rand.Intn(100-(-100)+1)
fmt.Printf("Число: %d\n", num)

Сравнительная таблица: глобальный vs локальный генератор

Характеристика Глобальный (Go 1.20+) Локальный (с seed)
Создание Автоматически rand.New(rand.NewSource(seed))
Инициализация Автоматическая Явная с seed
Детерминированность ❌ Разные числа каждый раз ✅ Одинаковые при одном seed
Синтаксис rand.Intn(10) rng.Intn(10)
Применение Общее использование Тестирование, отладка
Потокобезопасность ✅ Да ❌ Нет (нужна синхронизация)
Скорость Быстрее (оптимизирован) Медленнее

Best Practices

1. Используйте глобальный генератор для случайности

// ✅ Хорошо: простой и эффективный код
randomNum := rand.Intn(100)

2. Локальный генератор для тестов

// ✅ Хорошо: детерминированные тесты
func TestSomething(t *testing.T) {
    rng := rand.New(rand.NewSource(42))
    result := rng.Intn(100)
    expected := 39  // Всегда одинаково при seed=42
    if result != expected {
        t.Errorf("Expected %d, got %d", expected, result)
    }
}

3. Явные границы в формулах

// ✅ Хорошо: сразу видны границы
age := rand.Intn(65-18+1) + 18

// ❌ Плохо: непонятно откуда 48
age := rand.Intn(48) + 18

4. Не используйте rand.Seed() в новом коде

// ❌ Плохо: deprecated
rand.Seed(time.Now().UnixNano())

// ✅ Хорошо: автоматическая инициализация (ничего не делаем)
// или создаём локальный генератор

5. Комментируйте неочевидные диапазоны

// ✅ Хорошо: комментарий объясняет логику
// Возраст покупателя: от 18 (совершеннолетие) до 65 (пенсия)
age := 18 + rand.Intn(65-18+1)

6. Не используйте math/rand для безопасности

// ❌ ОПАСНО: math/rand предсказуем
sessionID := rand.Intn(1000000)  // НЕ БЕЗОПАСНО!

// ✅ Правильно: используйте crypto/rand
// (не рассматривается в этом уроке)

7. Используйте Float64() для вещественных чисел

// ❌ Плохо: сложная математика с низкой точностью
num := float64(rand.Intn(100)) / 100.0

// ✅ Хорошо: встроенная функция с высокой точностью
num := rand.Float64()

Типичные ошибки

Ошибка 1: Забытая инициализация (Go < 1.20)

// ❌ Старые версии Go - одинаковые числа каждый раз
func main() {
    // rand.Seed() не вызвана!
    fmt.Println(rand.Intn(100))
}

В Go 1.20+ это исправлено автоматически

Ошибка 2: rand.Intn(0) или отрицательное n

// ❌ ПАНИКА: rand.Intn(0) или rand.Intn(-5)
num := rand.Intn(0)   // panic: invalid argument to Intn
num := rand.Intn(-5)  // panic: invalid argument to Intn

Решение: Всегда проверяйте, что n > 0.

Ошибка 3: Неправильный диапазон [1, n]

// ❌ Плохо: не включает n
dice := rand.Intn(6)  // Даёт 0-5, а не 1-6

// ✅ Правильно: добавить 1
dice := rand.Intn(6) + 1  // Даёт 1-6

Ошибка 4: Использование math/rand для криптографии

// ❌ ОПАСНО: предсказуемо
password := rand.Intn(1000000)

// ✅ Используйте crypto/rand (не рассматривается здесь)

Ошибка 5: Забытый +1 в диапазоне

// ❌ Плохо: max не включён
age := 18 + rand.Intn(65-18)  // Даёт 18-64, а не 18-65

// ✅ Правильно: добавить +1
age := 18 + rand.Intn(65-18+1)  // Даёт 18-65

Ключевые моменты

  1. Псевдослучайные числа — детерминированные, но выглядят случайно
  2. math/rand — пакет для работы с PRNG в Go
  3. Go 1.20+ — глобальный генератор инициализируется автоматически
  4. rand.Int() — большое случайное целое число
  5. rand.Intn(n) — число из полуинтервала [0, n)
  6. rand.Float64() — вещественное число из [0.0, 1.0)
  7. Локальный генераторrand.New(rand.NewSource(seed)) для детерминированности
  8. Формула [min, max]min + rand.Intn(max-min+1) для целых
  9. Формула [min, max]min + rand.Float64()*(max-min) для вещественных
  10. rand.Seed() deprecated — не используйте в новом коде

Что запомнить

  • import “math/rand” — импорт пакета для PRNG
  • Go 1.20+ автоматически инициализирует глобальный генератор
  • rand.Int() — любое большое число
  • rand.Intn(n) — число от 0 до n-1
  • rand.Float64() — вещественное от 0.0 до 1.0
  • min + rand.Intn(max-min+1) — целое в диапазоне [min, max]
  • Локальный генератор — для детерминированных тестов
  • rand.Seed() устарела — не используйте
  • [0, n) — полуинтервал (n не включается)
  • Не для криптографии — только для симуляций и игр

Итог: Пакет math/rand в Go предоставляет удобные инструменты для генерации псевдослучайных чисел. Начиная с Go 1.20, глобальный генератор автоматически инициализируется случайным значением, что избавляет от необходимости вызывать rand.Seed(). Для генерации целых чисел используйте rand.Int() и rand.Intn(n), для вещественных — rand.Float64() и rand.Float32(). Для создания чисел в произвольных диапазонах применяйте математические формулы. Если нужна детерминированность (для тестирования или отладки), создавайте локальный генератор с фиксированным seed через rand.New(rand.NewSource(seed)). Помните, что math/rand не подходит для криптографических целей — для этого используйте crypto/rand.