Skip to main content

Go Maps

Maps in Go are powerful data structures that provide efficient key-value storage and retrieval. They are implemented as hash tables, offering fast access times for most operations. Understanding maps is crucial for building efficient Go programs that need to store and retrieve data by keys. This comprehensive guide will teach you everything you need to know about Go's map system.

Understanding Maps in Go

What Are Maps?

Maps in Go are collections of key-value pairs where each key is unique and maps to a specific value. They provide:

  • Fast access - O(1) average time complexity for most operations
  • Dynamic size - Maps can grow and shrink as needed
  • Type safety - Keys and values must be of specific types
  • Hash table implementation - Efficient storage and retrieval
  • Reference semantics - Maps are reference types

Map Characteristics

Go maps have several important characteristics:

Hash Table Implementation

Maps in Go are implemented as hash tables, providing fast average-case performance.

Reference Types

Maps are reference types, meaning they are passed by reference to functions.

Nil Maps

Maps can be nil, and operations on nil maps will cause runtime panics.

Non-Comparable Keys

Map keys must be comparable types (not slices, maps, or functions).

Map Creation and Initialization

Map Declaration and Creation

The make Built-in Function for Maps

The make function is used to create maps with specific key and value types.

Map Literal Syntax

Maps can be created using literal syntax with curly braces.

package main

import "fmt"

func main() {
// Map declaration and creation examples
fmt.Println("Map declaration and creation examples:")

// Declaration with var keyword
var map1 map[string]int
fmt.Printf("Map 1: %v (nil: %t)\n", map1, map1 == nil)
// Output: Map 1: map[] (nil: true)

// Creation with make function
map2 := make(map[string]int)
fmt.Printf("Map 2: %v (nil: %t)\n", map2, map2 == nil)
// Output: Map 2: map[] (nil: false)

// Creation with make function and initial capacity
map3 := make(map[string]int, 10)
fmt.Printf("Map 3: %v (length: %d)\n", map3, len(map3))
// Output: Map 3: map[] (length: 0)

// Map literal
map4 := map[string]int{
"apple": 5,
"banana": 3,
"orange": 8,
}
fmt.Printf("Map 4: %v\n", map4)
// Output: Map 4: map[apple:5 banana:3 orange:8]

// Map literal with different types
map5 := map[int]string{
1: "one",
2: "two",
3: "three",
}
fmt.Printf("Map 5: %v\n", map5)
// Output: Map 5: map[1:one 2:two 3:three]

// Map literal with boolean values
map6 := map[string]bool{
"isActive": true,
"isVerified": false,
"isPremium": true,
}
fmt.Printf("Map 6: %v\n", map6)
// Output: Map 6: map[isActive:true isPremium:true isVerified:false]
}

Map Operations and Access

Map Indexing

Maps use square bracket notation to access and modify values.

The len Built-in Function for Maps

The len function returns the number of key-value pairs in a map.

package main

import "fmt"

func main() {
// Map operations and access examples
fmt.Println("Map operations and access examples:")

// Create a map
scores := map[string]int{
"Alice": 95,
"Bob": 87,
"Charlie": 92,
}

// Get map length
length := len(scores)
fmt.Printf("Map length: %d\n", length)
// Output: Map length: 3

// Access values
fmt.Printf("Alice's score: %d\n", scores["Alice"])
fmt.Printf("Bob's score: %d\n", scores["Bob"])
// Output:
// Alice's score: 95
// Bob's score: 87

// Access non-existent key
fmt.Printf("David's score: %d\n", scores["David"])
// Output: David's score: 0 (zero value)

// Modify values
scores["Alice"] = 98
scores["David"] = 89
fmt.Printf("Updated scores: %v\n", scores)
// Output: Updated scores: map[Alice:98 Bob:87 Charlie:92 David:89]

// Check if key exists
if score, exists := scores["Alice"]; exists {
fmt.Printf("Alice's score: %d\n", score)
} else {
fmt.Println("Alice not found")
}
// Output: Alice's score: 98

// Check if key exists (alternative syntax)
score, exists := scores["Eve"]
if exists {
fmt.Printf("Eve's score: %d\n", score)
} else {
fmt.Println("Eve not found")
}
// Output: Eve not found

// Delete key-value pair
delete(scores, "Charlie")
fmt.Printf("After deletion: %v\n", scores)
// Output: After deletion: map[Alice:98 Bob:87 David:89]
}

Map Iteration and Range

The range Keyword with Maps

The range keyword allows you to iterate over map key-value pairs.

package main

import "fmt"

func main() {
// Map iteration and range examples
fmt.Println("Map iteration and range examples:")

// Create a map
colors := map[string]string{
"red": "#FF0000",
"green": "#00FF00",
"blue": "#0000FF",
"yellow": "#FFFF00",
"purple": "#800080",
}

// Iterate over key-value pairs
fmt.Println("All colors:")
for key, value := range colors {
fmt.Printf(" %s: %s\n", key, value)
}
// Output (order may vary):
// All colors:
// red: #FF0000
// green: #00FF00
// blue: #0000FF
// yellow: #FFFF00
// purple: #800080

// Iterate over keys only
fmt.Println("Color names:")
for key := range colors {
fmt.Printf(" %s\n", key)
}
// Output (order may vary):
// Color names:
// red
// green
// blue
// yellow
// purple

// Iterate over values only
fmt.Println("Color codes:")
for _, value := range colors {
fmt.Printf(" %s\n", value)
}
// Output (order may vary):
// Color codes:
// #FF0000
// #00FF00
// #0000FF
// #FFFF00
// #800080

// Iterate with index (if needed)
fmt.Println("Colors with index:")
index := 0
for key, value := range colors {
fmt.Printf(" %d: %s = %s\n", index, key, value)
index++
}
// Output (order may vary):
// Colors with index:
// 0: red = #FF0000
// 1: green = #00FF00
// 2: blue = #0000FF
// 3: yellow = #FFFF00
// 4: purple = #800080
}

Advanced Map Operations

Map as Function Parameters

Maps are passed by reference to functions, making them efficient for large data.

package main

import "fmt"

func main() {
// Map as function parameters examples
fmt.Println("Map as function parameters examples:")

// Create a map
inventory := map[string]int{
"apples": 50,
"bananas": 30,
"oranges": 25,
}

fmt.Printf("Original inventory: %v\n", inventory)
// Output: Original inventory: map[apples:50 bananas:30 oranges:25]

// Pass map to function
updateInventory(inventory, "apples", 45)
fmt.Printf("After updating apples: %v\n", inventory)
// Output: After updating apples: map[apples:45 bananas:30 oranges:25]

// Pass map to function that returns new map
discounted := applyDiscount(inventory, 0.1)
fmt.Printf("Original: %v\n", inventory)
fmt.Printf("Discounted: %v\n", discounted)
// Output:
// Original: map[apples:45 bananas:30 oranges:25]
// Discounted: map[apples:40 bananas:27 oranges:22]

// Pass map to function that filters
filtered := filterByQuantity(inventory, 30)
fmt.Printf("Filtered (qty >= 30): %v\n", filtered)
// Output: Filtered (qty >= 30): map[bananas:30]
}

// Function that modifies map in place
func updateInventory(inventory map[string]int, item string, quantity int) {
inventory[item] = quantity
}

// Function that returns new map
func applyDiscount(inventory map[string]int, discount float64) map[string]int {
result := make(map[string]int)
for item, quantity := range inventory {
result[item] = int(float64(quantity) * (1 - discount))
}
return result
}

// Function that filters map
func filterByQuantity(inventory map[string]int, minQuantity int) map[string]int {
result := make(map[string]int)
for item, quantity := range inventory {
if quantity >= minQuantity {
result[item] = quantity
}
}
return result
}

Map Patterns and Use Cases

Maps are versatile data structures with many practical applications.

package main

import "fmt"

func main() {
// Map patterns and use cases examples
fmt.Println("Map patterns and use cases examples:")

// Pattern 1: Counter/Histogram
text := "hello world"
charCount := make(map[rune]int)
for _, char := range text {
charCount[char]++
}
fmt.Printf("Character count: %v\n", charCount)
// Output: Character count: map[ :1 e:1 d:1 h:1 l:3 o:2 r:1 w:1]

// Pattern 2: Set simulation
numbers := []int{1, 2, 3, 2, 4, 3, 5, 1, 6}
uniqueNumbers := make(map[int]bool)
for _, num := range numbers {
uniqueNumbers[num] = true
}
fmt.Printf("Unique numbers: %v\n", uniqueNumbers)
// Output: Unique numbers: map[1:true 2:true 3:true 4:true 5:true 6:true]

// Pattern 3: Lookup table
monthNames := map[int]string{
1: "January",
2: "February",
3: "March",
4: "April",
5: "May",
6: "June",
7: "July",
8: "August",
9: "September",
10: "October",
11: "November",
12: "December",
}
fmt.Printf("Month 3: %s\n", monthNames[3])
// Output: Month 3: March

// Pattern 4: Grouping
students := []struct {
Name string
Grade string
}{
{"Alice", "A"},
{"Bob", "B"},
{"Charlie", "A"},
{"David", "C"},
{"Eve", "B"},
}

gradeGroups := make(map[string][]string)
for _, student := range students {
gradeGroups[student.Grade] = append(gradeGroups[student.Grade], student.Name)
}
fmt.Printf("Grade groups: %v\n", gradeGroups)
// Output: Grade groups: map[A:[Alice Charlie] B:[Bob Eve] C:[David]]

// Pattern 5: Configuration
config := map[string]interface{}{
"database": map[string]string{
"host": "localhost",
"port": "5432",
"name": "mydb",
},
"server": map[string]interface{}{
"port": 8080,
"debug": true,
},
"features": []string{"auth", "logging", "metrics"},
}
fmt.Printf("Config: %v\n", config)
// Output: Config: map[database:map[host:localhost name:mydb port:5432] features:[auth logging metrics] server:map[debug:true port:8080]]
}

Map Concurrency Considerations

Maps in Go are not thread-safe and require special handling for concurrent access.

package main

import (
"fmt"
"sync"
)

func main() {
// Map concurrency considerations examples
fmt.Println("Map concurrency considerations examples:")

// Unsafe concurrent access (will cause runtime panic)
// unsafeMap := make(map[string]int)
// go func() { unsafeMap["key1"] = 1 }()
// go func() { unsafeMap["key2"] = 2 }()

// Safe concurrent access with mutex
safeMap := make(map[string]int)
var mutex sync.RWMutex

// Write operations
mutex.Lock()
safeMap["key1"] = 1
safeMap["key2"] = 2
mutex.Unlock()

// Read operations
mutex.RLock()
value1 := safeMap["key1"]
value2 := safeMap["key2"]
mutex.RUnlock()

fmt.Printf("Safe map values: key1=%d, key2=%d\n", value1, value2)
// Output: Safe map values: key1=1, key2=2

// Safe concurrent access with sync.Map
var syncMap sync.Map

// Store values
syncMap.Store("key1", 1)
syncMap.Store("key2", 2)

// Load values
if value, ok := syncMap.Load("key1"); ok {
fmt.Printf("Sync map key1: %v\n", value)
}
// Output: Sync map key1: 1

// Range over sync.Map
fmt.Println("Sync map contents:")
syncMap.Range(func(key, value interface{}) bool {
fmt.Printf(" %v: %v\n", key, value)
return true
})
// Output:
// Sync map contents:
// key1: 1
// key2: 2
}

Map Performance and Optimization

Map Performance Characteristics

Understanding map performance helps you write efficient code.

package main

import (
"fmt"
"time"
)

func main() {
// Map performance characteristics examples
fmt.Println("Map performance characteristics examples:")

// Create large map
largeMap := make(map[int]string, 1000000)

// Measure insertion time
start := time.Now()
for i := 0; i < 1000000; i++ {
largeMap[i] = fmt.Sprintf("value_%d", i)
}
insertionTime := time.Since(start)
fmt.Printf("Insertion time for 1M items: %v\n", insertionTime)
// Output: Insertion time for 1M items: 200ms

// Measure lookup time
start = time.Now()
for i := 0; i < 1000000; i++ {
_ = largeMap[i]
}
lookupTime := time.Since(start)
fmt.Printf("Lookup time for 1M items: %v\n", lookupTime)
// Output: Lookup time for 1M items: 150ms

// Measure deletion time
start = time.Now()
for i := 0; i < 1000000; i++ {
delete(largeMap, i)
}
deletionTime := time.Since(start)
fmt.Printf("Deletion time for 1M items: %v\n", deletionTime)
// Output: Deletion time for 1M items: 180ms

// Map size after operations
fmt.Printf("Map size after operations: %d\n", len(largeMap))
// Output: Map size after operations: 0
}

Map Optimization Techniques

Following optimization techniques ensures efficient map usage.

package main

import "fmt"

func main() {
// Map optimization techniques examples
fmt.Println("Map optimization techniques examples:")

// Technique 1: Pre-allocate capacity
fmt.Println("Technique 1: Pre-allocate capacity")
optimizedMap := make(map[string]int, 1000) // Pre-allocate for 1000 items
for i := 0; i < 1000; i++ {
optimizedMap[fmt.Sprintf("key_%d", i)] = i
}
fmt.Printf("Optimized map size: %d\n", len(optimizedMap))
// Output: Optimized map size: 1000

// Technique 2: Use appropriate key types
fmt.Println("Technique 2: Use appropriate key types")
intKeyMap := make(map[int]string) // Fast integer keys
stringKeyMap := make(map[string]int) // Slower string keys

intKeyMap[1] = "one"
stringKeyMap["one"] = 1

fmt.Printf("Int key map: %v\n", intKeyMap)
fmt.Printf("String key map: %v\n", stringKeyMap)
// Output:
// Int key map: map[1:one]
// String key map: map[one:1]

// Technique 3: Avoid unnecessary allocations
fmt.Println("Technique 3: Avoid unnecessary allocations")
result := make(map[string]int, 0) // Start with empty capacity
for i := 0; i < 100; i++ {
result[fmt.Sprintf("item_%d", i)] = i
}
fmt.Printf("Result map size: %d\n", len(result))
// Output: Result map size: 100

// Technique 4: Use map for set operations
fmt.Println("Technique 4: Use map for set operations")
set := make(map[string]bool)
items := []string{"apple", "banana", "apple", "orange", "banana"}

for _, item := range items {
set[item] = true
}

fmt.Printf("Unique items: %v\n", set)
// Output: Unique items: map[apple:true banana:true orange:true]

// Technique 5: Use map for caching
fmt.Println("Technique 5: Use map for caching")
cache := make(map[int]int)

// Simulate expensive computation
expensiveComputation := func(n int) int {
if result, exists := cache[n]; exists {
fmt.Printf("Cache hit for %d\n", n)
return result
}
fmt.Printf("Computing for %d\n", n)
result := n * n // Simulate expensive operation
cache[n] = result
return result
}

fmt.Printf("Result 1: %d\n", expensiveComputation(5))
fmt.Printf("Result 2: %d\n", expensiveComputation(5))
fmt.Printf("Result 3: %d\n", expensiveComputation(3))
// Output:
// Computing for 5
// Result 1: 25
// Cache hit for 5
// Result 2: 25
// Computing for 3
// Result 3: 9
}

What You've Learned

Congratulations! You now have a comprehensive understanding of Go's map data structure:

Map Fundamentals

  • Understanding map declaration and creation
  • Working with map operations and access
  • Using map iteration and range
  • Understanding map characteristics and behavior

Advanced Map Operations

  • Using maps as function parameters
  • Implementing map patterns and use cases
  • Handling map concurrency considerations
  • Understanding map performance characteristics

Map Optimization

  • Following map optimization techniques
  • Understanding map performance characteristics
  • Implementing efficient map patterns
  • Using maps for caching and set operations

Built-in Functions

  • make - Creating maps with specific types
  • len - Getting map size
  • delete - Removing key-value pairs
  • range - Iterating over map contents

Next Steps

You now have a solid foundation in Go's map data structure. In the next section, we'll explore Go's struct system, which provides a way to create custom data types and organize related data.

Understanding maps is crucial for efficient key-value storage and retrieval. These concepts form the foundation for all the more advanced programming techniques we'll cover in the coming chapters.


Ready to learn about structs? Let's explore Go's powerful struct system and learn how to create custom data types!