Go Arrays, Slices, and Maps - Mastering Data Collections

Learning Objectives: By the end of this lesson, you will be able to:

  • Understand the differences between arrays and slices in Go
  • Master slice operations including append, copy, and slicing
  • Work effectively with maps for key-value data storage
  • Apply advanced slice and map techniques for real-world problems
  • Choose the appropriate data structure for different scenarios
  • Optimize memory usage and performance with proper data structure selection
  • Handle common pitfalls and edge cases in data collection manipulation
  • Build complex data processing applications using Go’s collection types

1. Welcome and Introduction (5 minutes)

Icebreaker Activity

Imagine you’re organizing a library. You could arrange books on fixed shelves (arrays), use expandable bookcases that can grow (slices), or create a card catalog system where you can quickly find any book by title (maps). Each approach has its strengths - which would you choose for different scenarios, and why?

Lesson Overview

  • Duration: 8-10 hours
  • Prerequisites: Go functions, methods, and control structures
  • Materials Needed: Go development environment, understanding of memory concepts

2. Learning Objectives and Session Outline (5 minutes)

What We’ll Cover Today:

  1. Arrays Fundamentals - Fixed-size collections and their limitations
  2. Slice Mastery - Dynamic arrays and advanced operations
  3. Map Operations - Key-value stores and hash table concepts
  4. Performance Considerations - Memory usage and optimization techniques
  5. Real-world Applications - Data processing, caching, and algorithms
  6. Advanced Patterns - Multi-dimensional data and complex structures
  7. Best Practices - Choosing the right data structure for your needs
  8. Common Pitfalls - Memory leaks, performance issues, and debugging

Success Criteria:

By the end of this session, you should be able to build efficient data processing applications, make informed decisions about data structure choice, and handle complex data manipulation tasks confidently.

3. Foundation Knowledge (15-20 minutes)

Understanding Go’s Collection Types

Go provides three main collection types, each optimized for different use cases:

Real-World Analogy: Think of data structures like different storage solutions:

  • Arrays: Like a parking garage with numbered spaces - fixed size, direct access
  • Slices: Like an expandable folder - can grow as needed, backed by arrays
  • Maps: Like a phone book - quick lookup by name (key) to find number (value)

Why This Matters: Choosing the right collection type affects:

  • Performance: Access speed and memory efficiency
  • Scalability: How your application handles growing data
  • Memory Usage: Efficient storage and garbage collection
  • Code Clarity: Expressing intent clearly through data structure choice

Core Concepts

Arrays vs Slices

  • Arrays: Fixed size, value types, rarely used directly
  • Slices: Dynamic size, reference types, built on top of arrays

Maps Characteristics

  • Hash tables: O(1) average access time
  • Reference types: Passed by reference to functions
  • Unordered: Iteration order is not guaranteed

Memory Model

Understanding how these structures work in memory is crucial for performance:

  • Arrays: Contiguous memory blocks
  • Slices: Header pointing to underlying array
  • Maps: Hash table with buckets and overflow chains

Key Terminology

  • Underlying Array: The actual array that backs a slice
  • Capacity: Maximum number of elements a slice can hold without reallocating
  • Length: Current number of elements in a slice or map
  • Zero Value: Empty slice (nil), empty map (nil), zero-filled array
  • Hash Function: Algorithm that converts keys into array indices
  • Bucket: Storage location in a hash table
  • Load Factor: Ratio of filled buckets to total buckets

4. Detailed Explanation and Examples (25-30 minutes)

Understanding Go’s Memory-Efficient Design

Go’s collection types are designed with performance and memory efficiency in mind:

Slice Internal Structure

Slice Header:
┌─────────┬────────┬────────┐
│ Pointer │ Length │Capacity│
└─────────┴────────┴────────┘
     │
     ▼
Underlying Array:
┌───┬───┬───┬───┬───┬───┐
│ 1 │ 2 │ 3 │ 4 │   │   │
└───┴───┴───┴───┴───┴───┘

Map Internal Structure

Maps use hash tables with separate chaining for collision resolution:

Hash Table:
┌─────┬─────┬─────┬─────┐
│Bucket│Bucket│Bucket│Bucket│
└─────┴─────┴─────┴─────┘
    │
    ▼
  ┌───────┬───────┬───────┐
  │Key|Val│Key|Val│Overflow│
  └───────┴───────┴───────┘

Practical Applications

These data structures are essential for:

  • Web Development: Request routing, session storage, response caching
  • Data Analysis: Processing CSV files, aggregating metrics
  • Algorithm Implementation: Sorting, searching, graph algorithms
  • System Programming: Configuration management, process tracking
  • Database Operations: In-memory caching, query result processing

Common Misconceptions

  1. Misconception: Arrays and slices are the same thing Reality: Arrays are fixed-size value types; slices are dynamic reference types

  2. Misconception: Maps maintain insertion order Reality: Map iteration order is randomized for security reasons

  3. Misconception: Appending to slices is always expensive Reality: Go’s growth strategy makes append amortized O(1)

5. Live Coding Demonstration (20-25 minutes)

Example 1: Arrays - The Foundation

package main

import "fmt"

func main() {
    fmt.Println("=== Array Basics ===")
    
    // Array declaration and initialization
    var numbers [5]int  // Zero-initialized array
    fmt.Printf("Zero array: %v\n", numbers)
    
    // Array literal
    primes := [5]int{2, 3, 5, 7, 11}
    fmt.Printf("Prime numbers: %v\n", primes)
    
    // Compiler-determined length
    colors := [...]string{"red", "green", "blue"}
    fmt.Printf("Colors: %v (length: %d)\n", colors, len(colors))
    
    // Sparse initialization
    sparse := [10]int{1: 100, 5: 500, 9: 900}
    fmt.Printf("Sparse array: %v\n", sparse)
    
    // Accessing and modifying elements
    primes[0] = 1  // Not a prime, but for demonstration
    fmt.Printf("Modified primes: %v\n", primes)
    
    // Array comparison (same type and size)
    arr1 := [3]int{1, 2, 3}
    arr2 := [3]int{1, 2, 3}
    arr3 := [3]int{1, 2, 4}
    
    fmt.Printf("arr1 == arr2: %t\n", arr1 == arr2)  // true
    fmt.Printf("arr1 == arr3: %t\n", arr1 == arr3)  // false
    
    // Arrays are value types (copied when passed)
    fmt.Println("\n=== Array Copy Behavior ===")
    original := [3]int{1, 2, 3}
    modified := original
    modified[0] = 999
    
    fmt.Printf("Original: %v\n", original)  // [1 2 3]
    fmt.Printf("Modified: %v\n", modified)  // [999 2 3]
    
    // Multi-dimensional arrays
    fmt.Println("\n=== Multi-dimensional Arrays ===")
    var matrix [3][3]int
    
    // Initialize matrix
    for i := 0; i < 3; i++ {
        for j := 0; j < 3; j++ {
            matrix[i][j] = (i+1)*10 + (j+1)
        }
    }
    
    // Print matrix
    for i := 0; i < 3; i++ {
        fmt.Printf("Row %d: %v\n", i, matrix[i])
    }
    
    // Array limitations
    fmt.Println("\n=== Array Limitations ===")
    // var flexibleArray [n]int  // Error: n must be constant
    // different := [4]int{1, 2, 3, 4}
    // arr1 = different  // Error: different types [3]int vs [4]int
}

Explanation:

  • Lines 7-9: Arrays are zero-initialized by default
  • Lines 12-13: Array literals provide convenient initialization
  • Lines 16-17: [...] lets compiler count elements
  • Lines 20-21: Sparse initialization sets specific indices
  • Lines 28-32: Arrays of same type and size can be compared
  • Lines 36-42: Arrays are value types - assignment creates copies

Example 2: Slices - Dynamic and Powerful

package main

import "fmt"

func main() {
    fmt.Println("=== Slice Basics ===")
    
    // Slice declaration (zero value is nil)
    var numbers []int
    fmt.Printf("Nil slice: %v, len: %d, cap: %d, nil: %t\n", 
               numbers, len(numbers), cap(numbers), numbers == nil)
    
    // Slice literal
    fruits := []string{"apple", "banana", "cherry"}
    fmt.Printf("Fruits: %v, len: %d, cap: %d\n", 
               fruits, len(fruits), cap(fruits))
    
    // Make function - create slice with specific length/capacity
    scores := make([]int, 5)      // length 5, capacity 5
    buffer := make([]byte, 0, 10) // length 0, capacity 10
    
    fmt.Printf("Scores: %v, len: %d, cap: %d\n", 
               scores, len(scores), cap(scores))
    fmt.Printf("Buffer: %v, len: %d, cap: %d\n", 
               buffer, len(buffer), cap(buffer))
    
    fmt.Println("\n=== Slice Operations ===")
    
    // Appending elements
    numbers = append(numbers, 1, 2, 3)
    fmt.Printf("After append: %v, len: %d, cap: %d\n", 
               numbers, len(numbers), cap(numbers))
    
    // Append another slice
    moreNumbers := []int{4, 5, 6}
    numbers = append(numbers, moreNumbers...)
    fmt.Printf("After append slice: %v, len: %d, cap: %d\n", 
               numbers, len(numbers), cap(numbers))
    
    // Slicing operations
    fmt.Println("\n=== Slicing Operations ===")
    data := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
    fmt.Printf("Original: %v\n", data)
    
    // Basic slicing
    fmt.Printf("data[2:5]: %v\n", data[2:5])    // [2 3 4]
    fmt.Printf("data[:3]: %v\n", data[:3])      // [0 1 2]
    fmt.Printf("data[7:]: %v\n", data[7:])      // [7 8 9]
    fmt.Printf("data[:]: %v\n", data[:])        // Full slice
    
    // Slicing with capacity
    subset := data[2:5:8]  // slice[low:high:max]
    fmt.Printf("Subset: %v, len: %d, cap: %d\n", 
               subset, len(subset), cap(subset))
    
    // Copy function
    fmt.Println("\n=== Copy Operations ===")
    source := []int{1, 2, 3, 4, 5}
    destination := make([]int, len(source))
    
    copied := copy(destination, source)
    fmt.Printf("Copied %d elements: %v\n", copied, destination)
    
    // Partial copy
    partial := make([]int, 3)
    copied = copy(partial, source)
    fmt.Printf("Partial copy (%d elements): %v\n", copied, partial)
    
    // Demonstrating shared underlying array
    fmt.Println("\n=== Shared Underlying Array ===")
    original := []int{1, 2, 3, 4, 5}
    slice1 := original[1:4]
    slice2 := original[2:5]
    
    fmt.Printf("Original: %v\n", original)
    fmt.Printf("Slice1: %v\n", slice1)
    fmt.Printf("Slice2: %v\n", slice2)
    
    // Modify through slice1
    slice1[1] = 999  // This is original[2]
    fmt.Printf("After modifying slice1[1]:\n")
    fmt.Printf("Original: %v\n", original)
    fmt.Printf("Slice1: %v\n", slice1)
    fmt.Printf("Slice2: %v\n", slice2)  // slice2[0] also changed!
    
    // Growth and reallocation
    fmt.Println("\n=== Growth and Reallocation ===")
    small := []int{1, 2}
    fmt.Printf("Small: %v, len: %d, cap: %d\n", small, len(small), cap(small))
    
    for i := 0; i < 10; i++ {
        small = append(small, i*10)
        fmt.Printf("After append %d: len=%d, cap=%d\n", 
                   i*10, len(small), cap(small))
    }
}

// Demonstrate slice as function parameter
func modifySlice(s []int) {
    if len(s) > 0 {
        s[0] = 999  // Modifies original slice's underlying array
    }
}

func appendToSlice(s []int) []int {
    return append(s, 100)  // May or may not modify original
}

func demonstrateSliceParameters() {
    fmt.Println("\n=== Slice Function Parameters ===")
    
    original := []int{1, 2, 3, 4, 5}
    fmt.Printf("Before modifySlice: %v\n", original)
    
    modifySlice(original)
    fmt.Printf("After modifySlice: %v\n", original)
    
    result := appendToSlice(original)
    fmt.Printf("Original after append: %v\n", original)
    fmt.Printf("Returned slice: %v\n", result)
}

Example 3: Maps - Key-Value Excellence

package main

import (
    "fmt"
    "sort"
)

func main() {
    fmt.Println("=== Map Basics ===")
    
    // Map declaration (zero value is nil)
    var scores map[string]int
    fmt.Printf("Nil map: %v, nil: %t\n", scores, scores == nil)
    
    // Map literal
    ages := map[string]int{
        "Alice": 25,
        "Bob":   30,
        "Carol": 35,
    }
    fmt.Printf("Ages: %v\n", ages)
    
    // Make function
    inventory := make(map[string]int)
    grades := make(map[string]int, 10)  // Initial capacity hint
    
    fmt.Printf("Empty inventory: %v\n", inventory)
    fmt.Printf("Empty grades: %v\n", grades)
    
    fmt.Println("\n=== Map Operations ===")
    
    // Adding/updating elements
    inventory["apples"] = 50
    inventory["bananas"] = 30
    inventory["oranges"] = 25
    
    fmt.Printf("Inventory: %v\n", inventory)
    
    // Accessing elements
    appleCount := inventory["apples"]
    fmt.Printf("Apple count: %d\n", appleCount)
    
    // Zero value for missing keys
    grapeCount := inventory["grapes"]
    fmt.Printf("Grape count (missing): %d\n", grapeCount)
    
    // Checking if key exists
    count, exists := inventory["grapes"]
    fmt.Printf("Grapes: count=%d, exists=%t\n", count, exists)
    
    bananas, exists := inventory["bananas"]
    fmt.Printf("Bananas: count=%d, exists=%t\n", bananas, exists)
    
    // Deleting elements
    fmt.Println("\n=== Deletion ===")
    fmt.Printf("Before delete: %v\n", inventory)
    delete(inventory, "bananas")
    fmt.Printf("After delete bananas: %v\n", inventory)
    
    // Deleting non-existent key (safe operation)
    delete(inventory, "grapes")
    fmt.Printf("After delete grapes (non-existent): %v\n", inventory)
    
    // Iteration (order is random)
    fmt.Println("\n=== Iteration ===")
    for key, value := range inventory {
        fmt.Printf("%s: %d\n", key, value)
    }
    
    // Keys only
    fmt.Println("Keys only:")
    for key := range inventory {
        fmt.Printf("- %s\n", key)
    }
    
    // Values only
    fmt.Println("Values only:")
    for _, value := range inventory {
        fmt.Printf("- %d\n", value)
    }
    
    // Map length
    fmt.Printf("Map length: %d\n", len(inventory))
    
    // Sorted iteration (manual sorting)
    fmt.Println("\n=== Sorted Iteration ===")
    keys := make([]string, 0, len(inventory))
    for key := range inventory {
        keys = append(keys, key)
    }
    sort.Strings(keys)
    
    fmt.Println("Sorted by key:")
    for _, key := range keys {
        fmt.Printf("%s: %d\n", key, inventory[key])
    }
    
    // Map of maps (nested)
    fmt.Println("\n=== Nested Maps ===")
    students := map[string]map[string]int{
        "Alice": {
            "Math":    95,
            "Science": 88,
            "English": 92,
        },
        "Bob": {
            "Math":    78,
            "Science": 85,
            "English": 80,
        },
    }
    
    for student, subjects := range students {
        fmt.Printf("%s's grades:\n", student)
        for subject, grade := range subjects {
            fmt.Printf("  %s: %d\n", subject, grade)
        }
    }
    
    // Map with slice values
    fmt.Println("\n=== Map with Slice Values ===")
    groups := map[string][]string{
        "fruits":     {"apple", "banana", "orange"},
        "vegetables": {"carrot", "broccoli", "spinach"},
        "grains":     {"rice", "wheat", "oats"},
    }
    
    for category, items := range groups {
        fmt.Printf("%s: %v\n", category, items)
    }
    
    // Add item to slice in map
    groups["fruits"] = append(groups["fruits"], "grape")
    fmt.Printf("Updated fruits: %v\n", groups["fruits"])
}

// Demonstrate map as function parameter
func updateInventory(inv map[string]int, item string, quantity int) {
    inv[item] = quantity  // Modifies original map
}

func copyMap(original map[string]int) map[string]int {
    copy := make(map[string]int)
    for key, value := range original {
        copy[key] = value
    }
    return copy
}

func demonstrateMapParameters() {
    fmt.Println("\n=== Map Function Parameters ===")
    
    inventory := map[string]int{
        "apples":  10,
        "bananas": 5,
    }
    
    fmt.Printf("Before update: %v\n", inventory)
    updateInventory(inventory, "oranges", 15)
    fmt.Printf("After update: %v\n", inventory)
    
    // Maps are reference types
    inventoryCopy := copyMap(inventory)
    inventoryCopy["apples"] = 999
    
    fmt.Printf("Original: %v\n", inventory)
    fmt.Printf("Copy: %v\n", inventoryCopy)
}

Example 4: Advanced Patterns and Real-World Applications

package main

import (
    "fmt"
    "strings"
)

// Word frequency counter
func countWords(text string) map[string]int {
    words := strings.Fields(strings.ToLower(text))
    frequency := make(map[string]int)
    
    for _, word := range words {
        // Clean word (remove punctuation for better counting)
        word = strings.Trim(word, ".,!?;:")
        if word != "" {
            frequency[word]++
        }
    }
    
    return frequency
}

// Find most frequent words
func findTopWords(frequency map[string]int, n int) []string {
    type wordCount struct {
        word  string
        count int
    }
    
    // Convert map to slice for sorting
    var words []wordCount
    for word, count := range frequency {
        words = append(words, wordCount{word, count})
    }
    
    // Simple bubble sort (for demonstration)
    for i := 0; i < len(words); i++ {
        for j := i + 1; j < len(words); j++ {
            if words[i].count < words[j].count {
                words[i], words[j] = words[j], words[i]
            }
        }
    }
    
    // Extract top n words
    result := make([]string, 0, n)
    for i := 0; i < n && i < len(words); i++ {
        result = append(result, words[i].word)
    }
    
    return result
}

// Matrix operations using slices
func createMatrix(rows, cols int) [][]int {
    matrix := make([][]int, rows)
    for i := range matrix {
        matrix[i] = make([]int, cols)
    }
    return matrix
}

func fillMatrix(matrix [][]int, value int) {
    for i := range matrix {
        for j := range matrix[i] {
            matrix[i][j] = value + i*len(matrix[i]) + j
        }
    }
}

func printMatrix(matrix [][]int) {
    for i, row := range matrix {
        fmt.Printf("Row %d: %v\n", i, row)
    }
}

// Cache implementation using map
type Cache struct {
    data map[string]interface{}
    maxSize int
}

func NewCache(maxSize int) *Cache {
    return &Cache{
        data: make(map[string]interface{}),
        maxSize: maxSize,
    }
}

func (c *Cache) Set(key string, value interface{}) {
    // Simple eviction: if full, clear everything (naive approach)
    if len(c.data) >= c.maxSize {
        c.data = make(map[string]interface{})
    }
    c.data[key] = value
}

func (c *Cache) Get(key string) (interface{}, bool) {
    value, exists := c.data[key]
    return value, exists
}

func (c *Cache) Size() int {
    return len(c.data)
}

// Slice manipulation utilities
func removeElement(slice []int, index int) []int {
    if index < 0 || index >= len(slice) {
        return slice
    }
    
    // Method 1: Preserve order
    return append(slice[:index], slice[index+1:]...)
}

func removeElementFast(slice []int, index int) []int {
    if index < 0 || index >= len(slice) {
        return slice
    }
    
    // Method 2: Faster, but doesn't preserve order
    slice[index] = slice[len(slice)-1]
    return slice[:len(slice)-1]
}

func insertElement(slice []int, index int, value int) []int {
    if index < 0 || index > len(slice) {
        return slice
    }
    
    // Expand slice
    slice = append(slice, 0)
    
    // Shift elements
    copy(slice[index+1:], slice[index:])
    
    // Insert new value
    slice[index] = value
    
    return slice
}

func main() {
    // Word frequency analysis
    fmt.Println("=== Word Frequency Analysis ===")
    text := "Go is great. Go is fast. Go is simple. Programming in Go is fun."
    frequency := countWords(text)
    
    fmt.Printf("Text: %s\n", text)
    fmt.Printf("Word frequencies: %v\n", frequency)
    
    topWords := findTopWords(frequency, 3)
    fmt.Printf("Top 3 words: %v\n", topWords)
    
    // Matrix operations
    fmt.Println("\n=== Matrix Operations ===")
    matrix := createMatrix(3, 4)
    fillMatrix(matrix, 1)
    printMatrix(matrix)
    
    // Cache demonstration
    fmt.Println("\n=== Cache Implementation ===")
    cache := NewCache(3)
    
    cache.Set("user1", "Alice")
    cache.Set("user2", "Bob")
    cache.Set("user3", "Carol")
    
    fmt.Printf("Cache size: %d\n", cache.Size())
    
    if value, exists := cache.Get("user2"); exists {
        fmt.Printf("Found user2: %s\n", value)
    }
    
    // This should trigger eviction
    cache.Set("user4", "David")
    fmt.Printf("Cache size after adding user4: %d\n", cache.Size())
    
    if _, exists := cache.Get("user1"); !exists {
        fmt.Println("user1 was evicted")
    }
    
    // Slice manipulation
    fmt.Println("\n=== Slice Manipulation ===")
    numbers := []int{1, 2, 3, 4, 5}
    fmt.Printf("Original: %v\n", numbers)
    
    // Remove element at index 2
    numbers = removeElement(numbers, 2)
    fmt.Printf("After removing index 2: %v\n", numbers)
    
    // Insert element at index 1
    numbers = insertElement(numbers, 1, 99)
    fmt.Printf("After inserting 99 at index 1: %v\n", numbers)
    
    // Demonstrate fast removal (order not preserved)
    numbers2 := []int{10, 20, 30, 40, 50}
    fmt.Printf("Before fast removal: %v\n", numbers2)
    numbers2 = removeElementFast(numbers2, 1)
    fmt.Printf("After fast removal at index 1: %v\n", numbers2)
}

Debugging Demonstration

Show common collection-related errors and solutions:

Problem: Slice bounds out of range

// Problematic code
slice := []int{1, 2, 3}
// element := slice[5]  // panic: runtime error: index out of range

// Safe approach
if index >= 0 && index < len(slice) {
    element := slice[index]
    fmt.Println(element)
} else {
    fmt.Println("Index out of range")
}

Problem: Modifying slice during iteration

// Problematic - can cause unexpected behavior
slice := []int{1, 2, 3, 4, 5}
for i, v := range slice {
    if v%2 == 0 {
        slice = append(slice, v*10)  // Modifying slice during iteration
    }
}

// Better approach - iterate backwards or collect changes
var toAdd []int
for _, v := range slice {
    if v%2 == 0 {
        toAdd = append(toAdd, v*10)
    }
}
slice = append(slice, toAdd...)

6. Hands-On Practice (30-40 minutes)

Exercise 1: Data Processing Pipeline (10 minutes)

Objective: Build a data processing system using slices and maps

Instructions:

  1. Create a function to read CSV-like data (simulate with string slices)
  2. Process the data to extract statistics
  3. Use maps to group and aggregate data
  4. Handle missing or invalid data gracefully

Starter Code:

package main

import (
    "fmt"
    "strconv"
    "strings"
)

// CSV data simulation
var csvData = []string{
    "Name,Age,Department,Salary",
    "Alice,25,Engineering,75000",
    "Bob,30,Marketing,65000",
    "Carol,35,Engineering,85000",
    "David,28,Marketing,60000",
    "Eve,32,Engineering,80000",
}

// TODO: Implement functions to:
// 1. Parse CSV data into structured format
// 2. Calculate average salary by department
// 3. Find employees in specific age ranges
// 4. Generate summary statistics

func main() {
    // Test your functions here
}

Solution:

package main

import (
    "fmt"
    "strconv"
    "strings"
)

type Employee struct {
    Name       string
    Age        int
    Department string
    Salary     int
}

var csvData = []string{
    "Name,Age,Department,Salary",
    "Alice,25,Engineering,75000",
    "Bob,30,Marketing,65000",
    "Carol,35,Engineering,85000",
    "David,28,Marketing,60000",
    "Eve,32,Engineering,80000",
}

func parseCSV(data []string) ([]Employee, error) {
    if len(data) < 2 {
        return nil, fmt.Errorf("insufficient data")
    }
    
    var employees []Employee
    
    // Skip header row
    for i := 1; i < len(data); i++ {
        fields := strings.Split(data[i], ",")
        if len(fields) != 4 {
            continue // Skip malformed rows
        }
        
        age, err := strconv.Atoi(fields[1])
        if err != nil {
            continue // Skip invalid age
        }
        
        salary, err := strconv.Atoi(fields[3])
        if err != nil {
            continue // Skip invalid salary
        }
        
        employee := Employee{
            Name:       strings.TrimSpace(fields[0]),
            Age:        age,
            Department: strings.TrimSpace(fields[2]),
            Salary:     salary,
        }
        
        employees = append(employees, employee)
    }
    
    return employees, nil
}

func averageSalaryByDepartment(employees []Employee) map[string]float64 {
    departmentSums := make(map[string]int)
    departmentCounts := make(map[string]int)
    
    for _, emp := range employees {
        departmentSums[emp.Department] += emp.Salary
        departmentCounts[emp.Department]++
    }
    
    averages := make(map[string]float64)
    for dept := range departmentSums {
        averages[dept] = float64(departmentSums[dept]) / float64(departmentCounts[dept])
    }
    
    return averages
}

func findEmployeesByAgeRange(employees []Employee, minAge, maxAge int) []Employee {
    var result []Employee
    
    for _, emp := range employees {
        if emp.Age >= minAge && emp.Age <= maxAge {
            result = append(result, emp)
        }
    }
    
    return result
}

func generateSummary(employees []Employee) map[string]interface{} {
    if len(employees) == 0 {
        return map[string]interface{}{"error": "no employees"}
    }
    
    totalSalary := 0
    minSalary := employees[0].Salary
    maxSalary := employees[0].Salary
    departments := make(map[string]bool)
    
    for _, emp := range employees {
        totalSalary += emp.Salary
        if emp.Salary < minSalary {
            minSalary = emp.Salary
        }
        if emp.Salary > maxSalary {
            maxSalary = emp.Salary
        }
        departments[emp.Department] = true
    }
    
    departmentCount := len(departments)
    avgSalary := float64(totalSalary) / float64(len(employees))
    
    return map[string]interface{}{
        "total_employees":   len(employees),
        "average_salary":    avgSalary,
        "min_salary":        minSalary,
        "max_salary":        maxSalary,
        "department_count":  departmentCount,
    }
}

func main() {
    employees, err := parseCSV(csvData)
    if err != nil {
        fmt.Printf("Error parsing CSV: %s\n", err)
        return
    }
    
    fmt.Printf("Parsed %d employees\n\n", len(employees))
    
    // Average salary by department
    avgSalaries := averageSalaryByDepartment(employees)
    fmt.Println("Average salaries by department:")
    for dept, avg := range avgSalaries {
        fmt.Printf("  %s: $%.2f\n", dept, avg)
    }
    
    // Employees in age range 25-30
    youngEmployees := findEmployeesByAgeRange(employees, 25, 30)
    fmt.Printf("\nEmployees aged 25-30 (%d):\n", len(youngEmployees))
    for _, emp := range youngEmployees {
        fmt.Printf("  %s: %d years old\n", emp.Name, emp.Age)
    }
    
    // Summary statistics
    summary := generateSummary(employees)
    fmt.Println("\nSummary statistics:")
    for key, value := range summary {
        fmt.Printf("  %s: %v\n", key, value)
    }
}

Exercise 2: Text Analysis Tool (15 minutes)

Objective: Create a comprehensive text analysis tool using maps and slices

Requirements:

  • Count word frequencies, character frequencies, and sentence lengths
  • Find palindromes and anagrams
  • Generate reading statistics (average word length, etc.)
  • Support multiple text inputs

Instructions:

  1. Create functions for different types of analysis
  2. Use maps to store frequency data
  3. Implement helper functions for text cleaning
  4. Generate comprehensive reports

Hints:

  • Use strings package for text manipulation
  • Consider case sensitivity in your analysis
  • Think about how to handle punctuation

Exercise 3: Inventory Management System (15 minutes)

Objective: Build a complete inventory system using all collection types

Challenge: Create a multi-featured inventory management system

Requirements:

  • Product catalog with categories
  • Stock tracking with locations
  • Sales history and analytics
  • Low stock alerts and reorder suggestions

Extension Ideas:

  • Add supplier information and relationships
  • Implement batch operations for stock updates
  • Create reports for best-selling items
  • Add price history tracking

7. Common Pitfalls and Troubleshooting (10 minutes)

Frequent Mistakes

  1. Problem: Slice bounds errors and panics Cause: Not checking slice length before accessing indices Solution: Always validate indices: if i >= 0 && i < len(slice)

  2. Problem: Memory leaks with large slices Cause: Keeping references to large underlying arrays Solution: Copy data when storing small slices from large ones

  3. Problem: Map concurrent access panics Cause: Multiple goroutines reading/writing maps simultaneously Solution: Use sync.RWMutex or sync.Map for concurrent access

Debugging Tips

  • Print lengths and capacities to understand slice behavior
  • **Use fmt.Printf("%p", slice) to check if slices share memory
  • Check for nil maps before accessing: if m != nil
  • Use go vet to catch common slice/map issues

Performance Considerations

  • Pre-allocate slices when size is known: make([]int, 0, expectedSize)
  • Use maps for O(1) lookup instead of linear search in slices
  • Consider memory usage with large maps and slices
  • Be aware of map growth and rehashing costs

8. Best Practices and Professional Tips (5 minutes)

Industry Standards

  • Nil Checks: Always check if maps are nil before use
  • Capacity Planning: Pre-allocate collections when size is predictable
  • Memory Management: Be conscious of slice growth and map capacity
  • Error Handling: Validate indices and handle missing map keys gracefully

Code Organization

// Use meaningful names for collection types
type UsersByID map[string]*User
type MessageQueue []Message
type ConfigOptions map[string]interface{}

// Helper functions for common operations
func (users UsersByID) GetByEmail(email string) *User {
    for _, user := range users {
        if user.Email == email {
            return user
        }
    }
    return nil
}

Performance Optimization

  • Benchmark different approaches for your specific use case
  • Profile memory usage with large datasets
  • Consider alternative data structures for specific needs
  • Use sync.Pool for frequently allocated/deallocated slices

Next Steps in Your Learning Journey

  • Structs and Interfaces: Build complex data structures
  • Concurrency: Learn to use collections safely with goroutines
  • Algorithms: Implement sorting and searching algorithms
  • Database Integration: Map between Go collections and database records

9. Summary and Review (5 minutes)

Key Takeaways

  1. Arrays vs Slices: Arrays are fixed-size; slices are dynamic and more commonly used
  2. Memory Efficiency: Understanding underlying arrays and capacity helps optimize performance
  3. Map Operations: Maps provide fast key-value lookup with O(1) average performance
  4. Reference Types: Slices and maps are reference types - be aware of sharing

Quick Review Questions

  1. What’s the difference between length and capacity of a slice?
  2. When do you need to check if a map key exists vs just accessing it?
  3. How do you safely remove an element from a slice?
  4. Why might iterating over a map give different orders each time?

Answers:

  1. Length is current elements; capacity is max elements without reallocation
  2. Check existence when zero value is a valid value in your domain
  3. Use append(slice[:i], slice[i+1:]...) or swap with last element
  4. Go randomizes map iteration order for security (hash DoS prevention)

For Instructors

Teaching Tips

  • Use visual diagrams to show slice headers and underlying arrays
  • Demonstrate memory sharing with concrete examples
  • Show performance differences between different approaches
  • Emphasize practical applications over theoretical concepts

Assessment Ideas

  • Collection Choice: Present scenarios and ask which collection type to use
  • Performance Analysis: Have students identify inefficient code patterns
  • Debugging: Provide buggy code with collection issues to fix
  • Real-world Projects: Build systems like caches, indexes, or data processors

Adaptation Notes

  • Beginner Focus: Start with simple operations before advanced patterns
  • Advanced Students: Explore assembly output and memory layouts
  • Visual Learners: Use diagrams extensively for slice/map internals
  • Time Management: Collection concepts can be complex - allow extra practice time

Common Student Questions

  1. “When should I use array vs slice?” - Almost always use slices unless you need fixed size
  2. “How do I sort a map?” - Extract keys to slice, sort slice, iterate in order
  3. “Why does my slice change when I don’t expect it?” - Likely sharing underlying array
  4. “How do I copy a map?” - Iterate and copy key-value pairs to new map