Menu

Множество (set) в Golang: map[T]struct{} и обобщённый Set

Встроенного типа множества в Go нет. Стандартная идиома это мапа с пустыми структурами в качестве значений. Добавление, проверка и удаление, объединение, пересечение и разность, а также небольшой обобщённый Set.

На этой странице есть исполняемые редакторы: меняйте, запускайте и сразу видите результат.

Идиома: мапа с пустыми значениями

Ключевого слова set в Go нет, и в стандартной библиотеке множества тоже нет. С задачей справляется мапа, ключи которой это элементы, а значения ничего не несут.

struct{} это тип пустой структуры, а struct{}{} его единственное значение. Оно занимает ноль байт, так что мапа хранит только ключи.

Действуют все правила мап: ключи должны быть сравнимыми, порядок обхода случайный, запись в nil-мапу вызывает панику, а конкурентная запись требует блокировки. Каждое правило разобрано на странице о мапах.

map[T]struct{} или map[T]bool

Другой распространённый вариант записи это map[T]bool. Он читается лучше, потому что отсутствующий ключ возвращает false:

seen := map[string]bool{}
seen["a"] = true
if seen["a"] { ... }

Компромиссы:

map[T]struct{}map[T]bool
Размер значения0 байт1 байт (плюс выравнивание)
Проверка принадлежности_, ok := s[k]s[k]
Неоднозначностьнетs[k] = false это третье состояние

Третья строка и есть настоящая причина, по которой многие кодовые базы предпочитают пустую структуру: с bool рано или поздно кто-нибудь напишет s[k] = false, и len(s) перестанет означать число элементов. Для маленьких множеств разница в памяти не важна.

Удаление дубликатов из слайса

Самое частое применение множества это удаление дубликатов. Этот код сохраняет первое вхождение каждого значения и порядок:

Вывод:

[b a c]
[3 1 2]
[1 2 3]

Объединение, пересечение, разность

Операции над множествами это несколько циклов. При проверке принадлежности к другому множеству обходите меньшее, ведь каждый поиск в среднем выполняется за константное время.

sorted нужна только для стабильного вывода. Если печатать множество обходом через range, порядок будет разным при каждом запуске. maps.Keys и slices.Sorted требуют Go 1.23.

Именованный тип вроде type set map[string]struct{} остаётся мапой: к нему так же обращаются по индексу, его так же обходят через range и удаляют из него через delete, а ещё к нему можно привязать методы.

Небольшой обобщённый Set

С дженериками (Go 1.18) один тип покрывает любой сравнимый тип элементов. Для большинства программ этого достаточно:

Оборачивание мапы в структуру прячет шум struct{}{} и гарантирует, что мапу создаёт конструктор, а это устраняет панику nil-мапы. Sorted не может быть методом: метод не может объявлять собственные параметры типа или ужесточать ограничение comparable у типа, а для сортировки нужен cmp.Ordered. Ограничения объясняет страница о дженериках.

Если нужно полноценное множество (потокобезопасные варианты, много операций), есть сторонние пакеты вроде github.com/deckarep/golang-set. В большинстве кода Go-разработчики используют идиому с мапой или тип строк на 30, как этот.

Множества структур

Элементом может быть любой сравнимый тип, включая структуры со сравнимыми полями. Поэтому проверки «видел ли я эту пару» пишутся напрямую:

type edge struct{ from, to string }
visited := map[edge]struct{}{}
visited[edge{"a", "b"}] = struct{}{}

Слайсы и мапы не могут быть элементами множества. Чтобы отслеживать уникальные слайсы, сначала превратите каждый в сравнимый ключ, например в массив фиксированного размера или строку, собранную через fmt.Sprint.

Частые ошибки

  • Забыли инициализировать. var s map[string]struct{} равна nil; первое же добавление вызовет панику.
  • Печать множества в расчёте на стабильный вывод. Сначала отсортируйте элементы.
  • map[T]bool с сохранённым false. Тогда len больше не считает элементы. Для удаления используйте delete.

Часто задаваемые вопросы

Есть ли в Go тип множества?

Нет. В стандартной библиотеке множества нет. Идиома это мапа, значения которой не несут информации: map[string]struct{}. Добавление: s[k] = struct{}{}, проверка принадлежности: _, ok := s[k], удаление: delete(s, k), размер: len(s).

Что использовать для множества в Go: map[T]bool или map[T]struct{}?

map[T]struct{} явно выражает намерение, а его значения занимают ноль байт. map[T]bool читается естественнее (if seen[x]), потому что отсутствующий ключ возвращает false. Оба варианта корректны; разница в памяти важна только для очень больших множеств. Выберите один и придерживайтесь его.

Как удалить дубликаты из слайса в Go?

Чтобы сохранить первое вхождение и порядок, пройдите циклом, отслеживая встреченные значения в map[T]struct{}, и добавляйте только новые. Если порядок не важен, отсортируйте и уберите соседние дубликаты: slices.Sort(s); s = slices.Compact(s).

Coddy programming languages illustration

Учитесь программировать с Coddy

НАЧАТЬ