Map #
A map in Go is a hash table implementation — a data structure providing O(1) average access for get, set, and delete operations. Unlike slices, which store ordered elements with numeric indexes, maps store key-value pairs where the key can be almost any comparable type. Maps are very useful for fast lookups, data grouping, and counters — but there are some behaviors to understand: iteration is non-deterministic, they’re not thread-safe, and there’s an important gotcha when modifying structs stored inside a map.
Internally, Go implements a map as a hash table that maps keys to values through a hash function and bucket structures. The data lookup flow in a map can be visualized in the following diagram:
flowchart TD
Start["Key Input"] --> HashFunc["Hash Function"]
HashFunc --> HashValue["Hash Value"]
HashValue --> BucketSelect["Select Memory Bucket"]
BucketSelect --> CheckKeys{"Iterate Keys in Bucket?"}
CheckKeys -->|"Key Found"| ReturnVal["Return Value + ok=true"]
CheckKeys -->|"Key Not Found"| ReturnZero["Return Zero Value + ok=false"]Ways to Create a Map #
make — The Recommended Way
#
// make produces a map that's ready to use
m := make(map[string]int)
m["one"] = 1
m["two"] = 2
// make with a capacity hint — doesn't limit, just an initial optimization
// useful if you know how many entries will be inserted
m2 := make(map[string]int, 100)
Map Literals #
// Initialize with values at once
prices := map[string]float64{
"apple": 5000,
"mango": 8000,
"orange": 4500,
"durian": 35000,
}
// Empty map with a literal — different from a nil map!
empty := map[string]int{}
Nil Maps — Don’t Write to Them Directly #
// var produces a nil map
var m map[string]int
fmt.Println(m == nil) // true
fmt.Println(len(m)) // 0 — a nil map has len 0
// READING from a nil map is safe — returns the zero value
v := m["key"]
fmt.Println(v) // 0 — no panic
// WRITING to a nil map → PANIC!
m["key"] = 1 // panic: assignment to entry in nil map
// Always initialize before writing
m = make(map[string]int)
m["key"] = 1 // ✓ safe
Writing to a nil map causes a panic. Unlike nil slices, which are safe to append to, a nil map crashes when you try to write to it. Always usemake()or a literal{}before write operations. This is one of the most common runtime panics in beginner Go code.
CRUD Operations #
m := map[string]int{
"apple": 5000,
"mango": 8000,
}
// CREATE / UPDATE — same syntax
m["orange"] = 4500 // add a new key
m["apple"] = 6000 // update an existing key
// READ
applePrice := m["apple"] // 6000
teaPrice := m["tea"] // 0 — key doesn't exist, returns the zero value (not an error/panic)
// DELETE
delete(m, "mango") // remove the "mango" key
delete(m, "nonexistent") // safe, no panic even if the key doesn't exist
// LENGTH
fmt.Println(len(m)) // current entry count
The Two-Value Form — Essential for Distinguishing “Not Present” from a Zero Value #
This is a very important pattern that must be understood. Reading a map with a missing key returns the zero value of the value type — so it can’t be distinguished from a key that exists with a zero value:
stock := map[string]int{
"apple": 50,
"mango": 0, // stock 0, different from "not registered"!
}
// ANTI-PATTERN: can't distinguish "not present" from "stock 0"
s := stock["orange"]
if s == 0 {
// This is ambiguous: is orange not registered, or is its stock really 0?
}
// CORRECT: always use the two-value form
s, ok := stock["mango"]
fmt.Println(s, ok) // 0 true — mango EXISTS, its stock is indeed 0
s, ok = stock["orange"]
fmt.Println(s, ok) // 0 false — orange DOES NOT EXIST
s, ok = stock["apple"]
fmt.Println(s, ok) // 50 true — apple exists with stock 50
// Idiomatic pattern for check and access
if price, ok := catalog["laptop"]; ok {
fmt.Printf("Laptop price: Rp%.0f\n", price)
} else {
fmt.Println("Laptop is not in the catalog")
}
Iteration — Non-Deterministic #
Iterating a map with for range returns all key-value pairs, but the order is not guaranteed and differs every time the program runs. This is a deliberate Go design decision to prevent developers from depending on a specific order.
m := map[string]int{"charlie": 3, "alice": 1, "bob": 2}
// The order can differ on every run
for k, v := range m {
fmt.Printf("%s: %d\n", k, v)
}
// Iterate keys only
for k := range m {
fmt.Println(k)
}
// Values only (rarely used because it loses the key context)
for _, v := range m {
fmt.Println(v)
}
Iteration with a Consistent Order #
import "sort"
m := map[string]int{"charlie": 3, "alice": 1, "bob": 2}
// Collect all keys, sort, iterate
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])
}
// Output is always consistent: alice:1, bob:2, charlie:3
Maps Are Reference Types #
Maps are reference types — when passed to a function or assigned to another variable, both point to the same underlying hash table:
func addEntry(m map[string]int, key string, val int) {
m[key] = val // modifies the ORIGINAL map, not a copy
}
func main() {
data := map[string]int{"a": 1}
addEntry(data, "b", 2)
fmt.Println(data) // map[a:1 b:2] — changed!
}
// Assignment also shares the same reference
original := map[string]int{"x": 10}
alias := original
alias["y"] = 20
fmt.Println(original) // map[x:10 y:20] — original changed too!
This differs from slices, which have a shared backing array — but the concept is similar. To make a truly independent copy:
// Copy a map manually
func copyMap(src map[string]int) map[string]int {
dst := make(map[string]int, len(src))
for k, v := range src {
dst[k] = v
}
return dst
}
Key Constraints — Types That Can Be Keys #
Map keys must be comparable — comparable with ==. This includes: all basic types (int, string, bool, float), pointers, arrays, and structs whose fields are all comparable. Slices, maps, and functions can’t be keys:
// ✓ Valid keys
m1 := map[string]int{}
m2 := map[int]string{}
m3 := map[bool]int{}
m4 := map[[3]int]string{} // arrays can be keys
type Point struct{ X, Y int }
m5 := map[Point]string{} // comparable structs can be keys
// ✗ Invalid keys — compile error
// m6 := map[[]int]string{} // slices are not comparable
// m7 := map[map[string]int]string{} // maps are not comparable
// Using a struct as a key
grid := map[Point]string{
{0, 0}: "start",
{10, 5}: "waypoint",
{20, 0}: "end",
}
fmt.Println(grid[Point{10, 5}]) // "waypoint"
Maps as Sets #
Go doesn’t have a built-in Set type. The idiomatic way is using map[T]struct{} — struct{} is a type without fields that takes up no memory (zero size):
// A set of strings
seen := make(map[string]struct{})
words := []string{"go", "python", "go", "rust", "python", "go"}
var unique []string
for _, w := range words {
if _, ok := seen[w]; !ok {
seen[w] = struct{}{} // mark as seen
unique = append(unique, w)
}
}
fmt.Println(unique) // [go python rust]
// Helper functions for set operations
func contains(set map[string]struct{}, item string) bool {
_, ok := set[item]
return ok
}
func addToSet(set map[string]struct{}, item string) {
set[item] = struct{}{}
}
func removeFromSet(set map[string]struct{}, item string) {
delete(set, item)
}
// Intersection — elements present in both sets
func intersection(a, b map[string]struct{}) map[string]struct{} {
result := make(map[string]struct{})
for k := range a {
if _, ok := b[k]; ok {
result[k] = struct{}{}
}
}
return result
}
// Union — the combination of all elements
func union(a, b map[string]struct{}) map[string]struct{} {
result := make(map[string]struct{}, len(a)+len(b))
for k := range a { result[k] = struct{}{} }
for k := range b { result[k] = struct{}{} }
return result
}
Nested Maps and the Struct Modification Gotcha #
Maps can hold values of other map or slice types:
// Map of slices — group data
byCategory := make(map[string][]string)
byCategory["fruit"] = append(byCategory["fruit"], "apple")
byCategory["fruit"] = append(byCategory["fruit"], "mango")
byCategory["vegetable"] = append(byCategory["vegetable"], "spinach")
// Map of maps — nested
config := map[string]map[string]string{
"database": {
"host": "localhost",
"port": "5432",
"name": "myapp",
},
"cache": {
"host": "localhost",
"port": "6379",
},
}
fmt.Println(config["database"]["host"]) // localhost
Gotcha: Can’t Modify a Struct Field Inside a Map Directly #
This is a limitation that often surprises:
type Counter struct {
Count int
Name string
}
counters := map[string]Counter{
"hits": {Count: 0, Name: "Page Hits"},
}
// ANTI-PATTERN: compile error!
// counters["hits"].Count++
// ← cannot assign to struct field in map
// Solution 1: get, modify, store back
c := counters["hits"]
c.Count++
counters["hits"] = c
// Solution 2: store pointers to structs
pCounters := map[string]*Counter{
"hits": {Count: 0, Name: "Page Hits"},
}
pCounters["hits"].Count++ // ✓ pointers can be modified directly
Deleting While Iterating — Safe in Go #
Unlike some other languages, in Go you can delete map entries during a for range iteration — it’s safe and doesn’t cause undefined behavior:
m := map[string]int{
"a": 1, "b": -2, "c": 3, "d": -4, "e": 5,
}
// Delete all entries with negative values
for k, v := range m {
if v < 0 {
delete(m, k) // ✓ safe to do during range
}
}
fmt.Println(m) // map[a:1 c:3 e:5]
The maps Package (Go 1.21+)
#
Since Go 1.21, the maps package in the standard library provides useful utility functions:
import "maps"
m := map[string]int{"a": 1, "b": 2, "c": 3}
// Clone — an independent copy
clone := maps.Clone(m)
clone["d"] = 4
fmt.Println(m) // map[a:1 b:2 c:3] — unchanged
fmt.Println(clone) // map[a:1 b:2 c:3 d:4]
// Keys and Values (as iterators, Go 1.23+)
// Or collect into a slice:
keys := make([]string, 0, len(m))
for k := range m { keys = append(keys, k) }
// Equal — compare two maps
m2 := map[string]int{"a": 1, "b": 2, "c": 3}
fmt.Println(maps.Equal(m, m2)) // true
// DeleteFunc — delete entries matching a condition
maps.DeleteFunc(m, func(k string, v int) bool {
return v > 2 // delete entries with value > 2
})
fmt.Println(m) // map[a:1 b:2]
Concurrency — Maps Are Not Thread-Safe #
Go maps are not safe for concurrent access from multiple goroutines. Reading and writing a map from different goroutines simultaneously is a race condition that causes a runtime panic:
// ANTI-PATTERN: race condition!
var counter = make(map[string]int)
func incrementUnsafe(key string) {
counter[key]++ // ← unsafe if called from many goroutines!
}
// Solution 1: sync.RWMutex — flexible, many reads one write
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 (sm *SafeMap) Increment(key string) {
sm.mu.Lock()
defer sm.mu.Unlock()
sm.m[key]++
}
// Solution 2: sync.Map — optimized for read-heavy workloads or rarely-changing keys
var safeCounter sync.Map
safeCounter.Store("hits", 0)
val, _ := safeCounter.Load("hits")
fmt.Println(val)
safeCounter.Store("hits", val.(int)+1)
// LoadOrStore — load if present, store if not
actual, loaded := safeCounter.LoadOrStore("new_key", 42)
fmt.Println(actual, loaded) // 42, false (new key)
// Range — iterate a sync.Map
safeCounter.Range(func(key, value any) bool {
fmt.Printf("%v: %v\n", key, value)
return true // return false to stop iteration
})
Idiomatic Patterns #
Frequency Counters #
text := "the quick brown fox jumps over the lazy dog the fox"
words := strings.Fields(text)
freq := make(map[string]int)
for _, w := range words {
freq[w]++ // increment — the zero value of int is 0, so ++ works directly
}
// Display by frequency order
type WordFreq struct {
Word string
Count int
}
var list []WordFreq
for w, c := range freq {
list = append(list, WordFreq{w, c})
}
sort.Slice(list, func(i, j int) bool {
if list[i].Count != list[j].Count {
return list[i].Count > list[j].Count // descending by count
}
return list[i].Word < list[j].Word // ascending by word if counts are equal
})
for _, wf := range list[:3] {
fmt.Printf("%-10s: %d\n", wf.Word, wf.Count)
}
Grouping Data #
type Order struct {
ID int
Customer string
Amount float64
Status string
}
orders := []Order{
{1, "Budi", 150000, "paid"},
{2, "Sari", 250000, "pending"},
{3, "Budi", 75000, "paid"},
{4, "Ahmad", 300000, "pending"},
{5, "Sari", 100000, "paid"},
}
// Group by customer
byCustomer := make(map[string][]Order)
for _, o := range orders {
byCustomer[o.Customer] = append(byCustomer[o.Customer], o)
}
// Calculate the total per customer
for customer, customerOrders := range byCustomer {
total := 0.0
for _, o := range customerOrders {
total += o.Amount
}
fmt.Printf("%s: %d orders, total Rp%.0f\n",
customer, len(customerOrders), total)
}
In-Memory Cache #
type Cache struct {
mu sync.RWMutex
store map[string]cacheEntry
}
type cacheEntry struct {
value interface{}
expireAt time.Time
}
func NewCache() *Cache {
return &Cache{store: make(map[string]cacheEntry)}
}
func (c *Cache) Set(key string, value interface{}, ttl time.Duration) {
c.mu.Lock()
defer c.mu.Unlock()
c.store[key] = cacheEntry{
value: value,
expireAt: time.Now().Add(ttl),
}
}
func (c *Cache) Get(key string) (interface{}, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
entry, ok := c.store[key]
if !ok || time.Now().After(entry.expireAt) {
return nil, false
}
return entry.value, true
}
func (c *Cache) Cleanup() {
c.mu.Lock()
defer c.mu.Unlock()
now := time.Now()
for k, v := range c.store {
if now.After(v.expireAt) {
delete(c.store, k) // remove expired entries
}
}
}
Complete Example Program #
The following program analyzes text and produces various statistics using maps:
package main
import (
"fmt"
"sort"
"strings"
"unicode"
)
type TextAnalyzer struct {
wordFreq map[string]int
charFreq map[rune]int
bigramFreq map[string]int
sentences int
totalWords int
totalChars int
}
func NewTextAnalyzer() *TextAnalyzer {
return &TextAnalyzer{
wordFreq: make(map[string]int),
charFreq: make(map[rune]int),
bigramFreq: make(map[string]int),
}
}
func (ta *TextAnalyzer) Analyze(text string) {
// Count characters
for _, r := range text {
if !unicode.IsSpace(r) {
ta.charFreq[unicode.ToLower(r)]++
ta.totalChars++
}
}
// Count words
words := strings.FieldsFunc(text, func(r rune) bool {
return !unicode.IsLetter(r) && !unicode.IsDigit(r)
})
cleanWords := make([]string, 0, len(words))
for _, w := range words {
if len(w) > 0 {
lower := strings.ToLower(w)
ta.wordFreq[lower]++
ta.totalWords++
cleanWords = append(cleanWords, lower)
}
}
// Count bigrams (consecutive word pairs)
for i := 0; i < len(cleanWords)-1; i++ {
bigram := cleanWords[i] + " " + cleanWords[i+1]
ta.bigramFreq[bigram]++
}
// Count sentences (rough estimate)
for _, r := range text {
if r == '.' || r == '!' || r == '?' {
ta.sentences++
}
}
}
// Top N entries from a map by frequency
func topN(freq map[string]int, n int) []struct{ Key string; Count int } {
type entry struct {
Key string
Count int
}
entries := make([]entry, 0, len(freq))
for k, v := range freq {
entries = append(entries, entry{k, v})
}
sort.Slice(entries, func(i, j int) bool {
if entries[i].Count != entries[j].Count {
return entries[i].Count > entries[j].Count
}
return entries[i].Key < entries[j].Key
})
if n > len(entries) {
n = len(entries)
}
result := make([]struct{ Key string; Count int }, n)
for i := 0; i < n; i++ {
result[i] = struct{ Key string; Count int }{entries[i].Key, entries[i].Count}
}
return result
}
func (ta *TextAnalyzer) Report() {
fmt.Println("=== Text Analysis Report ===\n")
fmt.Printf("Basic Statistics:\n")
fmt.Printf(" Total characters (non-space): %d\n", ta.totalChars)
fmt.Printf(" Total words : %d\n", ta.totalWords)
fmt.Printf(" Total sentences (estimate) : %d\n", ta.sentences)
fmt.Printf(" Unique vocabulary : %d\n", len(ta.wordFreq))
if ta.sentences > 0 {
fmt.Printf(" Average words/sentence : %.1f\n",
float64(ta.totalWords)/float64(ta.sentences))
}
fmt.Printf("\n10 Most Frequent Words:\n")
for i, e := range topN(ta.wordFreq, 10) {
bar := strings.Repeat("█", e.Count)
fmt.Printf(" %2d. %-15s %2d %s\n", i+1, e.Key, e.Count, bar)
}
fmt.Printf("\n5 Most Frequent Bigrams:\n")
for i, e := range topN(ta.bigramFreq, 5) {
fmt.Printf(" %d. %-25s %d times\n", i+1, e.Key, e.Count)
}
fmt.Printf("\n10 Most Frequent Characters:\n")
charEntries := make(map[string]int)
for r, c := range ta.charFreq {
if unicode.IsLetter(r) {
charEntries[string(r)] = c
}
}
for i, e := range topN(charEntries, 10) {
fmt.Printf(" %2d. '%s': %d\n", i+1, e.Key, e.Count)
}
// Word length distribution
lengthDist := make(map[int]int)
for w, c := range ta.wordFreq {
lengthDist[len([]rune(w))] += c
}
fmt.Printf("\nWord Length Distribution:\n")
lengths := make([]int, 0)
for l := range lengthDist {
lengths = append(lengths, l)
}
sort.Ints(lengths)
for _, l := range lengths {
if l <= 10 {
pct := float64(lengthDist[l]) / float64(ta.totalWords) * 100
bar := strings.Repeat("▪", int(pct/2))
fmt.Printf(" %2d letters: %4d words (%4.1f%%) %s\n",
l, lengthDist[l], pct, bar)
}
}
}
func main() {
text := `Go is a programming language developed by Google.
Go is designed for efficiency and simplicity. The Go language has
clean syntax that is easy to learn. Go supports concurrency
through goroutines and channels. Many companies use Go
to build scalable and efficient systems. Go is the right choice
for backend, microservice, and command-line tools.
The Go ecosystem keeps growing with many libraries available.
Go has a fast compiler and produces a single binary.`
analyzer := NewTextAnalyzer()
analyzer.Analyze(text)
analyzer.Report()
}
Summary #
- Nil maps are safe to read (returning the zero value) but panic if written to — always initialize with
make()or{}before writing.- The two-value form (
val, ok := m[key]) is the only way to distinguish “key not present” from “key present with a zero value.”- Iteration order is not guaranteed — sort keys into a slice first if you need consistent output.
- Maps are reference types — passing to a function or assigning to another variable shares the same underlying hash table.
- Keys must be comparable — strings, ints, bools, arrays, and comparable structs can be keys; slices, maps, and funcs can’t.
map[T]struct{}is the idiomatic Go Set —struct{}takes no memory.- You can’t modify a struct field inside a map directly — get it, modify it, store it back, or use pointers as values.
- Deleting while iterating is safe in Go —
delete(m, k)duringfor rangedoesn’t cause undefined behavior.- Maps are not thread-safe — use
sync.RWMutexfor read-heavy workloads orsync.Mapfor concurrent access.maps.Clone,maps.Equal,maps.DeleteFuncare available since Go 1.21 for common map operations.