Table Of Contents
- Introduction
- Map Fundamentals
- Advanced Map Operations
- Performance Optimization
- Concurrent Map Access
- Real-World Applications
- FAQ
- Conclusion
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!
Add Comment
No comments yet. Be the first to comment!