Skip to content

Go Maps: Key-Value Data Structures

Overview

Master Go's powerful map data structure for efficient key-value storage and retrieval. This comprehensive guide covers map creation, operations, iteration patterns, and advanced techniques, building upon our understanding of slices and data types.

Key Points

  • Maps are reference types with key-value associations
  • Unordered collections with unique keys
  • Built-in functions for efficient operations
  • Comparable key types required
  • Essential for lookups, caching, and data organization

Understanding Maps in Go 🗺

Maps are Go's built-in associative data structure, providing fast key-value lookups similar to hash tables or dictionaries in other languages. They're reference types that offer dynamic sizing and efficient operations.

Map Architecture

graph TD
    A[Go Map] --> B[Hash Table]
    B --> C[Bucket 0]
    B --> D[Bucket 1]
    B --> E[Bucket N]
    C --> F[Key1: Value1]
    C --> G[Key2: Value2]
    D --> H[Key3: Value3]
    E --> I[...]
    style A fill:#999,stroke:#333,stroke-width:2px,color:#000
    style B fill:#e1f5fe,stroke:#01579b,stroke-width:2px

Map Creation Patterns ⚙

Go provides multiple ways to create and initialize maps, each suited for different scenarios and use cases.

Map Literals and Declarations

Basic Map Creation

map_literals.go
package main

import "fmt"

func main() {
    // Create map with initial values
    ages := map[string]int{
        "Alice":   25,
        "Bob":     30,
        "Charlie": 35,
    }

    fmt.Printf("Ages: %v\n", ages)
    fmt.Printf("Alice's age: %d\n", ages["Alice"])

    // Output:
    // Ages: map[Alice:25 Bob:30 Charlie:35]
    // Alice's age: 25
}
empty_maps.go
package main

import "fmt"

func main() {
    // Nil map (zero value)
    var nilMap map[string]int
    fmt.Printf("Nil map: %v (nil=%t)\n", nilMap, nilMap == nil)

    // Empty map (not nil)
    emptyMap := map[string]int{}
    fmt.Printf("Empty map: %v (nil=%t)\n", emptyMap, emptyMap == nil)

    // Output:
    // Nil map: map[] (nil=true)
    // Empty map: map[] (nil=false)
}
key_types.go
package main

import "fmt"

func main() {
    // String keys
    colors := map[string]string{"red": "#FF0000", "green": "#00FF00"}

    // Integer keys
    squares := map[int]int{1: 1, 2: 4, 3: 9, 4: 16}

    // Boolean keys
    flags := map[bool]string{true: "enabled", false: "disabled"}

    fmt.Printf("Colors: %v\n", colors)
    fmt.Printf("Squares: %v\n", squares)
    fmt.Printf("Flags: %v\n", flags)
}

The make Function

Using make for Maps

make_maps.go
package main

import "fmt"

func main() {
    // Create empty map with make
    inventory := make(map[string]int)

    // Add elements
    inventory["apples"] = 50
    inventory["bananas"] = 30
    inventory["oranges"] = 25

    fmt.Printf("Inventory: %v\n", inventory)
    fmt.Printf("Length: %d\n", len(inventory))

    // Output:
    // Inventory: map[apples:50 bananas:30 oranges:25]
    // Length: 3
}
map_capacity.go
package main

import "fmt"

func main() {
    // Create map with capacity hint for performance
    largeMap := make(map[int]string, 1000)

    // Add some elements
    for i := 0; i < 5; i++ {
        largeMap[i] = fmt.Sprintf("value_%d", i)
    }

    fmt.Printf("Large map length: %d\n", len(largeMap))
    // Output: Large map length: 5
}
nil_vs_empty.go
package main

import "fmt"

func main() {
    // Nil map (zero value)
    var nilMap map[string]int

    // Reading from nil map is safe
    value := nilMap["key"]  // Returns zero value
    fmt.Printf("Value from nil map: %d\n", value)

    // Writing to nil map causes panic
    // nilMap["key"] = 42  // panic: assignment to entry in nil map

    // Must initialize before writing
    nilMap = make(map[string]int)
    nilMap["key"] = 42
    fmt.Printf("After initialization: %v\n", nilMap)
}

Map Operations and Access 🔧

Maps support efficient operations for adding, updating, accessing, and deleting key-value pairs.

Element Access and Modification

Map Operations

map_access.go
package main

import "fmt"

func main() {
    scores := map[string]int{
        "Alice": 95, "Bob": 87, "Charlie": 92,
    }

    // Access elements
    fmt.Printf("Alice's score: %d\n", scores["Alice"])

    // Update existing element
    scores["Alice"] = 98

    // Add new element
    scores["Diana"] = 89

    fmt.Printf("Updated scores: %v\n", scores)
}
key_existence.go
package main

import "fmt"

func main() {
    inventory := map[string]int{"apples": 50, "bananas": 30}

    // Check if key exists (comma ok idiom)
    if count, exists := inventory["apples"]; exists {
        fmt.Printf("Apples in stock: %d\n", count)
    }

    // Check for non-existent key
    if count, exists := inventory["oranges"]; exists {
        fmt.Printf("Oranges in stock: %d\n", count)
    } else {
        fmt.Println("Oranges not in inventory")
    }

    // Direct access returns zero value for missing keys
    fmt.Printf("Grapes count: %d\n", inventory["grapes"])  // 0
}
map_deletion.go
package main

import "fmt"

func main() {
    users := map[int]string{
        1: "Alice", 2: "Bob", 3: "Charlie", 4: "Diana",
    }

    fmt.Printf("Before deletion: %v\n", users)

    // Delete element
    delete(users, 2)  // Remove Bob

    // Safe to delete non-existent key
    delete(users, 999)

    fmt.Printf("After deletion: %v\n", users)
    fmt.Printf("Length: %d\n", len(users))
}

Map Iteration Patterns 🔁

Go provides the range keyword for iterating over maps, with different patterns for accessing keys, values, or both.

Iteration Order

Map iteration order is not guaranteed and may vary between program runs. This is intentional to prevent reliance on iteration order.

Iteration Techniques

Map Iteration

map_iteration.go
package main

import "fmt"

func main() {
    grades := map[string]int{
        "Alice": 95, "Bob": 87, "Charlie": 92, "Diana": 89,
    }

    // Iterate over keys and values
    for name, grade := range grades {
        fmt.Printf("%s: %d\n", name, grade)
    }

    // Note: Map iteration order is not guaranteed
}
keys_only.go
package main

import "fmt"

func main() {
    inventory := map[string]int{
        "apples": 50, "bananas": 30, "oranges": 25,
    }

    // Iterate over keys only
    for item := range inventory {
        fmt.Printf("Item: %s\n", item)
    }
}
values_only.go
package main

import "fmt"

func main() {
    scores := map[string]int{
        "Alice": 95, "Bob": 87, "Charlie": 92,
    }

    // Iterate over values only (using blank identifier)
    for _, score := range scores {
        fmt.Printf("Score: %d\n", score)
    }
}

Advanced Map Techniques ⚙

Complex Key and Value Types

Advanced Map Types

struct_keys.go
package main

import "fmt"

type Point struct {
    X, Y int
}

func main() {
    // Struct keys (must be comparable)
    distances := map[Point]float64{
        {0, 0}: 0.0,
        {1, 1}: 1.414,
        {3, 4}: 5.0,
    }

    origin := Point{0, 0}
    fmt.Printf("Distance from origin: %.3f\n", distances[origin])
}
slice_values.go
package main

import "fmt"

func main() {
    // Map with slice values for grouping
    categories := map[string][]string{
        "fruits":     {"apple", "banana", "orange"},
        "vegetables": {"carrot", "broccoli", "spinach"},
        "grains":     {"rice", "wheat", "oats"},
    }

    // Add to existing slice
    categories["fruits"] = append(categories["fruits"], "grape")

    fmt.Printf("Fruits: %v\n", categories["fruits"])
}
nested_maps.go
package main

import "fmt"

func main() {
    // Nested maps for complex data structures
    studentGrades := map[string]map[string]int{
        "Alice": {"Math": 95, "Science": 87, "English": 92},
        "Bob":   {"Math": 78, "Science": 85, "English": 88},
    }

    // Access nested values
    aliceMath := studentGrades["Alice"]["Math"]
    fmt.Printf("Alice's Math grade: %d\n", aliceMath)

    // Add new student
    studentGrades["Charlie"] = map[string]int{
        "Math": 90, "Science": 93, "English": 89,
    }
}

Common Map Patterns

Practical Patterns

counting.go
package main

import "fmt"

func main() {
    words := []string{"apple", "banana", "apple", "orange", "banana", "apple"}

    // Count word occurrences
    counts := make(map[string]int)
    for _, word := range words {
        counts[word]++  // Zero value of int is 0
    }

    fmt.Printf("Word counts: %v\n", counts)
    // Output: map[apple:3 banana:2 orange:1]
}
set_pattern.go
package main

import "fmt"

func main() {
    // Implement set using map[T]bool
    uniqueItems := make(map[string]bool)

    items := []string{"apple", "banana", "apple", "orange", "banana"}

    // Add items to set
    for _, item := range items {
        uniqueItems[item] = true
    }

    // Check membership
    if uniqueItems["apple"] {
        fmt.Println("Apple is in the set")
    }

    // Get all unique items
    fmt.Print("Unique items: ")
    for item := range uniqueItems {
        fmt.Printf("%s ", item)
    }
    fmt.Println()
}
grouping.go
package main

import "fmt"

type Person struct {
    Name string
    Age  int
    City string
}

func main() {
    people := []Person{
        {"Alice", 25, "NYC"}, {"Bob", 30, "LA"}, {"Charlie", 35, "NYC"},
    }

    // Group people by city
    byCity := make(map[string][]Person)
    for _, person := range people {
        byCity[person.City] = append(byCity[person.City], person)
    }

    for city, residents := range byCity {
        fmt.Printf("%s: %d residents\n", city, len(residents))
    }
}

Best Practices and Performance 📓

Map Best Practices

  1. Initialize Before Use

    1
    2
    3
    4
    5
    6
    7
    8
    // Good: Initialize with make or literal
    m := make(map[string]int)
    // or
    m := map[string]int{}
    
    // Bad: Using nil map
    var m map[string]int
    m["key"] = value  // panic!
    

  2. Use Comma Ok Idiom

    1
    2
    3
    4
    // Check key existence
    if value, exists := m["key"]; exists {
        // Key exists, use value
    }
    

  3. Pre-allocate When Size is Known

    // Hint capacity for better performance
    m := make(map[string]int, expectedSize)
    

  4. Handle Concurrent Access

    1
    2
    3
    4
    // Use sync.Map or mutex for concurrent access
    var mu sync.RWMutex
    // or
    var m sync.Map
    

Common Pitfalls

  • Nil Map Writes: Writing to nil map causes panic
  • Iteration Order: Never rely on map iteration order
  • Concurrent Access: Maps are not thread-safe
  • Key Comparability: Only comparable types can be keys

Quick Reference 📑

Key Takeaways

  1. Creation: Use literals, make(), or zero value (nil)
  2. Access: Direct access or comma ok idiom for safety
  3. Modification: Assignment for add/update, delete() for removal
  4. Iteration: Use range with various patterns
  5. Performance: O(1) average time complexity for operations
  6. Concurrency: Use synchronization for multi-goroutine access

Remember

"Maps are Go's associative arrays. Master key-value operations, understand reference semantics, and always check for key existence when needed. When in doubt about concurrency, synchronize access."