В разработке на Go часто встречаются задачи на генерацию уникальных данных. Разберем, как написать функцию, которая возвращает слайс уникальных чисел, и что важнее — как правильно подойти к ее тестированию.
Постановка задачи
- Написать функцию, которая принимает число
nи возвращает слайс размераnс уникальными числами. - Продумать стратегию тестирования этой функции.
Решение
Вот одна из возможных реализаций с использованием множества на основе 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, но и разработали для нее продуманную стратегию тестирования. Такой подход — с фокусом на проверку ключевых требований (уникальность, размер) и учетом граничных случаев — помогает создавать надежный и качественный код.
А как вы тестируете подобные функции? Сталкивались ли с подводными камнями при генерации уникальных данных? Поделитесь вашим опытом в комментариях.