Question

Why Go Does Not Have a Built-In Set Type: Understanding Sets with Maps in Go

godata-structuresset

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.

So the core Go idea is not "sets are unimportant." It is more like: the language already gives you a simple primitive that can express a set well enough in most cases.

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."

Take Quiz

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"] = struct{}{} // duplicate add, no effect on uniqueness

	_, hasRed := colors["red"]
	_, hasGreen := colors["green"]

	fmt.Println("Has red:", hasRed)
	fmt.Println("Has green:", hasGreen)
	fmt.Println("Number of unique colors:", len(colors))
}

What this shows

  • map[string]struct{} stores unique strings.
  • Adding the same key again does not create a duplicate.
  • Membership is checked with the value, ok map lookup pattern.
  • len(colors) gives the number of unique items in the set.

Why struct{} is common

struct{} is an empty struct. It takes no meaningful storage for fields and is often used when you only care about key presence.

visited := make(map[int]struct{})
visited[42] = struct{}{}

If you use map[int]bool, that also works, but struct{} better communicates: "the value does not matter."

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

  1. set := make(map[string]struct{})

    • Creates an empty map.
    • Right now, the set contains no elements.
  2. set["go"] = struct{}{}

    • Adds "go" to the set.
  3. set["python"] = struct{}{}

    • Adds "python" to the set.
  4. set["go"] = struct{}{}

    • Attempts to add "go" again.
    • Because map keys are unique, nothing new is added.
    • The set still contains only "go" and "python".
  5. _, hasGo := set["go"]

    • Looks for the key "go".
    • It exists, so hasGo becomes true.
  6. _, hasJava := set["java"]

    • Looks for "java".
    • It does not exist, so hasJava becomes false.
  7. fmt.Println(len(set))

    • Prints 2 because there are two unique elements.
  8. fmt.Println(hasGo)

    • Prints true.
  9. fmt.Println(hasJava)

    • Prints false.

Final output

2
true
false

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:

  • graph traversal
  • web crawling
  • avoiding repeated work in loops or recursive code

Blocking duplicates in processing pipelines

If an event ID has already been processed, store it in a set so it is not handled twice.

Comparing collections

You can use sets to find:

  • common items
  • missing items
  • extra items
Take Quiz

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 = map[string]struct{}{
	"pending": {},
	"done":    {},
	"failed":  {},
}

func isValidStatus(s string) bool {
	_, ok := validStatuses[s]
	return ok
}

This works well with guard clauses:

if !isValidStatus(status) {
	return fmt.Errorf("invalid status: %s", status)
}

Deduplication during transformation

When processing slices, developers often combine loops with a set:

func UniqueStrings(items []string) []string {
	seen := make(map[string]struct{})
	result := make([]string, 0, len(items))

	for _, item := range items {
		if _, ok := seen[item]; ok {
			continue
		}
		seen[item] = struct{}{}
		result = append(result, item)
	}

	return result
}

This pattern is simple, efficient, and very common.

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

Safer pattern:

_, ok := set["java"]
fmt.Println(ok)

3. Expecting insertion order

Maps in Go are not ordered. If you iterate over a map, the order is not guaranteed.

Broken assumption:

for item := range set {
	fmt.Println(item) // do not expect stable order
}

If you need sorted output, collect keys into a slice and sort them.

4. Thinking duplicate assignment creates duplicates

set["go"] = struct{}{}
set["go"] = struct{}{}
fmt.Println(len(set)) // 1, not 2

A set stores unique items only.

5. Forgetting that only comparable types can be map keys

Broken code:

set := make(map[[]int]struct{})

This does not compile because slices cannot be map keys.

Use comparable key types such as:

  • string
  • int
  • bool
  • structs whose fields are all comparable

6. Overengineering a set too early

Sometimes a small map is enough. You do not always need a full custom set package or generic abstraction unless your project benefits from it.

Comparisons

Common ways to represent a set in Go

RepresentationExampleGood forNotes
map[T]struct{}map[string]struct{}Most set use casesClear intent: only presence matters
map[T]boolmap[string]boolSimple membership checksSlightly less explicit because value can be true or false
Slice with manual checks[]stringVery small collectionsSlower membership checks because you scan linearly
Custom set type wrapping maptype StringSet map[string]struct{}Reusable project codeImproves readability and encapsulation

map[T]struct{} vs map[T]bool

Featuremap[T]struct{}map[T]bool
Expresses set intent clearlyYesUsually
Stores meaningful valueNoYes/Maybe
Membership check with ok idiomYesYes
Risk of confusion with false valuesNoYes

Set vs slice

OperationSet with mapSlice
Add itemFastFast append
Check if item existsUsually fastMust scan elements
Keep insertion orderNoYes
Allow duplicatesNoYes
Iterate in stable orderNoYes

The main trade-off is simple:

  • Use a set when uniqueness and membership checks matter.
  • Use a slice when order and duplicates matter.
Take Quiz

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 set type.
  • Use map[T]struct{} for a typical set.
  • Map keys are unique, so duplicates are naturally ignored.
  • Always initialize the map with make before 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

_, exists := set[value]

Alternative form

set := map[string]bool{}
set["apple"] = true

Prefer map[T]struct{} when the value is only a marker.

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.

Take Quiz

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

  1. Read a hardcoded slice of words.
  2. Use a map-based set to track words that have already appeared.
  3. Preserve the original order of first appearance.
  4. Print the final list of unique words.
Take Quiz

Keep learning

Related questions

View more