В Go нет Set: почему и как жить без встроенного множества

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

«Где мой Set?» — крик души Go-разработчика

Если вы пришли в Go из Python, JavaScript или Java, то рано или поздно зададитесь вопросом: «А где же, чёрт возьми, мой любимый Set?» — и это нормально. На Stack Overflow тема «Golang why don't we have a set datastructure» собрала тысячи просмотров и десятки горячих обсуждений. Разработчики привыкли к удобному множеству с уникальными элементами, а Go предлагает только массивы, срезы (slices) и карты (maps). В этой статье мы разберём философию создателей Go, научимся эмулировать Set и даже напишем свою минималистичную реализацию.

Почему создатели Go не добавили Set в стандартную библиотеку?

Ответ кроется в минимализме и прагматизме языка. Команда Go (Роб Пайк, Кен Томпсон, Роберт Гриземер) придерживалась принципа: «Если что-то можно сделать одной конструкцией языка — не плоди сущности». В Go map — это не просто словарь, а универсальный контейнер. Он уже поддерживает уникальность ключей, что является основным свойством множества.

Философия «меньше магии — больше ясности»

В отличие от Python, где set — отдельный тип с собственными методами (.add(), .union()), в Go решили не усложнять язык. Зачем писать отдельную структуру, если map[T]struct{} делает то же самое? Это экономит место в стандартной библиотеке и снижает порог входа для новичков. Главное — научиться правильно использовать map как set.

Как эмулировать Set в Go: три рабочих подхода

Давайте перейдём к практике. Вот как можно реализовать множество, используя только стандартные средства Go.

1. Самый популярный: map[type]struct{}

Это стандартный подход в сообществе Go. Почему struct{}? Потому что пустая структура не занимает памяти (0 байт) — идеально для метки «существования» элемента.

package main

import "fmt"

func main() {
    // Создаём множество целых чисел
    set := make(map[int]struct{})

    // Добавляем элементы
    set[1] = struct{}{}
    set[2] = struct{}{}
    set[3] = struct{}{}
    set[1] = struct{}{} // Дубликат игнорируется

    // Проверка существования
    if _, exists := set[2]; exists {
        fmt.Println("2 есть в множестве")
    }

    // Удаление
    delete(set, 3)

    // Перебор
    for key := range set {
        fmt.Println(key)
    }
}

Плюсы: нулевое выделение памяти под значения, простота. Минусы: нет методов .Add() или .Contains() — всё делается через синтаксис map.

2. Собственный тип с методами

Если вам нужен чистый API, оберните map в структуру:

package main

import "fmt"

// Set — generic множество (Go 1.18+)
type Set[T comparable] struct {
    items map[T]struct{}
}

// NewSet создаёт новый Set
func NewSet[T comparable]() *Set[T] {
    return &Set[T]{items: make(map[T]struct{})}
}

// Add добавляет элемент
func (s *Set[T]) Add(item T) {
    s.items[item] = struct{}{}
}

// Contains проверяет наличие
func (s *Set[T]) Contains(item T) bool {
    _, ok := s.items[item]
    return ok
}

// Size возвращает размер
func (s *Set[T]) Size() int {
    return len(s.items)
}

func main() {
    intSet := NewSet[int]()
    intSet.Add(10)
    intSet.Add(20)
    fmt.Println(intSet.Contains(10)) // true
    fmt.Println(intSet.Size())       // 2
}

Это уже похоже на полноценный set datastructure в Go. Дженерики (generics) появились в версии 1.18, поэтому код работает на современных проектах.

3. Использование bool вместо struct{}

Некоторые используют map[T]bool, но это тратит 1 байт на значение. В большинстве случаев разница незаметна, но если вы пишете высоконагруженный сервис — лучше придерживаться struct{}.

set := make(map[string]bool)
set["apple"] = true
if set["apple"] {
    fmt.Println("apple есть")
}

Частые вопросы о Set в Go (с ответами)

Собрали самые популярные вопросы из обсуждений на Stack Overflow и форумах.

Вопрос 1: Почему нельзя просто использовать map[type]bool?

Можно, но это менее эффективно по памяти. bool занимает 1 байт, а struct{} — 0 байт. Для маленьких коллекций разница несущественна, но для миллионов элементов — это килобайты.

Вопрос 2: Есть ли планы добавить Set в стандартную библиотеку Go?

На данный момент (Go 1.22) — нет. Команда Go считает, что map покрывает 99% сценариев. Однако есть golang.org/x/exp с экспериментальным пакетом maps, но это не официальный Set.

Вопрос 3: Как сделать Set с кастомной структурой?

Используйте map[MyStruct]struct{}, но убедитесь, что структура сравнима (comparable). Если в структуре есть срез или map — Go не даст скомпилировать. В таком случае используйте хеш-функцию или строковый ключ.

Вопрос 4: А как насчёт производительности?

map[T]struct{} работает за O(1) в среднем — так же, как и встроенные Set в других языках. Хеш-таблицы Go очень быстрые, поэтому для большинства задач производительность не будет узким местом.

Когда стоит использовать Set, а когда — нет?

Set в Go (даже эмулированный) идеален для:

  • Удаления дубликатов из слайса
  • Проверки принадлежности (белый список IP, стоп-слова)
  • Математических операций (объединение, пересечение)

Не используйте Set, если:

  • Вам нужен порядок элементов — map не гарантирует порядок итерации
  • Нужны дубликаты — используйте slice
  • Работаете с очень маленькими коллекциями (1-5 элементов) — slice с линейным поиском может быть быстрее

Пишем свою библиотеку Set: пошаговый гайд

Давайте создадим полноценный модуль для работы с множествами. Это отличный способ понять, как работает Golang set datastructure изнутри.

Шаг 1: Базовая структура с методами

package set

// Set — generic множество
type Set[T comparable] struct {
    items map[T]struct{}
}

// New создаёт новый Set
func New[T comparable]() *Set[T] {
    return &Set[T]{                

Блоги

Рекомендаций по книгам