"Where is my Set?" — The Cry of a Go Developer's Soul
\n\nIf you come to Go from Python, JavaScript, or Java, sooner or later you will ask yourself: "Where the hell is my beloved Set?" — and that's normal. On Stack Overflow, the topic "Golang why don't we have a set datastructure" has gathered thousands of views and dozens of heated discussions. Developers are used to a convenient set with unique elements, but Go only offers arrays, slices, and maps. In this article, we will explore the philosophy of Go's creators, learn how to emulate a Set, and even write our own minimalist implementation.
Why Didn't Go's Creators Add a Set to the Standard Library?
\n\nThe answer lies in the language's minimalism and pragmatism. The Go team (Rob Pike, Ken Thompson, Robert Griesemer) adhered to the principle: "If something can be done with one language construct, don't create extra entities." In Go, map is not just a dictionary, but a universal container. It already supports key uniqueness, which is the main property of a set.
The Philosophy of "Less Magic, More Clarity"
\n\nUnlike Python, where set is a separate type with its own methods (.add(), .union()), Go decided not to complicate the language. Why write a separate structure if map[T]struct{} does the same thing? This saves space in the standard library and lowers the entry barrier for beginners. The main thing is to learn how to correctly use a map as a set.
How to Emulate a Set in Go: Three Working Approaches
\n\nLet's move on to practice. Here's how you can implement a set using only Go's standard tools.
\n\n1. The Most Popular: map[type]struct{}
\n\nThis is the standard approach in the Go community. Why struct{}? Because an empty struct takes up no memory (0 bytes) — perfect as a marker for an element's "existence".
package main\n\nimport "fmt"\n\nfunc main() {\n // Create a set of integers\n set := make(map[int]struct{})\n\n // Add elements\n set[1] = struct{}{}\n set[2] = struct{}{}\n set[3] = struct{}{}\n set[1] = struct{}{} // Duplicate is ignored\n\n // Check for existence\n if _, exists := set[2]; exists {\n fmt.Println("2 is in the set")\n }\n\n // Deletion\n delete(set, 3)\n\n // Iteration\n for key := range set {\n fmt.Println(key)\n }\n}\n\nPros: zero memory allocation for values, simplicity. Cons: no .Add() or .Contains() methods — everything is done via map syntax.
2. Custom Type with Methods
\n\nIf you need a clean API, wrap the map in a struct:
\n\npackage main\n\nimport "fmt"\n\n// Set — generic set (Go 1.18+)\ntype Set[T comparable] struct {\n items map[T]struct{}\n}\n\n// NewSet creates a new Set\nfunc NewSet[T comparable]() *Set[T] {\n return &Set[T]{items: make(map[T]struct{})}\n}\n\n// Add adds an element\nfunc (s *Set[T]) Add(item T) {\n s.items[item] = struct{}{}\n}\n\n// Contains checks for presence\nfunc (s *Set[T]) Contains(item T) bool {\n _, ok := s.items[item]\n return ok\n}\n\n// Size returns the size\nfunc (s *Set[T]) Size() int {\n return len(s.items)\n}\n\nfunc main() {\n intSet := NewSet[int]()\n intSet.Add(10)\n intSet.Add(20)\n fmt.Println(intSet.Contains(10)) // true\n fmt.Println(intSet.Size()) // 2\n}\n\nThis already resembles a full-fledged set datastructure in Go. Generics appeared in version 1.18, so the code works on modern projects.
\n\n3. Using bool Instead of struct{}
\n\nSome use map[T]bool, but this wastes 1 byte per value. In most cases, the difference is negligible, but if you are writing a high-load service, it's better to stick with struct{}.