Navigation

Go

Go Map Operations and Best Practices for Key-Value Storage 2025

Master Go maps with comprehensive guide covering operations, performance optimization, concurrency safety, and best practices for efficient key-value storage in Go applications 2025.

Table Of Contents

Introduction

Maps are one of Go's most versatile and commonly used built-in data structures, providing efficient key-value storage with O(1) average-case lookup time. Whether you're building a cache, counting occurrences, grouping data, or implementing lookup tables, understanding how to effectively use Go maps is crucial for writing performant applications.

Many developers treat maps as simple dictionaries without considering their internal implementation, memory characteristics, or performance implications. However, Go maps have unique behaviors and optimization opportunities that can significantly impact your application's performance and reliability, especially when dealing with large datasets or concurrent access patterns.

In this comprehensive guide, we'll explore Go map fundamentals, dive deep into advanced operations, examine performance characteristics, and cover concurrent access patterns. You'll learn how to choose appropriate key types, optimize memory usage, handle edge cases, and implement map-based algorithms efficiently.

Map Fundamentals

Basic Map Operations

Let's start with the essential map operations in Go:

package main

import "fmt"

func main() {
    // Declaration and initialization
    var m1 map[string]int               // nil map
    m2 := make(map[string]int)          // empty map
    m3 := map[string]int{               // map literal
        "apple":  5,
        "banana": 3,
        "orange": 8,
    }
    
    fmt.Printf("m1: %v (nil: %t)\n", m1, m1 == nil)
    fmt.Printf("m2: %v (len: %d)\n", m2, len(m2))
    fmt.Printf("m3: %v (len: %d)\n", m3, len(m3))
    
    // Adding elements
    m2["key1"] = 100
    m2["key2"] = 200
    
    // Reading elements
    value1 := m2["key1"]           // Returns zero value if key doesn't exist
    value2, ok := m2["key3"]       // ok indicates if key exists
    
    fmt.Printf("value1: %d\n", value1)
    fmt.Printf("value2: %d, exists: %t\n", value2, ok)
    
    // Modifying elements
    m2["key1"] = 150
    
    // Deleting elements
    delete(m2, "key2")
    
    fmt.Printf("Final m2: %v\n", m2)
    
    // Iterating over maps
    fmt.Println("Iterating over m3:")
    for key, value := range m3 {
        fmt.Printf("  %s: %d\n", key, value)
    }
}

Map Types and Key Constraints

Go maps support various key types with specific requirements:

package main

import "fmt"

// Valid key types
func demonstrateKeyTypes() {
    // Basic types
    intMap := map[int]string{1: "one", 2: "two"}
    stringMap := map[string]int{"a": 1, "b": 2}
    floatMap := map[float64]bool{3.14: true, 2.71: false}
    
    // Struct keys (must be comparable)
    type Point struct {
        X, Y int
    }
    pointMap := map[Point]string{
        {0, 0}: "origin",
        {1, 1}: "diagonal",
    }
    
    // Array keys (fixed size)
    type Coordinate [2]int
    coordMap := map[Coordinate]string{
        {0, 0}: "start",
        {10, 10}: "end",
    }
    
    // Pointer keys
    type User struct {
        Name string
        Age  int
    }
    user1 := &User{"Alice", 30}
    user2 := &User{"Bob", 25}
    userMap := map[*User]string{
        user1: "admin",
        user2: "user",
    }
    
    fmt.Printf("intMap: %v\n", intMap)
    fmt.Printf("stringMap: %v\n", stringMap)
    fmt.Printf("floatMap: %v\n", floatMap)
    fmt.Printf("pointMap: %v\n", pointMap)
    fmt.Printf("coordMap: %v\n", coordMap)
    fmt.Printf("userMap: %v\n", userMap)
}

// Invalid key types (uncomment to see compilation errors)
func invalidKeyTypes() {
    // Slices are not comparable
    // sliceMap := map[[]int]string{} // Won't compile
    
    // Maps are not comparable
    // mapMap := map[map[string]int]string{} // Won't compile
    
    // Functions are not comparable
    // funcMap := map[func()]string{} // Won't compile
}

func main() {
    demonstrateKeyTypes()
}

Advanced Map Operations

Checking for Key Existence

Proper key existence checking is crucial for robust map operations:

package main

import "fmt"

func demonstrateKeyExistence() {
    counts := map[string]int{
        "apple":  5,
        "banana": 0,  // Zero value but key exists
    }
    
    // Wrong way: only checking value
    if counts["apple"] > 0 {
        fmt.Println("apple exists and has positive count")
    }
    
    if counts["banana"] > 0 {
        fmt.Println("banana exists and has positive count")
    } else {
        fmt.Println("banana doesn't exist OR has zero/negative count")
    }
    
    // Correct way: checking existence
    if count, exists := counts["apple"]; exists {
        fmt.Printf("apple exists with count: %d\n", count)
    }
    
    if count, exists := counts["banana"]; exists {
        fmt.Printf("banana exists with count: %d\n", count)
    } else {
        fmt.Println("banana doesn't exist")
    }
    
    if count, exists := counts["orange"]; exists {
        fmt.Printf("orange exists with count: %d\n", count)
    } else {
        fmt.Println("orange doesn't exist")
    }
}

// Helper function for safe map access
func getOrDefault[K comparable, V any](m map[K]V, key K, defaultValue V) V {
    if value, exists := m[key]; exists {
        return value
    }
    return defaultValue
}

func demonstrateHelperFunctions() {
    scores := map[string]int{
        "Alice": 95,
        "Bob":   87,
    }
    
    aliceScore := getOrDefault(scores, "Alice", 0)
    charlieScore := getOrDefault(scores, "Charlie", 0)
    
    fmt.Printf("Alice's score: %d\n", aliceScore)
    fmt.Printf("Charlie's score: %d\n", charlieScore)
}

func main() {
    demonstrateKeyExistence()
    fmt.Println()
    demonstrateHelperFunctions()
}

Map Copying and Merging

Maps are reference types, so copying requires careful consideration:

package main

import "fmt"

// Shallow copy - copies map structure but not values
func shallowCopy[K comparable, V any](original map[K]V) map[K]V {
    copy := make(map[K]V, len(original))
    for key, value := range original {
        copy[key] = value
    }
    return copy
}

// Deep copy for maps with pointer/slice values
func deepCopyStringSliceMap(original map[string][]string) map[string][]string {
    copy := make(map[string][]string, len(original))
    for key, value := range original {
        // Deep copy the slice
        copySlice := make([]string, len(value))
        copy(copySlice, value)
        copy[key] = copySlice
    }
    return copy
}

// Merge multiple maps
func mergeMaps[K comparable, V any](maps ...map[K]V) map[K]V {
    result := make(map[K]V)
    for _, m := range maps {
        for key, value := range m {
            result[key] = value
        }
    }
    return result
}

// Merge with conflict resolution
func mergeWithResolver[K comparable, V any](resolver func(V, V) V, maps ...map[K]V) map[K]V {
    result := make(map[K]V)
    for _, m := range maps {
        for key, value := range m {
            if existing, exists := result[key]; exists {
                result[key] = resolver(existing, value)
            } else {
                result[key] = value
            }
        }
    }
    return result
}

func demonstrateCopyingAndMerging() {
    // Original map
    original := map[string]int{
        "a": 1,
        "b": 2,
        "c": 3,
    }
    
    // Shallow copy
    copied := shallowCopy(original)
    copied["d"] = 4
    
    fmt.Printf("Original: %v\n", original)
    fmt.Printf("Copied: %v\n", copied)
    
    // Map with slice values
    categories := map[string][]string{
        "fruits":     {"apple", "banana"},
        "vegetables": {"carrot", "broccoli"},
    }
    
    categoriesCopy := deepCopyStringSliceMap(categories)
    categoriesCopy["fruits"] = append(categoriesCopy["fruits"], "orange")
    
    fmt.Printf("Original categories: %v\n", categories)
    fmt.Printf("Copied categories: %v\n", categoriesCopy)
    
    // Merging maps
    map1 := map[string]int{"a": 1, "b": 2}
    map2 := map[string]int{"c": 3, "d": 4}
    map3 := map[string]int{"a": 10, "e": 5} // "a" conflicts with map1
    
    merged := mergeMaps(map1, map2, map3)
    fmt.Printf("Merged (last wins): %v\n", merged)
    
    // Merge with sum resolver
    mergedSum := mergeWithResolver(func(existing, new int) int {
        return existing + new
    }, map1, map2, map3)
    fmt.Printf("Merged (sum): %v\n", mergedSum)
}

func main() {
    demonstrateCopyingAndMerging()
}

Map-Based Set Operations

Implementing set operations using maps:

package main

import (
    "fmt"
    "sort"
)

// Set implementation using map[T]bool
type Set[T comparable] map[T]bool

func NewSet[T comparable](items ...T) Set[T] {
    set := make(Set[T])
    for _, item := range items {
        set[item] = true
    }
    return set
}

func (s Set[T]) Add(item T) {
    s[item] = true
}

func (s Set[T]) Remove(item T) {
    delete(s, item)
}

func (s Set[T]) Contains(item T) bool {
    return s[item]
}

func (s Set[T]) Size() int {
    return len(s)
}

func (s Set[T]) ToSlice() []T {
    items := make([]T, 0, len(s))
    for item := range s {
        items = append(items, item)
    }
    return items
}

func (s Set[T]) Union(other Set[T]) Set[T] {
    result := make(Set[T])
    for item := range s {
        result[item] = true
    }
    for item := range other {
        result[item] = true
    }
    return result
}

func (s Set[T]) Intersection(other Set[T]) Set[T] {
    result := make(Set[T])
    for item := range s {
        if other.Contains(item) {
            result[item] = true
        }
    }
    return result
}

func (s Set[T]) Difference(other Set[T]) Set[T] {
    result := make(Set[T])
    for item := range s {
        if !other.Contains(item) {
            result[item] = true
        }
    }
    return result
}

func (s Set[T]) String() string {
    items := s.ToSlice()
    
    // Sort for consistent output (only works for some types)
    if len(items) > 0 {
        switch any(items[0]).(type) {
        case string:
            stringItems := make([]string, len(items))
            for i, item := range items {
                stringItems[i] = any(item).(string)
            }
            sort.Strings(stringItems)
            return fmt.Sprintf("Set%v", stringItems)
        case int:
            intItems := make([]int, len(items))
            for i, item := range items {
                intItems[i] = any(item).(int)
            }
            sort.Ints(intItems)
            return fmt.Sprintf("Set%v", intItems)
        }
    }
    
    return fmt.Sprintf("Set%v", items)
}

func demonstrateSetOperations() {
    // Create sets
    set1 := NewSet(1, 2, 3, 4)
    set2 := NewSet(3, 4, 5, 6)
    
    fmt.Printf("Set1: %v\n", set1)
    fmt.Printf("Set2: %v\n", set2)
    
    // Set operations
    union := set1.Union(set2)
    intersection := set1.Intersection(set2)
    difference := set1.Difference(set2)
    
    fmt.Printf("Union: %v\n", union)
    fmt.Printf("Intersection: %v\n", intersection)
    fmt.Printf("Set1 - Set2: %v\n", difference)
    
    // String sets
    fruits := NewSet("apple", "banana", "orange")
    citrus := NewSet("orange", "lemon", "lime")
    
    fmt.Printf("Fruits: %v\n", fruits)
    fmt.Printf("Citrus: %v\n", citrus)
    fmt.Printf("Citrus fruits: %v\n", fruits.Intersection(citrus))
}

func main() {
    demonstrateSetOperations()
}

Performance Optimization

Memory Preallocation

Optimizing map performance through proper initialization:

package main

import (
    "fmt"
    "runtime"
    "time"
)

func benchmarkMapAllocation() {
    const numElements = 100000
    const iterations = 100
    
    // Benchmark 1: Default initialization
    start := time.Now()
    var m1 runtime.MemStats
    runtime.ReadMemStats(&m1)
    
    for i := 0; i < iterations; i++ {
        m := make(map[int]int)
        for j := 0; j < numElements; j++ {
            m[j] = j * 2
        }
    }
    
    var m2 runtime.MemStats
    runtime.ReadMemStats(&m2)
    defaultTime := time.Since(start)
    defaultMemory := m2.TotalAlloc - m1.TotalAlloc
    
    // Benchmark 2: Pre-allocated map
    start = time.Now()
    runtime.ReadMemStats(&m1)
    
    for i := 0; i < iterations; i++ {
        m := make(map[int]int, numElements) // Pre-allocate
        for j := 0; j < numElements; j++ {
            m[j] = j * 2
        }
    }
    
    runtime.ReadMemStats(&m2)
    preallocTime := time.Since(start)
    preallocMemory := m2.TotalAlloc - m1.TotalAlloc
    
    fmt.Printf("Default allocation: %v, Memory: %d bytes\n", defaultTime, defaultMemory)
    fmt.Printf("Pre-allocated: %v, Memory: %d bytes\n", preallocTime, preallocMemory)
    fmt.Printf("Performance improvement: %.2fx\n", float64(defaultTime)/float64(preallocTime))
}

// Optimal map sizing
func optimalMapSize(expectedElements int) int {
    // Go maps resize when load factor exceeds ~6.5
    // Pre-allocate to avoid multiple resizes
    return expectedElements * 4 / 3
}

func demonstrateOptimalSizing() {
    expectedSize := 1000
    
    // Standard approach
    m1 := make(map[int]int)
    
    // Optimized approach
    m2 := make(map[int]int, optimalMapSize(expectedSize))
    
    fmt.Printf("Standard map initial capacity: maps don't expose capacity\n")
    fmt.Printf("Optimized map initial size hint: %d\n", optimalMapSize(expectedSize))
    
    // Both maps will work the same, but m2 will have fewer reallocations
    for i := 0; i < expectedSize; i++ {
        m1[i] = i
        m2[i] = i
    }
    
    fmt.Printf("Both maps final length: %d\n", len(m1))
}

func main() {
    fmt.Println("Map allocation benchmark:")
    benchmarkMapAllocation()
    
    fmt.Println("\nOptimal map sizing:")
    demonstrateOptimalSizing()
}

Efficient Key-Value Patterns

Choosing efficient key types and access patterns:

package main

import (
    "fmt"
    "strconv"
    "time"
)

// String key optimization
func benchmarkStringKeys() {
    const iterations = 1000000
    
    // Benchmark 1: String concatenation keys
    start := time.Now()
    m1 := make(map[string]int)
    for i := 0; i < iterations; i++ {
        key := "user_" + strconv.Itoa(i)
        m1[key] = i
    }
    stringKeyTime := time.Since(start)
    
    // Benchmark 2: Integer keys with custom type
    type UserID int
    start = time.Now()
    m2 := make(map[UserID]int)
    for i := 0; i < iterations; i++ {
        key := UserID(i)
        m2[key] = i
    }
    intKeyTime := time.Since(start)
    
    // Benchmark 3: Struct keys
    type CompositeKey struct {
        Category string
        ID       int
    }
    
    start = time.Now()
    m3 := make(map[CompositeKey]int)
    categories := []string{"user", "admin", "guest"}
    for i := 0; i < iterations/10; i++ { // Fewer iterations for struct keys
        key := CompositeKey{
            Category: categories[i%len(categories)],
            ID:       i,
        }
        m3[key] = i
    }
    structKeyTime := time.Since(start)
    
    fmt.Printf("String keys: %v\n", stringKeyTime)
    fmt.Printf("Integer keys: %v\n", intKeyTime)
    fmt.Printf("Struct keys: %v (fewer iterations)\n", structKeyTime)
    fmt.Printf("Integer vs String performance: %.2fx faster\n", 
        float64(stringKeyTime)/float64(intKeyTime))
}

// Memory-efficient value storage
func demonstrateValueOptimization() {
    // Bad: Storing large structs as values
    type UserProfile struct {
        Name        string
        Email       string
        Address     string
        Biography   string
        Preferences map[string]string
    }
    
    badMap := make(map[int]UserProfile)
    
    // Good: Storing pointers to reduce map overhead
    goodMap := make(map[int]*UserProfile)
    
    // Even better: Using indices into a slice
    profiles := make([]UserProfile, 0, 1000)
    indexMap := make(map[int]int) // maps user ID to slice index
    
    // Add some sample data
    for i := 0; i < 10; i++ {
        profile := UserProfile{
            Name:      fmt.Sprintf("User %d", i),
            Email:     fmt.Sprintf("user%d@example.com", i),
            Address:   fmt.Sprintf("%d Main St", i),
            Biography: fmt.Sprintf("This is user %d's biography", i),
            Preferences: map[string]string{
                "theme": "dark",
                "lang":  "en",
            },
        }
        
        badMap[i] = profile
        goodMap[i] = &profile
        
        profiles = append(profiles, profile)
        indexMap[i] = len(profiles) - 1
    }
    
    fmt.Printf("Bad map (value storage): %d entries\n", len(badMap))
    fmt.Printf("Good map (pointer storage): %d entries\n", len(goodMap))
    fmt.Printf("Index map: %d entries, %d profiles\n", len(indexMap), len(profiles))
    
    // Access patterns
    if profile, exists := badMap[5]; exists {
        fmt.Printf("Bad map access: %s\n", profile.Name)
    }
    
    if profile, exists := goodMap[5]; exists {
        fmt.Printf("Good map access: %s\n", profile.Name)
    }
    
    if index, exists := indexMap[5]; exists {
        profile := profiles[index]
        fmt.Printf("Index map access: %s\n", profile.Name)
    }
}

func main() {
    fmt.Println("String vs Integer key benchmark:")
    benchmarkStringKeys()
    
    fmt.Println("\nValue storage optimization:")
    demonstrateValueOptimization()
}

Concurrent Map Access

Problem with Concurrent Access

Go maps are not safe for concurrent access:

package main

import (
    "fmt"
    "sync"
    "time"
)

// Unsafe concurrent access (will panic or produce race conditions)
func unsafeConcurrentAccess() {
    m := make(map[int]int)
    var wg sync.WaitGroup
    
    // This will likely cause a panic or race condition
    defer func() {
        if r := recover(); r != nil {
            fmt.Printf("Recovered from panic: %v\n", r)
        }
    }()
    
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            for j := 0; j < 1000; j++ {
                m[id*1000+j] = j // Concurrent writes
            }
        }(i)
    }
    
    wg.Wait()
    fmt.Printf("Unsafe map size: %d\n", len(m))
}

func main() {
    fmt.Println("Demonstrating unsafe concurrent access:")
    unsafeConcurrentAccess()
}

Solution 1: Mutex Protection

package main

import (
    "fmt"
    "sync"
    "time"
)

// Thread-safe map with mutex
type SafeMap[K comparable, V any] struct {
    mu sync.RWMutex
    m  map[K]V
}

func NewSafeMap[K comparable, V any]() *SafeMap[K, V] {
    return &SafeMap[K, V]{
        m: make(map[K]V),
    }
}

func (sm *SafeMap[K, V]) Set(key K, value V) {
    sm.mu.Lock()
    defer sm.mu.Unlock()
    sm.m[key] = value
}

func (sm *SafeMap[K, V]) Get(key K) (V, bool) {
    sm.mu.RLock()
    defer sm.mu.RUnlock()
    value, exists := sm.m[key]
    return value, exists
}

func (sm *SafeMap[K, V]) Delete(key K) {
    sm.mu.Lock()
    defer sm.mu.Unlock()
    delete(sm.m, key)
}

func (sm *SafeMap[K, V]) Len() int {
    sm.mu.RLock()
    defer sm.mu.RUnlock()
    return len(sm.m)
}

func (sm *SafeMap[K, V]) Range(fn func(K, V) bool) {
    sm.mu.RLock()
    defer sm.mu.RUnlock()
    for key, value := range sm.m {
        if !fn(key, value) {
            break
        }
    }
}

// Bulk operations for better performance
func (sm *SafeMap[K, V]) SetMultiple(pairs map[K]V) {
    sm.mu.Lock()
    defer sm.mu.Unlock()
    for key, value := range pairs {
        sm.m[key] = value
    }
}

func (sm *SafeMap[K, V]) GetMultiple(keys []K) map[K]V {
    sm.mu.RLock()
    defer sm.mu.RUnlock()
    result := make(map[K]V)
    for _, key := range keys {
        if value, exists := sm.m[key]; exists {
            result[key] = value
        }
    }
    return result
}

func demonstrateSafeMap() {
    safeMap := NewSafeMap[int, string]()
    var wg sync.WaitGroup
    
    start := time.Now()
    
    // Concurrent writers
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            for j := 0; j < 1000; j++ {
                key := id*1000 + j
                safeMap.Set(key, fmt.Sprintf("value-%d", key))
            }
        }(i)
    }
    
    // Concurrent readers
    for i := 0; i < 5; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            count := 0
            for j := 0; j < 2000; j++ {
                if _, exists := safeMap.Get(j); exists {
                    count++
                }
            }
            fmt.Printf("Reader %d found %d keys\n", id, count)
        }(i)
    }
    
    wg.Wait()
    
    fmt.Printf("Safe map final size: %d\n", safeMap.Len())
    fmt.Printf("Time taken: %v\n", time.Since(start))
}

func main() {
    demonstrateSafeMap()
}

Solution 2: sync.Map

Using Go's built-in concurrent map:

package main

import (
    "fmt"
    "sync"
    "time"
)

func demonstrateSyncMap() {
    var syncMap sync.Map
    var wg sync.WaitGroup
    
    start := time.Now()
    
    // Concurrent writers
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            for j := 0; j < 1000; j++ {
                key := id*1000 + j
                syncMap.Store(key, fmt.Sprintf("value-%d", key))
            }
        }(i)
    }
    
    // Concurrent readers
    for i := 0; i < 5; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            count := 0
            for j := 0; j < 2000; j++ {
                if _, exists := syncMap.Load(j); exists {
                    count++
                }
            }
            fmt.Printf("Reader %d found %d keys\n", id, count)
        }(i)
    }
    
    wg.Wait()
    
    // Count total entries
    count := 0
    syncMap.Range(func(key, value interface{}) bool {
        count++
        return true
    })
    
    fmt.Printf("sync.Map final size: %d\n", count)
    fmt.Printf("Time taken: %v\n", time.Since(start))
    
    // Demonstrate LoadOrStore
    actual, loaded := syncMap.LoadOrStore("special-key", "default-value")
    fmt.Printf("LoadOrStore result: %v, was loaded: %t\n", actual, loaded)
    
    // Try again with the same key
    actual, loaded = syncMap.LoadOrStore("special-key", "another-value")
    fmt.Printf("LoadOrStore result: %v, was loaded: %t\n", actual, loaded)
}

func benchmarkConcurrentMaps() {
    const numGoroutines = 100
    const operationsPerGoroutine = 1000
    
    // Benchmark SafeMap
    start := time.Now()
    safeMap := NewSafeMap[int, int]()
    var wg sync.WaitGroup
    
    for i := 0; i < numGoroutines; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            for j := 0; j < operationsPerGoroutine; j++ {
                key := id*operationsPerGoroutine + j
                safeMap.Set(key, key*2)
                safeMap.Get(key)
            }
        }(i)
    }
    wg.Wait()
    safeMapTime := time.Since(start)
    
    // Benchmark sync.Map
    start = time.Now()
    var syncMap sync.Map
    
    for i := 0; i < numGoroutines; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            for j := 0; j < operationsPerGoroutine; j++ {
                key := id*operationsPerGoroutine + j
                syncMap.Store(key, key*2)
                syncMap.Load(key)
            }
        }(i)
    }
    wg.Wait()
    syncMapTime := time.Since(start)
    
    fmt.Printf("SafeMap time: %v\n", safeMapTime)
    fmt.Printf("sync.Map time: %v\n", syncMapTime)
    fmt.Printf("Performance ratio: %.2fx\n", float64(safeMapTime)/float64(syncMapTime))
}

func main() {
    fmt.Println("sync.Map demonstration:")
    demonstrateSyncMap()
    
    fmt.Println("\nConcurrent map performance comparison:")
    benchmarkConcurrentMaps()
}

Real-World Applications

Caching Implementation

Building an efficient cache using maps:

package main

import (
    "fmt"
    "sync"
    "time"
)

// Cache entry with TTL
type CacheEntry[T any] struct {
    Value     T
    ExpiresAt time.Time
}

func (e *CacheEntry[T]) IsExpired() bool {
    return time.Now().After(e.ExpiresAt)
}

// TTL Cache implementation
type TTLCache[K comparable, V any] struct {
    mu      sync.RWMutex
    data    map[K]*CacheEntry[V]
    defaultTTL time.Duration
    cleanupInterval time.Duration
    stopCleanup chan struct{}
}

func NewTTLCache[K comparable, V any](defaultTTL, cleanupInterval time.Duration) *TTLCache[K, V] {
    cache := &TTLCache[K, V]{
        data:           make(map[K]*CacheEntry[V]),
        defaultTTL:     defaultTTL,
        cleanupInterval: cleanupInterval,
        stopCleanup:    make(chan struct{}),
    }
    
    // Start cleanup goroutine
    go cache.cleanup()
    
    return cache
}

func (c *TTLCache[K, V]) Set(key K, value V) {
    c.SetWithTTL(key, value, c.defaultTTL)
}

func (c *TTLCache[K, V]) SetWithTTL(key K, value V, ttl time.Duration) {
    c.mu.Lock()
    defer c.mu.Unlock()
    
    c.data[key] = &CacheEntry[V]{
        Value:     value,
        ExpiresAt: time.Now().Add(ttl),
    }
}

func (c *TTLCache[K, V]) Get(key K) (V, bool) {
    c.mu.RLock()
    defer c.mu.RUnlock()
    
    entry, exists := c.data[key]
    if !exists {
        var zero V
        return zero, false
    }
    
    if entry.IsExpired() {
        var zero V
        return zero, false
    }
    
    return entry.Value, true
}

func (c *TTLCache[K, V]) Delete(key K) {
    c.mu.Lock()
    defer c.mu.Unlock()
    delete(c.data, key)
}

func (c *TTLCache[K, V]) Size() int {
    c.mu.RLock()
    defer c.mu.RUnlock()
    return len(c.data)
}

func (c *TTLCache[K, V]) Clear() {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.data = make(map[K]*CacheEntry[V])
}

func (c *TTLCache[K, V]) cleanup() {
    ticker := time.NewTicker(c.cleanupInterval)
    defer ticker.Stop()
    
    for {
        select {
        case <-ticker.C:
            c.removeExpired()
        case <-c.stopCleanup:
            return
        }
    }
}

func (c *TTLCache[K, V]) removeExpired() {
    c.mu.Lock()
    defer c.mu.Unlock()
    
    now := time.Now()
    for key, entry := range c.data {
        if now.After(entry.ExpiresAt) {
            delete(c.data, key)
        }
    }
}

func (c *TTLCache[K, V]) Stop() {
    close(c.stopCleanup)
}

func demonstrateCache() {
    // Create cache with 5-second TTL and 1-second cleanup interval
    cache := NewTTLCache[string, string](5*time.Second, 1*time.Second)
    defer cache.Stop()
    
    // Add some entries
    cache.Set("user:1", "Alice")
    cache.Set("user:2", "Bob")
    cache.SetWithTTL("temp:data", "temporary", 2*time.Second)
    
    fmt.Printf("Initial cache size: %d\n", cache.Size())
    
    // Read entries
    if value, exists := cache.Get("user:1"); exists {
        fmt.Printf("Found user:1 = %s\n", value)
    }
    
    // Wait for temporary data to expire
    fmt.Println("Waiting 3 seconds for temp:data to expire...")
    time.Sleep(3 * time.Second)
    
    if _, exists := cache.Get("temp:data"); exists {
        fmt.Println("temp:data still exists")
    } else {
        fmt.Println("temp:data expired")
    }
    
    fmt.Printf("Final cache size: %d\n", cache.Size())
}

func main() {
    demonstrateCache()
}

Frequency Counter and Analytics

Using maps for data analysis:

package main

import (
    "fmt"
    "sort"
    "strings"
)

// Frequency counter for analytics
type FrequencyCounter[T comparable] struct {
    counts map[T]int
    total  int
}

func NewFrequencyCounter[T comparable]() *FrequencyCounter[T] {
    return &FrequencyCounter[T]{
        counts: make(map[T]int),
    }
}

func (fc *FrequencyCounter[T]) Add(item T) {
    fc.counts[item]++
    fc.total++
}

func (fc *FrequencyCounter[T]) AddN(item T, count int) {
    fc.counts[item] += count
    fc.total += count
}

func (fc *FrequencyCounter[T]) Count(item T) int {
    return fc.counts[item]
}

func (fc *FrequencyCounter[T]) Frequency(item T) float64 {
    if fc.total == 0 {
        return 0
    }
    return float64(fc.counts[item]) / float64(fc.total)
}

func (fc *FrequencyCounter[T]) Total() int {
    return fc.total
}

func (fc *FrequencyCounter[T]) UniqueItems() int {
    return len(fc.counts)
}

// Get top N most frequent items
func (fc *FrequencyCounter[T]) TopN(n int) []struct {
    Item  T
    Count int
} {
    type ItemCount struct {
        Item  T
        Count int
    }
    
    items := make([]ItemCount, 0, len(fc.counts))
    for item, count := range fc.counts {
        items = append(items, ItemCount{Item: item, Count: count})
    }
    
    sort.Slice(items, func(i, j int) bool {
        return items[i].Count > items[j].Count
    })
    
    if n > len(items) {
        n = len(items)
    }
    
    result := make([]struct {
        Item  T
        Count int
    }, n)
    
    for i := 0; i < n; i++ {
        result[i] = struct {
            Item  T
            Count int
        }{items[i].Item, items[i].Count}
    }
    
    return result
}

// Word frequency analysis
func analyzeText(text string) {
    // Prepare text
    text = strings.ToLower(text)
    words := strings.FieldsFunc(text, func(c rune) bool {
        return !((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9'))
    })
    
    // Count word frequencies
    wordCounter := NewFrequencyCounter[string]()
    for _, word := range words {
        if len(word) > 2 { // Skip very short words
            wordCounter.Add(word)
        }
    }
    
    fmt.Printf("Text analysis:\n")
    fmt.Printf("Total words: %d\n", wordCounter.Total())
    fmt.Printf("Unique words: %d\n", wordCounter.UniqueItems())
    
    // Top 10 most frequent words
    top10 := wordCounter.TopN(10)
    fmt.Println("\nTop 10 most frequent words:")
    for i, item := range top10 {
        frequency := wordCounter.Frequency(item.Item)
        fmt.Printf("%d. %s: %d (%.2f%%)\n", i+1, item.Item, item.Count, frequency*100)
    }
}

// Character frequency analysis
func analyzeCharacters(text string) {
    charCounter := NewFrequencyCounter[rune]()
    
    for _, char := range text {
        if char != ' ' && char != '\n' && char != '\t' {
            charCounter.Add(char)
        }
    }
    
    fmt.Printf("\nCharacter analysis:\n")
    fmt.Printf("Total characters: %d\n", charCounter.Total())
    fmt.Printf("Unique characters: %d\n", charCounter.UniqueItems())
    
    // Top 10 most frequent characters
    top10 := charCounter.TopN(10)
    fmt.Println("\nTop 10 most frequent characters:")
    for i, item := range top10 {
        frequency := charCounter.Frequency(item.Item)
        fmt.Printf("%d. '%c': %d (%.2f%%)\n", i+1, item.Item, item.Count, frequency*100)
    }
}

func main() {
    sampleText := `
    Go is an open source programming language that makes it easy to build simple,
    reliable, and efficient software. Go was designed at Google in 2007 to improve
    programming productivity in an era of multicore, networked machines and large codebases.
    The designers wanted to address criticism of other languages in use at Google, but
    keep their useful characteristics. Go is syntactically similar to C, but with memory
    safety, garbage collection, structural typing, and CSP-style concurrency.
    `
    
    analyzeText(sampleText)
    analyzeCharacters(sampleText)
}

FAQ

Q: When should I use sync.Map instead of a regular map with mutex? A: Use sync.Map when you have a read-heavy workload with infrequent writes, or when keys are stable (written once and read many times). For write-heavy or frequently changing data, a regular map with RWMutex often performs better.

Q: How do I efficiently check if a map contains any of multiple keys? A: Iterate through your keys and check each one. For better performance with large key sets, consider using a separate map to track which keys you're looking for, or use a more sophisticated data structure like a bloom filter for approximate membership testing.

Q: Can I use a slice as a map key in Go? A: No, slices are not comparable in Go and cannot be used as map keys. Use arrays (which are comparable) or convert slices to strings if you need slice-like keys.

Q: How do I sort a map by its values? A: Maps themselves cannot be sorted since they're unordered. Extract the key-value pairs into a slice of structs and sort the slice using the sort package.

Q: What's the performance difference between map[string]int and map[int]string? A: String keys require more computation for hashing and comparison than integer keys, making integer keys generally faster. However, the difference is usually negligible unless you're doing millions of operations.

Q: How do I implement a LRU cache using Go maps? A: Combine a map for O(1) lookup with a doubly-linked list to track access order. The map stores keys to list nodes, and the list maintains the LRU order for efficient eviction.

Conclusion

Go maps are powerful and versatile data structures that, when used correctly, can significantly improve your application's performance and code clarity. Key takeaways from this guide:

  • Understand map fundamentals: reference semantics, key constraints, and basic operations
  • Optimize performance through proper initialization, efficient key types, and memory-conscious value storage
  • Handle concurrency safely using mutex-protected maps or sync.Map for appropriate use cases
  • Implement advanced patterns like caching, frequency analysis, and set operations using maps as building blocks
  • Be aware of common pitfalls like concurrent access, memory leaks with large maps, and proper key existence checking

Maps are particularly well-suited for caching, counting, grouping, and lookup operations. By following the patterns and best practices outlined in this article, you'll be able to leverage Go maps effectively in your applications.

Ready to optimize your map usage? Start by reviewing your current code for opportunities to improve map performance and safety. Consider implementing the concurrent patterns if you're dealing with multi-threaded applications. Share your experiences and questions in the comments below!

Share this article

Add Comment

No comments yet. Be the first to comment!

More from Go