> Как работает алгоритм генерации уникальных чисел с генератором и map в Go (Go)
Уровень: senior · Роль: backend · Язык: Go · Категория: Технические вопросы
Компании: Ozon
Стек: Go
> Пример ответа
Короткий ответ
Генератор уникальных чисел в Go - это функция, возвращающая замыкание, которое при каждом вызове возвращает следующее значение из последовательности. map здесь используется для отслеживания уже выданных чисел, чтобы гарантировать уникальность. Комбинация генератора и map позволяет создавать ленивые, потокобезопасные (при использовании мьютекса) последовательности без хранения всего набора в памяти. Ключевой момент - генератор инкапсулирует состояние, а map служит быстрым хранилищем для проверки дубликатов.
Подробное объяснение
Генератор в Go реализуется через замыкание: функция возвращает анонимную функцию, которая захватывает переменные из внешней области видимости. Эти переменные сохраняют состояние между вызовами. Для уникальности чисел используется map[int]struct{} или map[int]bool - проверка наличия ключа выполняется за O(1).
Алгоритм работы:
- Инициализируем
mapдля хранения выданных чисел. - Создаём счётчик или источник случайных чисел.
- При каждом вызове генератора:
- генерируем кандидата (инкремент или random);
- проверяем его наличие в
map; - если ключ уже существует - генерируем следующего кандидата;
- если ключа нет - добавляем в
mapи возвращаем значение.
Для последовательных уникальных чисел (1, 2, 3...) map избыточен - достаточно инкремента. Но для случайных или восстанавливаемых последовательностей map обязателен. Также map позволяет проверять уникальность в рамках нескольких генераторов или внешних источников данных.
Потокобезопасность достигается через sync.Mutex внутри замыкания. Без мьютекса конкурентные вызовы могут привести к гонке данных и дубликатам.
На практике
В реальном backend-коде такой генератор применяется для:
- создания уникальных ID в распределённых системах (но чаще используют snowflake или UUID);
- генерации номеров заказов в рамках одной ноды;
- тестирования - создание уникальных тестовых данных;
- пулов идентификаторов с последующим освобождением.
Важный trade-off: map растёт с каждым выданным числом, что увеличивает потребление памяти. Для больших последовательностей лучше использовать битовые карты или просто инкремент. Если нужна воспроизводимость - используйте math/rand с фиксированным seed.
Пример кода
GOpackage mainimport ("fmt""sync")// uniqueGenerator возвращает генератор уникальных чисел.// start - начальное значение, step - шаг.func uniqueGenerator(start, step int) func() int {mu := &sync.Mutex{}used := make(map[int]struct{})current := startreturn func() int {mu.Lock()defer mu.Unlock()for {candidate := currentcurrent += stepif _, exists := used[candidate]; !exists {used[candidate] = struct{}{}return candidate}}}}func main() {gen := uniqueGenerator(1, 1)fmt.Println(gen()) // 1fmt.Println(gen()) // 2fmt.Println(gen()) // 3}
Как отвечать на собеседовании
Начните с определения генератора как замыкания с состоянием. Затем объясните роль map - быстрая проверка дубликатов. Упомяните сложность O(1) для проверки и вставки. Обязательно скажите про потокобезопасность и мьютекс. Приведите пример, где такой подход оправдан, и где он избыточен (например, для последовательных чисел достаточно инкремента). Если спросят про альтернативы - упомяните sync/atomic для атомарного инкремента или каналы для генерации.
Что проверяет интервьюер
- понимание замыканий и захвата переменных;
- знание структур данных и их сложности;
- умение работать с конкурентностью;
- способность оценить trade-off между памятью и скоростью;
- практический опыт написания идиоматичного Go-кода.
Типичные ошибки
- использование
map[int]boolвместоmap[int]struct{}- лишнее выделение памяти; - отсутствие мьютекса при конкурентном доступе;
- бесконечный цикл при исчерпании диапазона (например, при переполнении int);
- попытка использовать генератор для криптографически уникальных значений - нужен
crypto/rand; - забывают, что
mapне гарантирует порядок - если порядок важен, используйте слайс или отдельный счётчик.
> Похожие задачи по Go
Когда можно закрыть канал при нескольких писателях и читателях в Go?
Какая основная идея воркерпула?
Для чего используется синтаксис срезов в Go
Зачем создавать слайс с длиной и емкостью вместо использования append в Go
> Похожие задачи по backend
Когда можно закрыть канал при нескольких писателях и читателях в Go?
Какая основная идея воркерпула?
Для чего используется синтаксис срезов в Go
Зачем создавать слайс с длиной и емкостью вместо использования append в Go
> ГОТОВЫ К СЛЕДУЮЩЕМУ СОБЕСЕДОВАНИЮ?
Запустите тренировочную сессию с ИИ и получите детальную обратную связь, чтобы увереннее проходить реальные интервью