Start learning free

LESSON 236 OF 338 · HASH TABLE

Hash Sets in Go: map[T]bool and map[T]struct{}

Lesson Read the full explanation

Hash Sets are collections of unique items i.e no element can be repeated.

Hash Sets were designed to give us a way of adding and looking for unique values in a collection in a quick manner.

The values in a Hash Set can be either simple primitives like strings or integers as well as more complex object types like object literals or arrays.

In Go, Hash Sets are typically implemented using the built-in map type with empty struct values (map[T]struct{}).


Creation:

To create a new empty set that holds strings, you use the following syntax:

mySet := map[string]struct{}{}

Creating an empty Set takes O(1) time.


Constructing from iterable

The Set constructor also accepts an optional iterable object. If you pass an iterable object to the Set constructor, all the unique elements of the object will be added to the new set:

elements := []string{"b", "c", "c", "d"}
mySet := make(map[string]struct{})
for _, e := range elements {
    mySet[e] = struct{}{}
}

// mySet contains {"b", "c", "d"}

This takes O(n) time, where n is the number of elements in the iterable object.


Adding elements:

To add an element to the set, you assign it as a key with an empty struct value:

mySet := map[string]struct{}{"b": {}, "c": {}, "d": {}}

// Adding new elements
mySet["x"] = struct{}{}

// Adding an existing element
mySet["d"] = struct{}{} // nothing happens

// mySet contains {"b", "c", "d", "x"}

The Set first checks if that element already exists and if so, it does nothing and also doesn't raise an error.

Adding an element to a Set takes O(1) time.


Checking if a value exists:

To check if a set has a specific element, you use the comma-ok idiom which returns true if the set contains the element, otherwise, it returns false.

mySet := map[string]struct{}{"b": {}, "c": {}, "d": {}}

_, ok := mySet["c"]
fmt.Println(ok) // prints true since 'c' exists (remember that set keeps unique values)

_, ok = mySet["x"]
fmt.Println(ok) // prints false since it does not exist in the set

Checking if a value exists in a Set takes O(1) time.


Removing elements:

To delete a specified element from a set, you use the delete() built-in function:

mySet := map[string]struct{}{"b": {}, "c": {}, "d": {}}

// Deleting elements
delete(mySet, "c")

// Deleting a non-existent element
delete(mySet, "z") // does nothing

// mySet contains {"b", "d"}

The Set first checks if that key exists and if not, it does nothing and also doesn't raise an error, so it's safe to delete non-existing elements.

Removing an element from a Set takes O(1) time.


Iterating over the Set values:

If we want to iterate over values of the set, we can use the for loop along with the range keyword:

mySet := map[string]struct{}{"b": {}, "c": {}, "d": {}}

for val := range mySet {
    fmt.Println(val)
}

// This will print the following:
// d
// c
// b

Iterating over a map takes O(n) time.

Notice the order is not the same as we initiated. map keeps the data in random order


Space Complexity

A Set uses O(n) space, where n is the number of elements existing in the Set.


Assignment
Follow the Coding Tutorial and let's play with some Hash Sets.


Hint
Look at the examples above if you get stuck.


STEP 1 OF 11
    Ctrl + Enter to check

    GO · HASH TABLE

    Hash Set in Go, explained

    What a hash set is

    A hash set holds unique items and answers "is this item in the set?" in O(1) time on average. Adding an item that is already present changes nothing. Sets fit tasks such as removing duplicates or remembering which values have been visited.

    Go has no built-in set type. The usual substitute is a map whose keys are the set's elements and whose values carry no information. Two spellings are common:

    • map[string]bool: an element is in the set when its value is true. Reading an absent key gives false, so seen[x] works directly as a membership test.
    • map[string]struct{}: the value type is the empty struct, which occupies no memory. Membership is tested with the comma-ok form.

    Which spelling to choose is mostly taste. The bool version reads more naturally in conditions. The struct{} version states clearly that the values mean nothing, and it cannot be misused by storing false. Either way the lookup cost is the same, and the keys must be comparable types such as strings, numbers or structs of those.

    Operations

    Creating and adding

    mySet := map[string]struct{}{}
    mySet["x"] = struct{}{}
    mySet["x"] = struct{}{}
    fmt.Println(len(mySet))
    
    1
    

    The second assignment overwrites the same key, so the set still has one element. In struct{}{}, the first struct{} is the type and the second pair of braces builds a value of it. Building a set from a slice is a loop that adds each element, O(n) overall.

    Checking membership

    _, ok := mySet["x"]
    fmt.Println(ok)
    
    true
    

    With a map[T]bool the same test is just if seen["x"] { ... }. Absent keys read as false, and the read does not add them.

    Removing

    delete(mySet, "x") removes an element. Deleting an absent element does nothing, so it is safe to call without checking.

    Iterating

    for val := range mySet visits every element in O(n) time, in no guaranteed order. The order can change between runs. If the output must be predictable, record the elements in a slice as you add them, which is what the exercise does. A set of n elements uses O(n) space.

    The lesson's exercise

    The exercise builds a set of fruits that keeps its insertion order, using a map[string]bool named seen for membership and a slice named fruits for order.

    1. Start with data containing a duplicate: fruitsArray := []string{"apples", "bananas", "apples"}.
    2. Declare the two structures, both empty: seen and fruits.
    3. Loop over fruitsArray. For each fruit, !seen[fruit] is true only the first time the fruit appears. Then mark it in seen and append it to fruits. The second "apples" fails the test and is skipped.
    4. Add "oranges" with the same check-then-add pattern.
    5. Test for "apples" with if seen["apples"] and print "We have apples".
    6. Delete "bananas" from seen only. The slice still holds it.
    7. Print what remains. Loop over fruits and print each fruit that seen still contains. The map decides what is present; the slice supplies the order.
    fruitsArray := []string{"apples", "bananas", "apples"}
    
    seen := map[string]bool{}
    fruits := []string{}
    for _, fruit := range fruitsArray {
        if !seen[fruit] {
            seen[fruit] = true
            fruits = append(fruits, fruit)
        }
    }
    
    if !seen["oranges"] {
        seen["oranges"] = true
        fruits = append(fruits, "oranges")
    }
    
    if seen["apples"] {
        fmt.Println("We have apples")
    }
    
    delete(seen, "bananas")
    
    for _, fruit := range fruits {
        if seen[fruit] {
            fmt.Println(fruit)
        }
    }
    

    The output is:

    We have apples
    apples
    oranges
    

    Two details matter. append may allocate a new backing array, so its result must be assigned back: fruits = append(fruits, fruit). And the program needs the fmt import.

    Common mistakes

    • Writing to a nil map. var seen map[string]bool is nil, and seen["a"] = true panics. Create it with map[string]bool{} or make.
    • Relying on iteration order. Ranging over a map in tests that compare printed output gives flaky results.
    • Setting a bool value to false instead of deleting. The key stays in the map, len still counts it, and range still visits it. Use delete to remove an element.
    • Forgetting that deleting from one structure does not touch the other. The exercise's slice keeps "bananas" after the delete, which is why the final loop checks seen.

    Key takeaways

    • A Go set is a map: map[T]struct{} or map[T]bool.
    • Add, test and delete are O(1) on average; iteration is O(n) and unordered.
    • Adding an existing element is harmless; deleting an absent one is harmless.
    • Pair the set with a slice when you need unique items in a stable order.
    Keep going with GoAbout 340 step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free