Question
Why Go Does Not Have a Built-In Set Type: Understanding Sets with Maps in Go
Question
I am working on a Go exercise that requires a set-like data structure. I know I can define my own set type, but why does Go not include a built-in set?
Given that a set is a common and useful data structure, why did Go's designers choose not to add direct language support for it? In practice, how are sets typically represented and used in Go?
Short Answer
By the end of this page, you will understand why Go does not provide a built-in set type, how Go programmers usually model sets with map values, and what trade-offs this design choice creates. You will also learn how to build and use a simple set in Go for real programs.
Concept
In programming, a set is a collection of unique values. Unlike a slice or array, a set does not care about element order, and it does not allow duplicates.
Go does not include a dedicated built-in set type. Instead, Go programmers usually represent a set with a map, most commonly:
map[string]struct{}
or
map[int]bool
The idea is simple:
- The map key is the set element.
- The map value is just a placeholder.
- If a key exists in the map, the element is in the set.
For example:
seen := map[string]struct{}{}
seen["go"] = struct{}{}
seen["go"] = struct{}{} // still only one logical element
This works because map keys are unique.
Why this matters in real programming:
- You often need to remove duplicates.
- You may need fast membership checks like "have I seen this before?"
- Sets are useful in validation, permissions, graph traversal, search, caching, and data cleanup.
Why Go likely does this with maps instead of a dedicated built-in type:
- Go keeps the language and standard library small and simple.
- Maps already provide the main behavior needed for sets: unique keys and fast lookup.
- A separate built-in set type would overlap heavily with existing map functionality.
Mental Model
Think of a set like a coat-check list at an event.
- You only care whether a person's name is on the list.
- You do not care how many times someone tries to add the same name.
- You do not care about the order of names.
In Go, a map used as a set is like a clipboard where each name appears at most once.
- Key exists -> the item is in the set.
- Key missing -> the item is not in the set.
The value part of the map is not the important part. It is just a marker saying, "this key is present."
Syntax and Examples
Core syntax
A common Go set pattern is:
set := make(map[string]struct{})
Add an item
set["apple"] = struct{}{}
Check membership
_, exists := set["apple"]
fmt.Println(exists) // true
Remove an item
delete(set, "apple")
Beginner-friendly example
package main
import "fmt"
func main() {
colors := make(map[string]struct{})
colors["red"] = struct{}{}
colors["blue"] = struct{}{}
colors["red"] = {}{}
_, hasRed := colors[]
_, hasGreen := colors[]
fmt.Println(, hasRed)
fmt.Println(, hasGreen)
fmt.Println(, (colors))
}
Step by Step Execution
Consider this example:
package main
import "fmt"
func main() {
set := make(map[string]struct{})
set["go"] = struct{}{}
set["python"] = struct{}{}
set["go"] = struct{}{}
_, hasGo := set["go"]
_, hasJava := set["java"]
fmt.Println(len(set))
fmt.Println(hasGo)
fmt.Println(hasJava)
}
Step by step
-
set := make(map[string]struct{})- Creates an empty map.
- Right now, the set contains no elements.
-
set["go"] = struct{}{}- Adds
"go"to the set.
- Adds
-
set["python"] = struct{}{}- Adds
"python"to the set.
- Adds
Real World Use Cases
Sets appear in many practical Go programs, even when they are implemented with maps.
Removing duplicates from input
names := []string{"Ana", "Bob", "Ana", "Cara"}
unique := make(map[string]struct{})
for _, name := range names {
unique[name] = struct{}{}
}
Useful for:
- cleaning CSV data
- importing records
- processing API results
Fast membership checks
allowed := map[string]struct{}{
"admin": {},
"editor": {},
}
_, ok := allowed["admin"]
Useful for:
- permissions
- feature flags
- allowed configuration values
Tracking visited items
visited := make(map[int]struct{})
Useful for:
Real Codebase Usage
In real Go codebases, developers usually do not ask for a special built-in set type first. They ask what operations they need, then choose the simplest representation.
Common pattern: map as a set
seen := map[string]struct{}{}
This is common for internal logic where only a few operations are needed.
Wrapping a map in a custom type
If code uses set behavior in many places, teams often define a named type:
type StringSet map[string]struct{}
func (s StringSet) Add(value string) {
s[value] = struct{}{}
}
func (s StringSet) Has(value string) bool {
_, ok := s[value]
return ok
}
func (s StringSet) Remove(value string) {
delete(s, value)
}
This improves readability and gives the project a consistent API.
Validation and guard clauses
Sets are often used to validate values early:
var validStatuses = []{}{
: {},
: {},
: {},
}
{
_, ok := validStatuses[s]
ok
}
Common Mistakes
1. Using a nil map without initializing it
Broken code:
var set map[string]struct{}
set["go"] = struct{}{}
This panics because the map is nil.
Correct code:
set := make(map[string]struct{})
set["go"] = struct{}{}
2. Confusing map[T]bool with exact set semantics
This works:
set := make(map[string]bool)
set["go"] = true
But beginners sometimes do this:
if set["java"] {
fmt.Println("exists")
}
This cannot distinguish between:
- key not present
- key present with value
false
Comparisons
Common ways to represent a set in Go
| Representation | Example | Good for | Notes |
|---|---|---|---|
map[T]struct{} | map[string]struct{} | Most set use cases | Clear intent: only presence matters |
map[T]bool | map[string]bool | Simple membership checks | Slightly less explicit because value can be true or false |
| Slice with manual checks | []string | Very small collections | Slower membership checks because you scan linearly |
| Custom set type wrapping map | type StringSet map[string]struct{} | Reusable project code |
Cheat Sheet
// Create a set of strings
set := make(map[string]struct{})
// Add
set["apple"] = struct{}{}
// Check membership
_, ok := set["apple"]
// Remove
delete(set, "apple")
// Size
n := len(set)
// Iterate
for item := range set {
fmt.Println(item)
}
Rules to remember
- Go has no built-in
settype. - Use
map[T]struct{}for a typical set. - Map keys are unique, so duplicates are naturally ignored.
- Always initialize the map with
makebefore writing to it. - Map iteration order is not guaranteed.
- Map keys must be comparable.
Common key types
map[string]struct{}
map[int]struct{}
map[rune]struct{}
Membership pattern
FAQ
Why does Go not have a built-in set type?
Because Go keeps its core types small and relies on maps to cover set behavior efficiently. A map already provides unique keys and fast membership checks.
How do you create a set in Go?
Usually with a map, such as:
set := make(map[string]struct{})
Why use struct{} instead of bool?
Because the value is not important in a set. struct{} clearly signals that only key presence matters.
Can I use a slice as a set?
You can, but membership checks are slower because you must search through the slice manually.
Does Go support generic sets?
Go does not provide a built-in generic set type in the language itself. A project can define its own generic set type, but many programs still use maps directly.
Are map-based sets ordered?
No. If you need sorted or stable order, copy the keys into a slice and sort them.
Can any type be a set element in a map-based set?
Only types that are valid map keys. In Go, map keys must be comparable.
Mini Project
Description
Build a small Go program that removes duplicate words from a sentence using a map-based set. This demonstrates the most common real use of a set in Go: tracking which values have already been seen.
Goal
Create a program that keeps only the first occurrence of each word and prints the unique words in their original order.
Requirements
- Read a hardcoded slice of words.
- Use a map-based set to track words that have already appeared.
- Preserve the original order of first appearance.
- Print the final list of unique words.
Keep learning
Related questions
Blank Identifier Imports in Go: What `_` Means in an Import Statement
Learn what `_` means in a Go import, why blank identifier imports run package init code, and when to use them safely.
Check if a Value Exists in a Slice in Go
Learn how to check whether a value exists in a slice in Go, and why Go has no Python-style `in` operator for arrays or slices.
Concatenating Slices in Go with append
Learn how to concatenate two slices in Go using append and the ... operator, with examples, pitfalls, and practical usage.