Working with Maps in Go
Maps are Go's built-in associative data type (also known as hashes or dictionaries in other languages). This guide covers everything you need to know about working with maps effectively.
Map Basics
Map Declaration
// Basic map declaration
var scores map[string]int
// Create map with make
scores = make(map[string]int)
// Map literal declaration
colors := map[string]string{
"red": "#ff0000",
"green": "#00ff00",
"blue": "#0000ff",
}
// Empty map literal
empty := map[string]int{}
Map Operations
// Adding or updating entries
scores["alice"] = 100
scores["bob"] = 85
// Retrieving values
aliceScore := scores["alice"]
// Checking existence
score, exists := scores["charlie"]
if exists {
fmt.Printf("Score: %d\n", score)
} else {
fmt.Println("No score found")
}
// Deleting entries
delete(scores, "bob")
// Map length
count := len(scores)
Map Iteration
// Range over map
for key, value := range scores {
fmt.Printf("%s: %d\n", key, value)
}
// Iterate over keys only
for key := range scores {
fmt.Printf("Key: %s\n", key)
}
// Iterate over values only
for _, value := range scores {
fmt.Printf("Value: %d\n", value)
}
Map Types
Basic Map Types
// String keys (most common)
users := map[string]User{}
// Integer keys
matrix := map[int][]float64{}
// Boolean keys
flags := map[bool]string{
true: "yes",
false: "no",
}
// Struct keys
type Point struct {
X, Y int
}
locations := map[Point]string{}
Complex Map Types
// Maps of maps
graph := map[string]map[string]int{
"A": {"B": 1, "C": 2},
"B": {"A": 1, "D": 3},
}
// Maps of slices
userRoles := map[string][]string{
"alice": {"admin", "user"},
"bob": {"user"},
}
// Maps of interfaces
data := map[string]interface{}{
"name": "John",
"age": 30,
"scores": []int{85, 90, 95},
"address": map[string]string{"city": "New York"},
}
Map Patterns
1. Set Implementation
// Set using map with empty struct
type Set map[string]struct{}
func (s Set) Add(item string) {
s[item] = struct{}{}
}
func (s Set) Remove(item string) {
delete(s, item)
}
func (s Set) Contains(item string) bool {
_, exists := s[item]
return exists
}
// Usage
uniqueWords := make(Set)
uniqueWords.Add("apple")
uniqueWords.Add("banana")
fmt.Println(uniqueWords.Contains("apple")) // true
2. Counter Implementation
type Counter map[string]int
func (c Counter) Increment(key string) {
c[key]++
}
func (c Counter) GetCount(key string) int {
return c[key]
}
// Usage
wordCount := make(Counter)
words := []string{"apple", "banana", "apple", "cherry"}
for _, word := range words {
wordCount.Increment(word)
}
3. Cache Implementation
type Cache struct {
data map[string]interface{}
mu sync.RWMutex
}
func NewCache() *Cache {
return &Cache{
data: make(map[string]interface{}),
}
}
func (c *Cache) Set(key string, value interface{}) {
c.mu.Lock()
defer c.mu.Unlock()
c.data[key] = value
}
func (c *Cache) Get(key string) (interface{}, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
value, exists := c.data[key]
return value, exists
}
Best Practices
1. Initialization
// Good: Initialize with expected size
users := make(map[string]User, 100)
// Avoid: Growing map without size hint
users := make(map[string]User) // Default size
// Good: Clear map while preserving capacity
for k := range users {
delete(users, k)
}
// Avoid: Reallocating map
users = make(map[string]User) // Loses allocated space
2. Concurrent Access
// Good: Use sync.Map for concurrent access
var cache sync.Map
cache.Store("key", "value")
value, ok := cache.Load("key")
// Alternative: Use mutex
type SafeMap struct {
sync.RWMutex
data map[string]interface{}
}
func (m *SafeMap) Get(key string) interface{} {
m.RLock()
defer m.RUnlock()
return m.data[key]
}
3. Memory Management
// Good: Delete unused entries
for key := range largeMap {
if isExpired(key) {
delete(largeMap, key)
}
}
// Good: Use pointers for large values
map[string]*LargeStruct{} // Instead of map[string]LargeStruct
// Avoid memory leaks
// Clear references in values
for k := range objMap {
objMap[k].Clear() // Clear internal references
delete(objMap, k)
}
Common Operations
1. Map Merging
// Merge two maps
func merge(m1, m2 map[string]int) map[string]int {
result := make(map[string]int, len(m1)+len(m2))
for k, v := range m1 {
result[k] = v
}
for k, v := range m2 {
result[k] = v // m2 values override m1
}
return result
}
2. Map Filtering
// Filter map entries
func filter(m map[string]int, pred func(string, int) bool) map[string]int {
result := make(map[string]int)
for k, v := range m {
if pred(k, v) {
result[k] = v
}
}
return result
}
// Usage
positives := filter(numbers, func(k string, v int) bool {
return v > 0
})
3. Map Transformation
// Transform map values
func transform(m map[string]int, fn func(int) int) map[string]int {
result := make(map[string]int, len(m))
for k, v := range m {
result[k] = fn(v)
}
return result
}
// Usage
doubled := transform(numbers, func(v int) int {
return v * 2
})
Performance Considerations
1. Map Size
// Pre-allocate for better performance
users := make(map[string]User, expectedSize)
// Benchmark different sizes
func BenchmarkMapOperations(b *testing.B) {
for size := 100; size <= 10000; size *= 10 {
b.Run(fmt.Sprintf("size-%d", size), func(b *testing.B) {
m := make(map[string]int, size)
// Benchmark operations...
})
}
}
2. Key Types
// Good: Simple comparable types
map[string]int{}
map[int]string{}
// Avoid: Complex key types
map[*MyStruct]string{} // Pointer comparison
map[[]int]string{} // Invalid: slices aren't comparable
3. Value Types
// Good: Use pointers for large values
type LargeStruct struct {
Data [1024]byte
}
cache := map[string]*LargeStruct{}
// Avoid copying large values
cache := map[string]LargeStruct{} // Copies on assignment
Next Steps
- Learn about structs
- Explore interfaces
- Study concurrency
- Practice with algorithms