Как сгенерировать случайную строку фиксированной длины в Go?
Проблема: детерминированность и безопасность
Ваш код страдает от двух основных проблем:
- Детерминированность:
math/randбез инициализации seed всегда даёт одинаковую последовательность. Даже сtime.Now().UnixNano()при быстрых запусках seed может совпасть. - Отсутствие криптостойкости:
math/randпредсказуем и не подходит для токенов, паролей или ключей.
Решение 1: Простая генерация (некриптостойкая)
Для некритичных задач (например, случайные имена в тестах) используйте math/rand с правильным seed:
package main
import (
"fmt"
"math/rand"
"time"
)
const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
func init() {
rand.Seed(time.Now().UnixNano())
}
func RandString(n int) string {
b := make([]byte, n)
for i := range b {
b[i] = letterBytes[rand.Intn(len(letterBytes))]
}
return string(b)
}
func main() {
fmt.Println(RandString(10))
}
Важно: init() гарантирует, что seed будет установлен до вызова main(). Однако это всё ещё не подходит для безопасности.
Решение 2: Криптостойкая генерация (рекомендуется)
Для паролей, токенов, API-ключей используйте crypto/rand:
package main
import (
"crypto/rand"
"fmt"
"math/big"
)
const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
func CryptoRandString(n int) (string, error) {
b := make([]byte, n)
for i := range b {
idx, err := rand.Int(rand.Reader, big.NewInt(int64(len(letterBytes))))
if err != nil {
return "", err
}
b[i] = letterBytes[idx.Int64()]
}
return string(b), nil
}
func main() {
str, err := CryptoRandString(10)
if err != nil {
panic(err)
}
fmt.Println(str)
}
Почему это лучше:
crypto/randиспользует системный энтропийный пул (например,/dev/urandomв Linux).- Невозможно предсказать результат даже при знании предыдущих значений.
- Не требует seed — безопасность гарантируется ОС.
Решение 3: Оптимизация производительности
Если нужно генерировать миллионы строк, цикл с rand.Int() для каждого байта медленный. Используйте маску и остаток для ускорения:
package main
import (
"crypto/rand"
"fmt"
"math/big"
)
const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
const (
letterIdxBits = 6 // 6 бит для 64 символов (2^6 = 64)
letterIdxMask = 1 << letterIdxBits - 1 // Все 6 бит установлены
letterIdxMax = 63 / letterIdxBits // Сколько индексов можно получить из одного 63-битного числа
)
func CryptoRandStringFast(n int) (string, error) {
b := make([]byte, n)
cache, remain := big.NewInt(0), big.NewInt(0)
for i := 0; i < n; {
idx, err := rand.Int(rand.Reader, big.NewInt(1<<63))
if err != nil {
return "", err
}
for j := 0; j < letterIdxMax && i < n; j++ {
cache.Set(idx)
cache.Rsh(cache, uint(letterIdxBits*j))
cache.And(cache, big.NewInt(letterIdxMask))
if int(cache.Int64()) < len(letterBytes) {
b[i] = letterBytes[cache.Int64()]
i++
}
}
}
return string(b), nil
}
func main() {
str, err := CryptoRandStringFast(10)
if err != nil {
panic(err)
}
fmt.Println(str)
}
Как это работает: мы получаем одно большое случайное число и "выкусываем" из него по 6 бит, пока не наберём нужное количество символов. Это в ~5 раз быстрее, чем вызывать rand.Int для каждого байта.
Вывод
- Для игрушек/тестов —
math/randс seed. - Для продакшена — всегда
crypto/rand. - Для высокой производительности — используйте битовую маску.
Не забывайте обрабатывать ошибки от crypto/rand — в редких случаях системная энтропия может быть недоступна.
Альтернативный подход: использование пакета math/rand/v2 (Go 1.22+)
В Go 1.22 появился новый пакет math/rand/v2, который решает проблемы с seed и производительностью без crypto/rand:
package main
import (
"fmt"
"math/rand/v2"
)
const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
func RandStringV2(n int) string {
b := make([]byte, n)
for i := range b {
b[i] = letterBytes[rand.IntN(len(letterBytes))]
}
return string(b)
}
func main() {
fmt.Println(RandStringV2(10))
}
Преимущества:
- Автоматическая инициализация seed — не нужно вызывать
rand.Seed(). - Лучшая производительность за счёт внутренних оптимизаций.
- Более безопасный генератор (ChaCha8), чем старый
math/rand.
Недостаток: всё ещё не криптостойкий. Используйте crypto/rand для токенов, паролей и любых данных, где предсказуемость — это уязвимость. math/rand/v2 подходит для симуляций, игр, тестовых данных.
Если вы обновились до Go 1.22, это самый простой и быстрый вариант для некритичных задач.