Генерация псевдослучайных чисел: пакет 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
Важные моменты
- Нижняя граница: всегда
0 - Верхняя граница:
n - 1(не самn) - Паника: если
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
Логика
rand.Intn(6)→ возвращает0, 1, 2, 3, 4, 5+ 1→ сдвигаем диапазон на1вправо- Результат:
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: Добавление + 1 → 3 + 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)
Логика формулы
max - min— размер диапазона (без границ)+ 1— включение верхней границыrand.Intn(...)— случайное число от0до размера диапазона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)→00 + 18 = 18✅
Максимальное значение:
rand.Intn(48)→4747 + 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или10= решка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() в коде:
- Код писался для Go < 1.20 — устаревшая версия
- Код нужно обновить — удалить
rand.Seed() - Исключение: Явная потребность в детерминированности (используйте локальный генератор)
Восстановление старого поведения
Если по каким-то причинам нужно старое детерминированное поведение глобального генератора:
Способ 1: Явный вызов Seed
rand.Seed(1) // Фиксированная последовательность
Способ 2: Переменная окружения
GODEBUG=randautoseed=0 ./myprogram
Локальный генератор с фиксированным seed
Зачем нужен локальный генератор
Детерминированность — при одинаковом seed последовательность чисел повторяется при каждом запуске программы.
Применение:
- Тестирование — воспроизводимые тесты
- Отладка — воспроизведение ошибок
- Симуляции — повторяемые результаты
- Демонстрация — одинаковое поведение на разных машинах
Создание локального генератора
rng := rand.New(rand.NewSource(seed))
rand.NewSource(seed)— создаёт источник случайных чисел с заданным seedrand.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)
Логика формулы
rand.Float64()→ число от0.0до1.0(max - min)→ размер диапазона- Умножение → масштабирование от
0до размера диапазона 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.0→0.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)
Логика:
rand.Intn(5)→ целая часть[0, 4]rand.Intn(100)/100.0→ дробная часть[0.00, 0.99]- Сложение →
[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
Ключевые моменты
- Псевдослучайные числа — детерминированные, но выглядят случайно
- math/rand — пакет для работы с PRNG в Go
- Go 1.20+ — глобальный генератор инициализируется автоматически
- rand.Int() — большое случайное целое число
- rand.Intn(n) — число из полуинтервала [0, n)
- rand.Float64() — вещественное число из [0.0, 1.0)
- Локальный генератор —
rand.New(rand.NewSource(seed))для детерминированности - Формула [min, max] —
min + rand.Intn(max-min+1)для целых - Формула [min, max] —
min + rand.Float64()*(max-min)для вещественных - 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.