«Где мой 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]{