37. crypto/rand и math/rand
Философия разделения: Скорость против Безопасности
В вычислительной технике понятие «случайности» обманчиво. Компьютер — детерминированная машина, и заставить процессор выдать число, не вытекающее из предыдущего состояния регистров, — фундаментальная инженерная дилемма. В Go генерация случайных чисел строго разделена на два пакета, отражающих ключевой компромисс системного программирования: производительность против непредсказуемости.
Пакет math/rand реализует быстрый алгоритмический генератор псевдослучайных чисел (PRNG). Он детерминирован, вычисляется исключительно на CPU и предназначен для симуляций, игр, случайной выборки данных и небезопасных идентификаторов. Начиная с Go 1.20, он автоматически инициализируется случайным seed-ом при старте программы, что упрощает работу, но не меняет его криптографическую слабость. В современном Go (Go 1.22+) появился пакет нового поколения math/rand/v2, использующий быстрые алгоритмы PCG и ChaCha8 с поддержкой дженериков rand.N[T](n).
Пакет crypto/rand — это интерфейс к генератору криптографически стойких случайных чисел (CSPRNG). Он опирается на пул энтропии ядра ОС, гарантируя, что следующий байт невозможно предсказать даже при знании всех предыдущих. Это единственный допустимый источник для токенов сессий, паролей, nonce, ключей шифрования и любых данных, где утечка паттернов ведет к взлому системы.
Under the hood. Алгоритмы и источники энтропии
Механика math/rand
Пакет math/rand (v1) исторически использует аддитивный алгоритм Lagged Fibonacci. В Go 1.20 в нем добавили лишь автосидирование глобального генератора при старте. А вот в современном math/rand/v2 (Go 1.22+) стандартным быстрым генератором стал PCG (Permuted Congruential Generator) с 128-битным состоянием (а также криптографически обоснованный ChaCha8), обеспечивающий высокое статистическое качество и скорость.
Глобальные функции (rand.Intn, rand.Float64) защищены внутренним sync.Mutex. Это делает их потокобезопасными, но создает точку конкуренции при высокой нагрузке. Создание локального источника rand.New(rand.NewSource(seed)) убирает мьютекс, но запрещает конкурентный доступ к одному экземпляру rand.Rand.
Механика crypto/rand
Пакет не реализует математику сам. Он выступает тонкой оберткой над системными вызовами:
- Linux:
getrandom()(современные ядра) или чтение/dev/urandom. - macOS/FreeBSD:
getentropy()илиsysctl(KERN_URND). - Windows:
BCryptGenRandom().
На современных системах пул энтропии ядра не «истощается». Он смешивает шумы аппаратных прерываний, таймеров, дисков и сетевого стека. Вызов crypto/rand инициирует переход в Ring 0, чтение буфера ядра и возврат данных. Это гарантирует криптографическую стойкость, но добавляет задержку syscall.
flowchart LR
classDef process fill:#1e293b,stroke:#38bdf8,stroke-width:1px,color:#f8fafc
classDef success fill:#064e3b,stroke:#34d399,stroke-width:1px,color:#f8fafc
classDef error fill:#7f1d1d,stroke:#f87171,stroke-width:1px,color:#f8fafc
classDef warning fill:#78350f,stroke:#fbbf24,stroke-width:1px,color:#f8fafc
classDef entry fill:#1e3a5f,stroke:#38bdf8,stroke-width:2px,color:#f8fafc
classDef aux fill:#334155,stroke:#64748b,stroke-width:1px,color:#f8fafc
classDef system fill:#312e81,stroke:#a78bfa,stroke-width:1px,color:#f8fafc
classDef data fill:#132d29,stroke:#2dd4bf,stroke-width:1px,color:#f8fafc
classDef network fill:#881337,stroke:#fb7185,stroke-width:1px,color:#f8fafc
classDef accent fill:#1e1b4b,stroke:#818cf8,stroke-width:1px,color:#f8fafc
A["Вызов math/rand/v2.IntN"]:::system --> B["CPU: Битовые операции PCG"]
B --> C["Обновление 128-бит состояния<br>Возврат результата"]
D["Вызов crypto/rand.Read"]:::system --> E["Syscall getrandom /dev/urandom"]
E --> F["Ring 0: Ядро ОС"]
F --> G["Смешивание пула энтропии<br>Прерывания, таймеры, диски"]
G --> H["Возврат непредсказуемых байтов<br>В Ring 3"]
Mechanical Sympathy: CPU-циклы, contention и аллокации
1. Цена конкуренции в math/rand
При использовании глобальных rand.* в тысячях горутин одновременно, sync.Mutex внутри рантайма становится bottleneck'ом. Горутины блокируются, планировщик переводит их в ожидание, растут latency.
Решение: Для высоконагруженных сценариев используйте пул локальных источников:
var randPool = sync.Pool{
New: func() interface{} {
// Go 1.20+ автоматически сидирует, но для детерминизма можно указать явно
return rand.New(rand.NewSource(time.Now().UnixNano()))
},
}
func fastRandomInt(n int) int {
r := randPool.Get().(*rand.Rand)
defer randPool.Put(r)
return r.Intn(n)
}
Это полностью устраняет блокировки, так как каждая горутина работает с изолированным состоянием PCG в L1-кэше своего треда.
2. Syscall overhead в crypto/rand
Каждый вызов rand.Read() требует переключения контекста. На современных CPU это ~100-300 наносекунд, что в 50-100 раз медленнее math/rand. Для генерации больших объемов данных (например, ключей или токенов) предпочтительно запрашивать буфер нужного размера одним вызовом, а не вызывать Read([]byte{b}) в цикле.
3. GC и аллокации
math/rand не аллоцирует память после инициализации. crypto/rand также минимален, но передача среза []byte в rand.Read() может вызвать escape analysis, если буфер создается внутри цикла. Всегда переиспользуйте буферы или создавайте их вне горячих путей.
Идиомы и паттерны для Production
Безопасная генерация токенов
Никогда не конвертируйте случайные байты в строку через fmt.Sprintf или string(). Используйте кодирование base64.URLEncoding для URL-безопасных идентификаторов:
func GenerateSecureToken(length int) (string, error) {
b := make([]byte, length)
if _, err := rand.Read(b); err != nil {
return "", fmt.Errorf("read crypto rand: %w", err)
}
// URL-safe, без padding, предсказуемая длина
return base64.URLEncoding.EncodeToString(b), nil
}
Перемешивание слайсов
rand.Shuffle использует глобальный источник. Для больших слайсов в конкурентной среде лучше реализовать собственный алгоритм Фишера-Йетса с локальным rand.Rand, избегая мьютекса.
Ловушки и вопросы с собеседований
| Сценарий | Проблема | Решение |
|---|---|---|
math/rand для JWT токенов |
Алгоритм детерминирован. Зная seed, атакер восстановит все прошлые и будущие токены. | Используйте строго crypto/rand. |
Общий rand.Rand между горутинами |
Внутреннее состояние PCG повреждается гонкой данных. Вызовет панику или зацикливание. | Создавайте экземпляр на горутину или используйте sync.Pool. |
/dev/random блокирует процесс |
На старых ядрах Linux /dev/random блокируется при низкой энтропии. |
Go использует /dev/urandom или getrandom, которые никогда не блокируются. |
crypto/rand.Read возвращает n < len |
В отличие от io.Reader, rand.Read всегда заполняет буфер полностью или возвращает ошибку. |
Не нужно проверять n в цикле. Достаточно if _, err := rand.Read(b); err != nil. |
| Auto-seed в Go 1.20+ | Разработчик вручную сидирует math/rand, ломая авто-инициализацию или делая её предсказуемой. |
Не вызывайте rand.Seed() в новых версиях. Удаляйте legacy-код. |
Сравнение с экосистемами других языков
| Язык | Пакет / Механизм | Особенности в сравнении с Go |
|---|---|---|
| Python | random vs secrets / os.urandom |
random использует Mersenne Twister. secrets обертка над os.urandom. Разделение есть, но random по умолчанию не потокобезопасен. Go math/rand безопаснее глобально, но требует явного пулинга для скорости. |
| Java | java.util.Random vs SecureRandom |
Random использует линейный конгруэнтный генератор. SecureRandom делегирует ОС. В Java SecureRandom может блокироваться на старых системах, в Go этого не происходит благодаря getrandom. |
| C++ | std::rand() vs std::random_device |
std::rand() считается устаревшим и плохим. random_device часто реализован через /dev/urandom, но стандарт не гарантирует криптографическую стойкость. Go дает строгие гарантии API. |
| Go | math/rand vs crypto/rand |
Четкое разделение, авто-сидирование в 1.20+, потокобезопасные глобальные вызовы, гарантия неблокируемости CSPRNG на всех ОС. |
Итог
math/rand— для скорости, симуляций и некритичных данных. Начиная с Go 1.20, автоматически сидируется. Глобальные функции потокобезопасны, локальные — нет.crypto/rand— строго для секретов, токенов и криптографии. Использует syscall к энтропии ядра, не блокируется, потокобезопасен.- Избегайте глобального
math/randв высоконагруженных конкурентных путях. Используйтеsync.Poolдля локальных источников, чтобы убрать contention мьютекса. - Никогда не пытайтесь «улучшить»
math/randхешированием или солью для безопасности. Детерминизм алгоритма делает предсказуемость неизбежной при известном seed. crypto/rand.Readвсегда заполняет буфер целиком или возвращает ошибку. Не проверяйтеnв цикле.
Разобравшись с источниками случайности, мы переходим к фундаментальным инструментам защиты данных. Как Go реализует криптографические примитивы, обеспечивает безопасность на уровне CPU и почему стандартная библиотека отказалась от устаревших алгоритмов? В следующей статье: 38. crypto. Хеши, HMAC, AES, RSA, TLS.