Iterating Maps — Find the Bug
Bug 1 🟢 — Assuming Map Order
package main
import "fmt"
func main() {
days := map[int]string{
1: "Monday", 2: "Tuesday", 3: "Wednesday",
4: "Thursday", 5: "Friday",
}
fmt.Println("Week schedule:")
for day, name := range days {
fmt.Printf("Day %d: %s\n", day, name)
}
// Expected: Day 1 Mon, Day 2 Tue, etc. in order
}
Solution
Map iteration is random. Days will print in unpredictable order. **Fix:** import "sort"
keys := make([]int, 0, len(days))
for k := range days { keys = append(keys, k) }
sort.Ints(keys)
for _, day := range keys {
fmt.Printf("Day %d: %s\n", day, days[day])
}
Bug 2 🟢 — Modifying Struct Value via Range Variable
package main
import "fmt"
type Account struct {
Balance float64
}
func applyInterest(accounts map[string]Account, rate float64) {
for name, acc := range accounts {
acc.Balance *= (1 + rate)
_ = name // "applied" but not saved!
}
}
func main() {
accounts := map[string]Account{
"Alice": {1000.0},
"Bob": {2000.0},
}
applyInterest(accounts, 0.05)
fmt.Println(accounts["Alice"].Balance) // expected 1050, gets 1000
}
Solution
`acc` is a copy. Modifying it does not affect the map. **Fix:** func applyInterest(accounts map[string]Account, rate float64) {
for name := range accounts {
acc := accounts[name]
acc.Balance *= (1 + rate)
accounts[name] = acc
}
}
// Or use map[string]*Account to avoid this issue entirely
Bug 3 🟢 — nil Map Write Panic
package main
import "fmt"
func countWords(text string, freq map[string]int) {
words := splitWords(text)
for _, w := range words {
freq[w]++ // PANIC if freq is nil!
}
}
func splitWords(s string) []string {
return []string{"hello", "world"}
}
func main() {
var freq map[string]int // nil map
countWords("hello world", freq)
fmt.Println(freq)
}
Solution
Writing to a nil map causes a panic: `assignment to entry in nil map`. Reading from nil map is fine (returns zero value), but writing panics. **Fix:** freq := make(map[string]int) // initialize first
countWords("hello world", freq)
// Or inside countWords:
// if freq == nil { freq = make(map[string]int) } // but this won't affect caller
Bug 4 🟡 — Deleting Keys Not Found in Range Variable
package main
import "fmt"
func removeExpired(cache map[string]int, expired []string) {
for key := range cache {
for _, exp := range expired {
if key == exp {
delete(cache, exp) // safe, but...
}
}
}
// Alternative attempt that's wrong:
for _, exp := range expired {
for k := range cache {
if k == exp {
// Works but O(n*m) — quadratic!
delete(cache, k)
}
}
}
}
func main() {
cache := map[string]int{"a": 1, "b": 2, "c": 3, "d": 4}
removeExpired(cache, []string{"b", "d"})
fmt.Println(cache) // expected: map[a:1 c:3]
}
Solution
The implementation is O(n×m) — for each cache key, it scans all expired keys. This is quadratic. **Fix:** Build a set of expired keys first: func removeExpired(cache map[string]int, expired []string) {
expSet := make(map[string]struct{}, len(expired))
for _, k := range expired {
expSet[k] = struct{}{}
}
for k := range cache {
if _, ok := expSet[k]; ok {
delete(cache, k) // O(1) lookup — total O(n+m)
}
}
}
Bug 5 🟡 — Range Variable Capture in Closures
package main
import "fmt"
func main() {
actions := map[string]func(){
"a": nil,
"b": nil,
"c": nil,
}
for key := range actions {
key := key // OOPS: re-declared here but then ignored below
actions[key] = func() {
fmt.Println(key) // actually uses OUTER key (pre-1.22)
}
}
// ...wait, key := key does shadow. But the issue is:
// All closures were originally created without the shadow in a common mistake:
handlers := map[string]func(){}
for k := range actions {
handlers[k] = func() {
fmt.Println(k) // captures k by reference — prints last value
}
}
for _, h := range handlers {
h() // pre-1.22: all print the same (last) k
}
}
Solution
In pre-Go 1.22, `k` is shared across all closure captures. All closures print the same value (whichever bucket was last). **Fix:** for k := range actions {
k := k // per-iteration copy
handlers[k] = func() {
fmt.Println(k) // captures its own k
}
}
// In Go 1.22+: per-iteration semantics fix this automatically
Bug 6 🟡 — Concurrent Map Write During Range
package main
import (
"fmt"
"sync"
)
func updateAll(m map[string]int) {
var wg sync.WaitGroup
for k := range m {
wg.Add(1)
go func(key string) {
defer wg.Done()
m[key]++ // concurrent map write while main goroutine may still be ranging!
}(k)
}
wg.Wait()
fmt.Println(m)
}
func main() {
m := map[string]int{"a": 1, "b": 2, "c": 3}
updateAll(m)
}
Solution
The goroutines write to `m` concurrently while the main goroutine might still be ranging over it (or while other goroutines are also writing). This is a data race — fatal `concurrent map read and map write` panic. **Fix:** func updateAll(m map[string]int) {
var mu sync.Mutex
var wg sync.WaitGroup
for k := range m {
k := k
wg.Add(1)
go func() {
defer wg.Done()
mu.Lock()
m[k]++
mu.Unlock()
}()
}
wg.Wait()
fmt.Println(m)
}
Bug 7 🟡 — Adding Keys During Range (Infinite-Like Loop)
package main
import "fmt"
func expandMap(m map[string]int, depth int) {
for k, v := range m {
if depth > 0 {
m[k+"_child"] = v + 1 // adds keys — may iterate them!
}
}
}
func main() {
m := map[string]int{"root": 0}
expandMap(m, 1)
fmt.Println(len(m)) // expected 2, may get 2, 3, or more!
}
Solution
Newly added keys may or may not be visited during the current range. The function may visit `"root_child"` and add `"root_child_child"`, etc. The behavior is non-deterministic. **Fix:** func expandMap(m map[string]int, depth int) {
toAdd := make(map[string]int)
for k, v := range m {
if depth > 0 {
toAdd[k+"_child"] = v + 1
}
}
for k, v := range toAdd {
m[k] = v
}
}
Bug 8 🔴 — Map Used as Cache Key (Non-Deterministic)
package main
import (
"fmt"
"strings"
)
var cache = map[string]string{}
func getResult(params map[string]string) string {
// Build cache key from params
var sb strings.Builder
for k, v := range params { // RANDOM ORDER
sb.WriteString(k + "=" + v + "&")
}
key := sb.String()
if cached, ok := cache[key]; ok {
return cached
}
result := expensiveCompute(params)
cache[key] = result
return result
}
func expensiveCompute(p map[string]string) string { return "result" }
func main() {
params := map[string]string{"a": "1", "b": "2", "c": "3"}
r1 := getResult(params)
r2 := getResult(params) // may generate DIFFERENT key!
fmt.Println(r1 == r2) // may be false — cache miss!
}
Solution
The cache key built from map iteration is non-deterministic. The same params map produces different keys on different calls, making the cache useless. **Fix:** import "sort"
func buildKey(params map[string]string) string {
keys := make([]string, 0, len(params))
for k := range params { keys = append(keys, k) }
sort.Strings(keys)
var sb strings.Builder
for _, k := range keys {
sb.WriteString(k + "=" + params[k] + "&")
}
return sb.String()
}
Bug 9 🔴 — Snapshot Not Taken, Stale Data
package main
import (
"fmt"
"sync"
"time"
)
type Store struct {
mu sync.RWMutex
data map[string]int
}
func (s *Store) ProcessAll() {
s.mu.RLock()
keys := make([]string, 0, len(s.data))
for k := range s.data {
keys = append(keys, k)
}
s.mu.RUnlock()
// Lock released! Data may change between here and processing
for _, k := range keys {
s.mu.RLock()
v := s.data[k] // k might have been deleted!
s.mu.RUnlock()
time.Sleep(1 * time.Millisecond) // simulate processing
fmt.Println(k, v)
}
}
Solution
Between collecting keys and looking up values, the map can be modified. Keys collected may no longer exist, or values may have changed. **Fix:** Take a full snapshot under the lock: func (s *Store) ProcessAll() {
s.mu.RLock()
snap := make(map[string]int, len(s.data))
for k, v := range s.data { // snapshot entire map
snap[k] = v
}
s.mu.RUnlock()
// Process snapshot — consistent view, no lock needed
for k, v := range snap {
fmt.Println(k, v)
}
}
Bug 10 🔴 — Map Value Pointer Aliasing
package main
import "fmt"
func buildPtrMap(keys []string) map[string]*int {
m := map[string]*int{}
shared := 0
for _, k := range keys {
m[k] = &shared // ALL entries point to the SAME variable!
shared++
}
return m
}
func main() {
m := buildPtrMap([]string{"a", "b", "c"})
for k, v := range m {
fmt.Printf("%s -> %d\n", k, *v) // all print same value!
}
}
Solution
`shared` is a single variable. All map values point to the same memory location (`&shared`). After the loop, `*v` is `3` (final value of shared) for all entries. **Fix:** func buildPtrMap(keys []string) map[string]*int {
m := map[string]*int{}
for i, k := range keys {
val := i // new variable per iteration
m[k] = &val
}
return m
}
// Or:
func buildPtrMap(keys []string) map[string]*int {
m := map[string]*int{}
for i, k := range keys {
i := i // shadow
m[k] = &i
}
return m
}
Bug 11 🔴 — Race in Map Inside Struct Without Embedded Mutex
package main
import (
"fmt"
"sync"
)
type EventLog struct {
events map[string]int
}
var log = &EventLog{events: map[string]int{}}
var wg sync.WaitGroup
func logEvent(name string) {
wg.Add(1)
go func() {
defer wg.Done()
log.events[name]++ // no protection!
}()
}
func main() {
for _, event := range []string{"login", "logout", "login", "purchase"} {
logEvent(event)
}
wg.Wait()
for event, count := range log.events {
fmt.Printf("%s: %d\n", event, count)
}
}
Solution
Multiple goroutines write to `log.events` concurrently without any synchronization. This causes a data race and potential fatal panic. **Fix:** type EventLog struct {
mu sync.Mutex
events map[string]int
}
func (l *EventLog) Log(name string) {
l.mu.Lock()
l.events[name]++
l.mu.Unlock()
}
// Or use sync.Map:
type EventLog struct {
events sync.Map
}
func (l *EventLog) Log(name string) {
actual, _ := l.events.LoadOrStore(name, new(int))
// ... use atomic for counting
}
Bug 12 🔴 — Incorrect Nil Check for Map Value
package main
import "fmt"
func findUser(db map[string]*User, name string) *User {
user, _ := db[name]
if user == nil {
return nil
}
return user
}
type User struct{ Name string }
func main() {
db := map[string]*User{
"alice": {Name: "Alice"},
"bob": nil, // explicit nil value
}
u := findUser(db, "charlie") // key doesn't exist
fmt.Println(u == nil) // true — correct
u2 := findUser(db, "bob") // key exists, value is nil
fmt.Println(u2 == nil) // true — but is this correct? Key EXISTS!
}
Solution
The function cannot distinguish between "key not found" and "key found with nil value". Both return `nil`. This can mask bugs where a key was explicitly set to nil. **Fix:** Use the two-value form: func findUser(db map[string]*User, name string) (*User, bool) {
user, ok := db[name]
return user, ok
}
// Now: findUser(db, "charlie") -> (nil, false) "not found"
// findUser(db, "bob") -> (nil, true) "found, value is nil"