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:
- Arrays Fundamentals - Fixed-size collections and their limitations
- Slice Mastery - Dynamic arrays and advanced operations
- Map Operations - Key-value stores and hash table concepts
- Performance Considerations - Memory usage and optimization techniques
- Real-world Applications - Data processing, caching, and algorithms
- Advanced Patterns - Multi-dimensional data and complex structures
- Best Practices - Choosing the right data structure for your needs
- 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
-
Misconception: Arrays and slices are the same thing Reality: Arrays are fixed-size value types; slices are dynamic reference types
-
Misconception: Maps maintain insertion order Reality: Map iteration order is randomized for security reasons
-
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:
- Create a function to read CSV-like data (simulate with string slices)
- Process the data to extract statistics
- Use maps to group and aggregate data
- 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:
- Create functions for different types of analysis
- Use maps to store frequency data
- Implement helper functions for text cleaning
- Generate comprehensive reports
Hints:
- Use
stringspackage 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
-
Problem: Slice bounds errors and panics Cause: Not checking slice length before accessing indices Solution: Always validate indices:
if i >= 0 && i < len(slice) -
Problem: Memory leaks with large slices Cause: Keeping references to large underlying arrays Solution: Copy data when storing small slices from large ones
-
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 vetto 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
- Arrays vs Slices: Arrays are fixed-size; slices are dynamic and more commonly used
- Memory Efficiency: Understanding underlying arrays and capacity helps optimize performance
- Map Operations: Maps provide fast key-value lookup with O(1) average performance
- Reference Types: Slices and maps are reference types - be aware of sharing
Quick Review Questions
- What’s the difference between length and capacity of a slice?
- When do you need to check if a map key exists vs just accessing it?
- How do you safely remove an element from a slice?
- Why might iterating over a map give different orders each time?
Answers:
- Length is current elements; capacity is max elements without reallocation
- Check existence when zero value is a valid value in your domain
- Use
append(slice[:i], slice[i+1:]...)or swap with last element - 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
- “When should I use array vs slice?” - Almost always use slices unless you need fixed size
- “How do I sort a map?” - Extract keys to slice, sort slice, iterate in order
- “Why does my slice change when I don’t expect it?” - Likely sharing underlying array
- “How do I copy a map?” - Iterate and copy key-value pairs to new map