Уникальные числа в Go: решение и тестирование

уникальные числа

В разработке на Go часто встречаются задачи на генерацию уникальных данных. Разберем, как написать функцию, которая возвращает слайс уникальных чисел, и что важнее — как правильно подойти к ее тестированию.

Постановка задачи

  1. Написать функцию, которая принимает число n и возвращает слайс размера n с уникальными числами.
  2. Продумать стратегию тестирования этой функции.

Решение

Вот одна из возможных реализаций с использованием множества на основе map:

package main

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

// GenerateUniqueNumbers возвращает слайс из n уникальных целых чисел.
// Использует map[int]struct{} для эмуляции множества.
func GenerateUniqueNumbers(n int) []int {
    if n <= 0 {
        return nil
    }

    uniqueSet := make(map[int]struct{})
    result := make([]int, 0, n) // предварительное выделение памяти
    rnd := rand.New(rand.NewSource(time.Now().UnixNano()))

    for len(uniqueSet) < n {
        num := rnd.Intn(1000) // генерируем числа от 0 до 999
        if _, exists := uniqueSet[num]; !exists {
            // пустая структура не занимает память
            uniqueSet[num] = struct{}{}
            result = append(result, num)
        }
    }
    return result
}

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

Детальный разбор

1. Проверка входных данных

if n <= 0 { 
    return nil
}
  • Обработаем некорректные значения
  • Вернем nil для отрицательных чисел и нуля

2. Инициализация структур данных

uniqueSet := make(map[int]struct{}) 
result := make([]int, 0, n)
  • uniqueSet — отслеживает уже сгенерированные числа
  • result — предварительно выделяем память под результирующий слайс

3. Генератор случайных чисел

rnd := rand.New(rand.NewSource(time.Now().UnixNano()))
  • Создадим новый генератор случайных чисел на основе текущего времени
  • Гарантирует разные последовательности при каждом запуске

4. Основной цикл генерации

for len(uniqueSet) < n { 
    num := rnd.Intn(1000) // генерируем числа от 0 до 999
    if _, exists := uniqueSet[num]; !exists { 
        // пустая структура не занимает память 
        uniqueSet[num] = struct{}{} 
        result = append(result, num) 
    }
}
  • Генерируем числа пока не наберем нужное количество
  • Проверяем уникальность через map
  • Добавляем только новые числа

Стратегия тестирования

Как можно подойти к тестированию этой функции? Стратегия включает несколько уровней проверки. Покажу ключевые примеры, а остальные лишь обозначу.

1. Базовые сценарии

Проверим корректность работы на граничных и простых значениях.

 
func TestGenerateUniqueNumbers_BasicCases(t *testing.T) {
    tests := []struct {
            name     string
            input    int
            expected int
    }{
           {"Positive Number", 5, 5}, 
           {"Zero", 0, 0}, 
           {"Negative Number", -1, 0},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            result := GenerateUniqueNumbers(tt.input)
            if len(result) != tt.expected {
                t.Errorf("Для входа %d ожидалась длина %d, получена %d", tt.input, tt.expected, len(result))
            }
        })
    }
}

2. Проверка уникальности

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

 
func TestGenerateUniqueNumbers_Uniqueness(t *testing.T) {
    n := 100
    result := GenerateUniqueNumbers(n)
    
    // проверим длину результата
    if len(result) != n {
        t.Fatalf("Ожидалась длина %d, получена %d", n, len(result))
    }
    seen := make(map[int]bool)
    for _, num := range result {
        seen[num] = true
    }

    if len(seen) != n {
        t.Errorf("Ожидалось %d уникальных чисел, получено %d. Результат: %v", n, len(seen), result)
    }
}

Другие важные аспекты стратегии

  • Детерминированное тестирование: Для проверки предсказуемости можно модифицировать функцию, чтобы она принимала rand.Source. Это позволит в тестах использовать фиксированный источник случайности и сравнивать результаты.
  • Тестирование производительности: С помощью go test -bench . можно оценить, как функция ведет себя с разным размером n (10, 1000, 10000).
  • Property-based тестирование: Используя библиотеку вроде testing/quick, можно проверить свойства функции (например, функция всегда возвращает слайс длины n для любого n > 0).
  • Тестирование в рамках ограничений: Что будет, если n будет очень большим, а диапазон генерации чисел rnd.Intn(1000) маленьким? Функция может войти в бесконечный цикл. Это тоже важно учесть.

Возможные улучшения и альтернативы

  • Метод перемешивания (Shuffle): Для некоторых случаев эффективнее создать последовательность чисел (например: 0, 1, 2, ... , m), а затем перемешать ее и взять первые n элементов. Это гарантирует уникальность и избавляет от коллизий.
  • Кастомизируемый диапазон: Хорошей идей будет добавить параметры min и max для генерации чисел в заданном диапазоне.

Ключевой вывод

Мы не только написали работающую функцию на Go, но и разработали для нее продуманную стратегию тестирования. Такой подход — с фокусом на проверку ключевых требований (уникальность, размер) и учетом граничных случаев — помогает создавать надежный и качественный код.

А как вы тестируете подобные функции? Сталкивались ли с подводными камнями при генерации уникальных данных? Поделитесь вашим опытом в комментариях.

Понравилась статья? Поделиться с друзьями:
Добавить комментарий

;-) :| :x :twisted: :smile: :shock: :sad: :roll: :razz: :oops: :o :mrgreen: :lol: :idea: :grin: :evil: :cry: :cool: :arrow: :???: :?: :!: