Avatar ·

Как правильно инициализировать генератор случайных чисел в Go?

Привет! Я новичок в Go и пытаюсь разобраться с генерацией случайных чисел. Я прочитал, что в Go есть пакет `math/rand`, но у меня возникла проблема: каждый раз при запуске программы я получаю одну и ту же последовательность чисел. Вот мой код: ```go package main import ( "fmt" "math/rand" ) func main() { for i := 0; i < 5; i++ { fmt.Println(rand.Intn(100)) } } ``` При каждом запуске вывод всегда одинаковый: `81, 87, 47, 59, 81`. Я ожидал, что числа будут меняться. Я слышал, что нужно как-то "seed" (инициализировать) генератор, но не понимаю, как это правильно сделать. Когда я пытаюсь использовать `rand.Seed(time.Now().UnixNano())`, я получаю предупреждение от линтера, что Seed устарел в Go 1.20. Как же правильно инициализировать генератор в современных версиях Go? Есть ли разница между `math/rand` и `crypto/rand`? Какой подход считается правильным для обычных задач (не криптографических)? Буду благодарен за подробное объяснение с примерами. Спасибо! <<>>

Правильная инициализация генератора случайных чисел в Go (современный подход)

Вы столкнулись с классической проблемой. Действительно, в Go 1.20 функция rand.Seed() была объявлена устаревшей (deprecated). Разработчики Go изменили подход: теперь генератор инициализируется автоматически при импорте пакета math/rand. Однако есть важный нюанс — автоматическая инициализация происходит один раз при старте программы, и если вы создаете несколько генераторов вручную, их нужно инициализировать самостоятельно.

Решение 1: Использовать глобальный генератор (рекомендуется для большинства задач)

В Go 1.20+ вам не нужно явно вызывать Seed. Глобальный генератор rand.Intn(), rand.Float64() и т.д. инициализируется автоматически случайным значением. Ваш код будет работать корректно без вызова Seed:

package main

import (
    "fmt"
    "math/rand"
)

func main() {
    for i := 0; i < 5; i++ {
        fmt.Println(rand.Intn(100))
    }
}

Теперь при каждом запуске вы будете получать разные числа. Если вы всё ещё видите предупреждение — обновите версию Go до 1.20 или выше.

Решение 2: Создать свой генератор (для тестов или воспроизводимости)

Если вам нужна воспроизводимая последовательность (например, для тестирования или отладки), создайте генератор с явным seed:

import (
    "fmt"
    "math/rand"
)

func main() {
    // Фиксированный seed для воспроизводимости
    rng := rand.New(rand.NewSource(42))
    for i := 0; i < 5; i++ {
        fmt.Println(rng.Intn(100))
    }
}

Для случайной инициализации собственного генератора используйте crypto/rand:

import (
    "crypto/rand"
    "encoding/binary"
    "fmt"
    "math/rand"
)

func main() {
    var seed int64
    binary.Read(rand.Reader, binary.LittleEndian, &seed)
    rng := rand.New(rand.NewSource(seed))
    for i := 0; i < 5; i++ {
        fmt.Println(rng.Intn(100))
    }
}

Решение 3: Использовать crypto/rand для криптографической стойкости

Если вам нужны непредсказуемые случайные числа (пароли, токены, ключи), используйте crypto/rand:

import (
    "crypto/rand"
    "fmt"
    "math/big"
)

func main() {
    for i := 0; i < 5; i++ {
        n, _ := rand.Int(rand.Reader, big.NewInt(100))
        fmt.Println(n)
    }
}

Разница между math/rand и crypto/rand

  • math/rand — быстрый, детерминированный генератор. Подходит для игр, симуляций, тестов. Не подходит для криптографии.
  • crypto/rand — медленный, но криптостойкий. Использует энтропию ОС. Подходит для паролей, ключей, токенов.

Рекомендации

  • Для 95% задач используйте глобальный math/rand без явного Seed.
  • Для тестов — создавайте генератор с фиксированным seed.
  • Для безопасности — используйте crypto/rand.
  • Не используйте time.Now().UnixNano() для seed — это может дать предсказуемые результаты при быстрых запусках.
<<>>

Альтернативный подход: используйте пакет "math/rand/v2" (экспериментальный)

В Go 1.22 появился новый пакет math/rand/v2, который решает многие проблемы старого API. Он автоматически инициализируется и предлагает более удобный интерфейс:

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

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

Обратите внимание: в v2 функция называется IntN, а не Intn. Пакет использует алгоритм ChaCha8, который быстрее и криптостойче старого. Если ваш проект на Go 1.22+, рекомендую переходить на v2.

Для микросервисов и высоконагруженных систем

Избегайте частого создания новых генераторов. Создайте один глобальный генератор в main и передавайте его через зависимости:

type Service struct {
    rng *rand.Rand
}

func NewService() *Service {
    return &Service{rng: rand.New(rand.NewSource(time.Now().UnixNano()))}
}

Это гарантирует, что seed будет уникальным для каждого запуска сервиса, а генератор будет использоваться эффективно.