Идиома: мапа с пустыми значениями
Ключевого слова 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).