1. Two map patterns: set and counter
When you first see map, you want to use it literally: “key → value”. But in real code, map often acts as a universal bookkeeping mechanism: it can quickly answer the questions “have I seen this before?” and “how many times did this occur?”. That is why in Go people build lots of things on maps that other languages provide as separate data structures.
Imagine you are writing a small console utility: the user enters words/tags, and the program must (a) show a list of unique values and (b) count frequencies. You could store everything in a slice and linearly search for matches each time, but then the program will start feeling slow already at a few hundred elements. map solves both tasks elegantly, and today you will learn to read and write such code confidently.
For reference, here is a mini table of “what we want to get”:
| Task | What to store | What the type looks like in Go | How to “check” |
|---|---|---|---|
| A set of unique values (set) | presence fact | |
|
| Frequencies/counters | number of occurrences | |
or |
2. Set: map[T]struct{}
If you come from languages that have a built-in Set, Go may feel “stingy” at first: like, why do I have to do everything myself again? In reality Go is not stingy, it is minimalist: the basic map is so convenient that it makes an excellent set with no magic and no new rules. You get a data structure you already know how to create, iterate over, and clear.
The idea of a set is simple: we do not need to store a “value”, we only need the fact: “the element is present”. So as the value (V) we can use a type that stores nothing. Go has such a type: the empty struct struct{}. It has no fields, and it is often used as a marker “just because we need something there”.
Minimal set example
package main
import "fmt"
func main() {
set := make(map[string]struct{})
set["go"] = struct{}{}
set["map"] = struct{}{}
fmt.Println(len(set)) // 2
}
Here struct{}{} is an “empty value”. We put it there only because map requires a value, but the meaning is carried by the key.
Set operations: add/has/remove/size
When you see map[string]struct{} in someone else’s code, do not try to figure out “what is stored there”. What is stored there is “nothing”, and that is the whole point. Think of it as a “present/absent” table. Then set operations read almost like English: “add”, “check”, “remove”, “count”.
Here is a compact set of operations you will write all the time:
package main
import "fmt"
func main() {
tags := make(map[string]struct{})
tags["urgent"] = struct{}{} // add
_, ok := tags["urgent"] // has
delete(tags, "urgent") // remove
fmt.Println(ok, len(tags)) // true 0
}
Note: delete(tags, "urgent") is safe even if the key does not exist—it will simply do nothing. This is pure “zen”: fewer checks means fewer places to make a mistake.
Why not map[string]bool
A logical beginner question: “Why not implement a set as map[string]bool?” After all, you can store true/false. You can. But it is less expressive and sometimes riskier to read. The main problem: false is a normal value, and it is hard to distinguish from the situation “the key does not exist”.
Look at this example: it compiles, but logically it can confuse you a week later (or even two hours later).
package main
import "fmt"
func main() {
m := map[string]bool{"featureA": false}
fmt.Println(m["featureA"]) // false (key exists, value is false)
fmt.Println(m["featureB"]) // false (key does not exist, zero value)
}
Both times it prints false, but the meaning is different. Of course, you can use the ok idiom here too, but then the question becomes: why bool at all? If we are building a set, then map[string]struct{} tells the reader directly: “I am a set”, and reduces the chance that someone will write if m[x] { ... } and accidentally confuse “no key” with “key exists but is false”.
3. Counters and frequencies: map[string]int
After sets, the second most useful map pattern is a counter. It is so common that you see it in algorithm problems, log processing, analytics, data imports, and even in very everyday CLI programs. And here we benefit from what we already know: if a key is absent, reading returns the zero value.
For int, the zero value is 0. That means the expression counts[word]++ works even if word appears for the first time: first it reads 0, then adds 1, then stores it back in the map.
Mini example: frequencies
package main
import "fmt"
func main() {
words := []string{"go", "map", "go"}
counts := make(map[string]int)
for _, w := range words {
counts[w]++
}
fmt.Println(counts["go"]) // 2
fmt.Println(counts["java"]) // 0
}
And here it is important not to draw the wrong conclusion: 0 does not mean “the key definitely existed”. It means “either the key did not exist, or the value equals 0”. If you need to distinguish these cases, go back to the ok idiom.
When you still need ok
In a simple frequency counter, 0 is almost always fine. But there are situations where 0 is a real value. For example, you store a “current bonus balance”, and it can become zero after a write-off. Or you store “number of available attempts”, and 0 means “the key exists, but no attempts left”. In such cases it is useful to be able to distinguish the absence of a key.
A short example of the ok idiom for map[string]int:
package main
import "fmt"
func main() {
counts := map[string]int{"go": 0}
v1, ok1 := counts["go"]
v2, ok2 := counts["java"]
fmt.Println(v1, ok1) // 0 true
fmt.Println(v2, ok2) // 0 false
}
This check is especially helpful when you write code like “delete the key if the value became 0”. This is not a mandatory rule, but sometimes it keeps the data cleaner: fewer keys with zero values.
4. Example: a mini utility TagStats
Now we will put everything together into one small console program. It will be simple (we do not yet know a convenient way to read full lines with spaces, so we read “tokens”), but very straightforward: the user enters n, then n tags. The program prints the number of unique tags and the frequency of each tag.
Read input into []string
First, read input into []string. Note that we pre-allocate the slice capacity—you have already done this in the topic about make and append.
package main
import "fmt"
func main() {
var n int
fmt.Scan(&n)
tags := make([]string, 0, n)
for i := 0; i < n; i++ {
var t string
fmt.Scan(&t)
tags = append(tags, t)
}
fmt.Println(tags) // example: [go map go]
}
Build a set and a counter on top of the same data
Now on top of this data, build both the set of unique tags and the counter at the same time:
unique := make(map[string]struct{})
counts := make(map[string]int)
for _, t := range tags {
unique[t] = struct{}{}
counts[t]++
}
fmt.Println("unique:", len(unique)) // unique: 2
So far we only printed the count of unique tags. But we also want to print the frequencies.
Stable output: sort keys
Map iteration order is not guaranteed, so for a “nice and repeatable” result we collect the keys and sort them.
package main
import (
"fmt"
"sort"
)
func main() {
counts := map[string]int{"go": 2, "map": 1}
keys := make([]string, 0, len(counts))
for k := range counts {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
fmt.Println(k, counts[k])
}
// go 2
// map 1
}
If you put it all together, you get a “small application” that can truly be extended in later topics (for example, to store “tasks” and “task tags”). For now, without structs we are limited in data modeling, but we can already do useful analytics over strings.
Data flow diagram
A small data flow diagram (so your brain does not pretend it is a nil map):
flowchart TD
A[Read n tags] --> B[For each tag t]
B --> C["unique[t] = struct{}{}"]
B --> D["counts[t]++"]
C --> E["Unique = len(unique)"]
D --> F[Keys of counts -> sort -> print]
5. Key constraints: what you can put in map and set
Sometimes it feels like map is a magic bag where you can throw in any keys: strings, numbers, any stuff. But maps have an important constraint: a key must be something that can be compared and “hashed”. In plain language, that means the key must be comparable (support ==/!= without surprises). Therefore, for example, slices cannot be used as keys.
There are even scenarios where a key “looks like” it could accept different types, and then trying to use a non-comparable type can crash with a message like "hash of unhashable type []int". For this course it is enough to remember a simple rule: for the sets and counters from today’s lecture, use string or int as keys—and you will have a calm life without unexpected panics.
6. Typical mistakes
Mistake #1: implement a set as map[string]bool and check it via if set[x] { ... }.
Such code sometimes “works”, but it reads poorly and breaks easily if someone starts treating false as a meaningful value. If you truly want a set, map[string]struct{} expresses the intent better: presence is checked via _, ok := set[x], and it does not get confused with values.
Mistake #2: try to check key absence in a counter via if counts[k] == 0.
For frequencies this is often acceptable, but logically it means not “there is no key”, but “the value is currently 0”. If it matters to distinguish “never existed” from “existed but became 0”, use v, ok := counts[k]. The same rule saves you when you want to delete zero-valued keys and are not sure they were present at all.
Mistake #3: forget make and write to a nil map.
var set map[string]struct{} looks fine, and reading from such a map will not crash. But as soon as you do set["x"] = struct{}{}, the program will crash at runtime. So the practical rule is simple: if you are going to add elements, create the map via make(...) before the first write.
Mistake #4: expect a “nice order” when printing a counter via for k, v := range counts.
The iteration order of map is not a contract; it can change. Because of that, tests, output comparisons, and even just “eyeballing it” become painful. If you print results to a user and want stability, use the already familiar recipe: collect keys into a slice, sort them, print by keys.
Mistake #5: store something different in the set than what you check.
This sounds funny, but it happens often: you add strings to a set in one format (for example, with different case: "Go" and "go"), but you check in another format, and “for some reason it is not found”. If you build a set over strings, normalize them ahead of time (at least with strings.ToLower) and only then put them into map[string]struct{}.
GO TO FULL VERSION