Maps
What Is a Map?β
A map is a hash-table-based data structure that stores key-value pairs. Values are looked up by key in O(1) time, making maps ideal for fast lookups.
Go maps are reference types β passing a map to a function shares the underlying hash table. Writing to a nil map causes a runtime panic.
Declaring and Initialising Mapsβ
package main
import "fmt"
func main() {
// 1. nil map (reads are safe, writes panic)
var m1 map[string]int
fmt.Println(m1 == nil) // true
fmt.Println(m1["key"]) // 0 β reading returns the zero value
// m1["key"] = 1 // runtime panic!
// 2. make (recommended)
m2 := make(map[string]int)
m2["alice"] = 90
m2["bob"] = 85
fmt.Println(m2) // map[alice:90 bob:85]
// 3. Map literal
m3 := map[string]int{
"alice": 90,
"bob": 85,
"carol": 78,
}
fmt.Println(m3)
// 4. Capacity hint (performance optimisation)
m4 := make(map[string]int, 100)
_ = m4
}
CRUD Operationsβ
package main
import "fmt"
func main() {
scores := map[string]int{
"alice": 90,
"bob": 85,
}
// Create / Update β same syntax
scores["carol"] = 78 // new key
scores["alice"] = 95 // update existing key
// Read
fmt.Println(scores["alice"]) // 95
fmt.Println(scores["none"]) // 0 (non-existent key β zero value)
// Check key existence (comma-ok idiom)
val, ok := scores["bob"]
if ok {
fmt.Printf("bob's score: %d\n", val)
}
_, exists := scores["dave"]
fmt.Println("dave exists:", exists) // false
// Delete
delete(scores, "bob")
fmt.Println(scores) // map[alice:95 carol:78]
// Deleting a non-existent key is a no-op
delete(scores, "nobody")
}
Iterating Over a Mapβ
Map iteration order is random on every run. Sort the keys if order matters.
package main
import (
"fmt"
"sort"
)
func main() {
m := map[string]int{
"banana": 3,
"apple": 5,
"cherry": 2,
}
// Default iteration β order not guaranteed
for k, v := range m {
fmt.Printf("%s: %d\n", k, v)
}
fmt.Println("--- sorted ---")
// Sorted iteration
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
fmt.Printf("%s: %d\n", k, m[k])
}
// apple: 5
// banana: 3
// cherry: 2
}
Nested Maps and Complex Value Typesβ
package main
import "fmt"
func main() {
// Map with slice values (grouping)
groups := map[string][]string{
"backend": {"alice", "bob"},
"frontend": {"carol", "dave"},
}
groups["backend"] = append(groups["backend"], "eve")
fmt.Println(groups)
// map[backend:[alice bob eve] frontend:[carol dave]]
// Nested maps β inner maps must be initialised explicitly
nested := map[string]map[string]int{}
nested["team1"] = map[string]int{"alice": 90, "bob": 85}
nested["team2"] = map[string]int{"carol": 78}
fmt.Println(nested["team1"]["alice"]) // 90
// Bad pattern β writing to uninitialised inner map panics
// var bad map[string]map[string]int
// bad["k1"]["k2"] = 1 // panic!
}
Maps with Struct Valuesβ
In real projects, struct values in maps are very common.
package main
import "fmt"
type Student struct {
Name string
Score int
Grade string
}
func toGrade(score int) string {
switch {
case score >= 90:
return "A"
case score >= 80:
return "B"
case score >= 70:
return "C"
default:
return "F"
}
}
func main() {
students := map[string]Student{
"s001": {Name: "Alice", Score: 92},
"s002": {Name: "Bob", Score: 78},
"s003": {Name: "Carol", Score: 85},
}
// Updating a struct field: must extract, modify, and replace
s := students["s001"]
s.Grade = toGrade(s.Score)
students["s001"] = s
for id, st := range students {
fmt.Printf("[%s] %s: %d (%s)\n", id, st.Name, st.Score, st.Grade)
}
}
Note:
students["s001"].Grade = "A"does not compile β map values are not addressable. Extract the value, modify it, and put it back. Using a pointer (map[string]*Student) allows direct field modification.
Concurrency Cautionβ
The built-in map is not goroutine-safe. Concurrent reads and writes cause a runtime panic.
package main
import (
"fmt"
"sync"
)
type SafeMap struct {
mu sync.RWMutex
m map[string]int
}
func NewSafeMap() *SafeMap {
return &SafeMap{m: make(map[string]int)}
}
func (sm *SafeMap) Set(key string, val int) {
sm.mu.Lock()
defer sm.mu.Unlock()
sm.m[key] = val
}
func (sm *SafeMap) Get(key string) (int, bool) {
sm.mu.RLock()
defer sm.mu.RUnlock()
v, ok := sm.m[key]
return v, ok
}
func main() {
sm := NewSafeMap()
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Add(1)
go func(n int) {
defer wg.Done()
key := fmt.Sprintf("key%d", n%10)
sm.Set(key, n)
}(i)
}
wg.Wait()
if v, ok := sm.Get("key0"); ok {
fmt.Println("key0:", v)
}
// sync.Map β goroutine-safe, optimised for read-heavy workloads
var syncMap sync.Map
syncMap.Store("hello", 42)
if val, ok := syncMap.Load("hello"); ok {
fmt.Println(val.(int)) // 42
}
syncMap.Range(func(k, v any) bool {
fmt.Printf("%v: %v\n", k, v)
return true
})
}
Practical Example β Word Frequency Counterβ
package main
import (
"fmt"
"sort"
"strings"
)
type WordCount struct {
Word string
Count int
}
func topN(text string, n int) []WordCount {
freq := make(map[string]int)
for _, word := range strings.Fields(strings.ToLower(text)) {
word = strings.Trim(word, ".,!?;:")
if word != "" {
freq[word]++
}
}
counts := make([]WordCount, 0, len(freq))
for word, count := range freq {
counts = append(counts, WordCount{word, count})
}
sort.Slice(counts, func(i, j int) bool {
if counts[i].Count != counts[j].Count {
return counts[i].Count > counts[j].Count
}
return counts[i].Word < counts[j].Word
})
if n > len(counts) {
n = len(counts)
}
return counts[:n]
}
func main() {
text := "go is great go is fast go is simple and go is powerful"
for _, wc := range topN(text, 5) {
fmt.Printf("%-10s %d\n", wc.Word, wc.Count)
}
// go 4
// is 4
// and 1
// fast 1
// great 1
}