Avatar ·

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

Всем привет! Я только начал изучать Go и столкнулся с задачей: мне нужно генерировать случайные строки фиксированной длины, например, для создания временных паролей или идентификаторов сессий. Я попробовал использовать пакет `math/rand` и просто выбирать случайные символы из набора букв, но столкнулся с проблемой — строки получаются не совсем случайными, и иногда даже повторяются. Вот мой текущий код: ```go package main import ( "fmt" "math/rand" ) func main() { fmt.Println(randString(10)) } func randString(n int) string { var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") b := make([]rune, n) for i := range b { b[i] = letters[rand.Intn(len(letters))] } return string(b) } ``` Проблема в том, что при каждом запуске программы я получаю одну и ту же строку, если не вызывать `rand.Seed`. Я читал, что в Go 1.20 `math/rand` автоматически инициализируется, но всё равно не уверен, что это лучший подход. Кроме того, мне нужно, чтобы строка содержала не только буквы, но и цифры, а также была криптостойкой для использования в безопасности. Подскажите, как правильно генерировать случайные строки в Go? Есть ли стандартные решения или нужно писать свою функцию? Заранее спасибо! <<>>

Понимание проблемы

Ваш код почти правильный, но есть несколько нюансов. Во-первых, math/rand не является криптостойким генератором — он детерминирован и предсказуем, что не подходит для паролей или токенов. Во-вторых, до Go 1.20 требовался явный вызов rand.Seed(), иначе seed был фиксирован (0). Начиная с Go 1.20 seed устанавливается автоматически, но это всё ещё псевдослучайность. Для безопасных строк используйте crypto/rand.

Решение 1: Базовое с использованием math/rand (для небезопасных целей)

Если вам не нужна криптостойкость (например, для тестов или генерации ID, которые не должны быть секретными), используйте math/rand с явным seed из текущего времени. Вот улучшенная версия вашего кода:

package main

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

const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"

func init() {
    rand.Seed(time.Now().UnixNano())
}

func RandString(length int) string {
    b := make([]byte, length)
    for i := range b {
        b[i] = charset[rand.Intn(len(charset))]
    }
    return string(b)
}

func main() {
    fmt.Println(RandString(10))
}
  • charset — строка со всеми допустимыми символами (буквы + цифры).
  • init() — инициализация seed при старте программы.
  • rand.Intn() — возвращает случайное число от 0 до len(charset)-1.

Недостаток: math/rand небезопасен для криптографии.

Решение 2: Криптостойкое с использованием crypto/rand (рекомендуется)

Для паролей, токенов или любых данных, где важна безопасность, используйте crypto/rand. Он использует системный энтропийный пул и недетерминирован:

package main

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

const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"

func CryptoRandString(length int) (string, error) {
    b := make([]byte, length)
    for i := range b {
        idx, err := rand.Int(rand.Reader, big.NewInt(int64(len(charset))))
        if err != nil {
            return "", err
        }
        b[i] = charset[idx.Int64()]
    }
    return string(b), nil
}

func main() {
    str, err := CryptoRandString(10)
    if err != nil {
        panic(err)
    }
    fmt.Println(str)
}
  • crypto/rand.Reader — глобальный источник криптостойкой случайности.
  • rand.Int() — безопасно генерирует число в диапазоне [0, n).
  • Возвращает ошибку, если системный источник недоступен (крайне маловероятно).

Решение 3: Оптимизированное с пакетом math/rand/v2 (Go 1.22+)

Начиная с Go 1.22 появился пакет math/rand/v2, который быстрее и использует более качественный генератор. Пример:

package main

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

const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"

func RandStringV2(length int) string {
    b := make([]byte, length)
    for i := range b {
        b[i] = charset[rand.IntN(len(charset))]
    }
    return string(b)
}

func main() {
    fmt.Println(RandStringV2(10))
}

Обратите внимание: rand.IntN() — новый синтаксис, seed не требуется.

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

  • Для большинства случаев используйте crypto/rand — это правильно и безопасно.
  • Если производительность критична (миллионы генераций в секунду) и безопасность не нужна — math/rand/v2.
  • Не используйте math/rand без seed в старых версиях Go.
  • Добавьте в charset специальные символы (!@#$%^&*), если требуется сложность пароля.
<<>>

Альтернативный подход: Использование готовых библиотек

Если вы не хотите писать свою реализацию, можно воспользоваться популярными сторонними пакетами, которые уже решают эту задачу с учётом всех нюансов. Например, github.com/thanhpk/randstr или github.com/gofrs/uuid (для UUID).

Пример с randstr:

package main

import (
    "fmt"
    "github.com/thanhpk/randstr"
)

func main() {
    // Строка из букв и цифр длиной 12
    fmt.Println(randstr.String(12))
    // Только цифры
    fmt.Println(randstr.Digit(6))
    // Только буквы нижнего регистра
    fmt.Println(randstr.Lower(8))
}

Библиотека использует crypto/rand внутри, поэтому безопасна. Это удобно, если в проекте уже есть зависимости или вам нужно больше опций (например, исключить похожие символы, как "l" и "1").

Минус: Добавляет внешнюю зависимость. Для простых проектов лучше написать свою функцию.