Introduction

Go maps are not safe for concurrent use. When one goroutine writes to a map while another reads or writes, the runtime may detect it with the race detector (-race flag) and in Go 1.6+ may even crash with fatal error: concurrent map writes. This is one of the most common concurrency bugs in Go applications.

Symptoms

  • WARNING: DATA RACE from go test -race
  • fatal error: concurrent map writes (crash)
  • fatal error: concurrent map iteration and map write
  • Non-deterministic behavior - sometimes works, sometimes crashes
  • Race condition report showing goroutine IDs and stack traces

``` WARNING: DATA RACE Write at 0x00c0000a6300 by goroutine 15: runtime.mapassign_faststr() /usr/local/go/src/runtime/map_faststr.go:203 +0x0 main.(*Cache).Set() /app/cache.go:15 +0x89

Previous read at 0x00c0000a6300 by goroutine 12: runtime.mapaccess1_faststr() /usr/local/go/src/runtime/map_faststr.go:13 +0x0 main.(*Cache).Get() /app/cache.go:22 +0x73 ```

Common Causes

  • Shared map accessed by multiple HTTP handler goroutines
  • Global configuration map updated during runtime while being read
  • Cache implementations without mutex protection
  • Map used as shared state between producer and consumer goroutines
  • Test code sharing maps between parallel test functions

Step-by-Step Fix

  1. 1.Protect map with sync.RWMutex:
  2. 2.```go
  3. 3.type Cache struct {
  4. 4.mu sync.RWMutex
  5. 5.items map[string]string
  6. 6.}

func NewCache() *Cache { return &Cache{items: make(map[string]string)} }

func (c *Cache) Set(key, value string) { c.mu.Lock() defer c.mu.Unlock() c.items[key] = value }

func (c *Cache) Get(key string) (string, bool) { c.mu.RLock() defer c.mu.RUnlock() val, ok := c.items[key] return val, ok }

func (c *Cache) Delete(key string) { c.mu.Lock() defer c.mu.Unlock() delete(c.items, key) } ```

  1. 1.Use sync.Map for specific use cases:
  2. 2.```go
  3. 3.// sync.Map is optimized for:
  4. 4.// 1. Entries written once but read many times
  5. 5.// 2. Multiple goroutines reading/writing/disjoint sets of keys

var config sync.Map

func SetConfig(key string, value interface{}) { config.Store(key, value) }

func GetConfig(key string) interface{} { val, _ := config.Load(key) return val }

// Iterate safely config.Range(func(key, value interface{}) bool { fmt.Printf("%s: %v\n", key, value) return true }) ```

  1. 1.Use channels instead of shared maps:
  2. 2.```go
  3. 3.type CacheServer struct {
  4. 4.items map[string]string
  5. 5.sets chan setReq
  6. 6.gets chan getReq
  7. 7.}

type setReq struct { key, value string done chan struct{} }

type getReq struct { key string result chan string }

func (cs *CacheServer) Run() { for { select { case req := <-cs.sets: cs.items[req.key] = req.value close(req.done) case req := <-cs.gets: req.result <- cs.items[req.key] } } } ```

  1. 1.Run with race detector in CI:
  2. 2.```bash
  3. 3.# Test with race detection
  4. 4.go test -race ./...

# Build with race detection for integration tests go build -race -o app-race ./cmd/server

# Run integration test ./app-race & APP_PID=$! run_tests kill $APP_PID ```

Prevention

  • Always run go test -race before merging
  • Use sync.RWMutex for read-heavy maps, sync.Mutex for write-heavy
  • Prefer sync.Map only for append-mostly, read-many patterns
  • Consider github.com/orcaman/concurrent-map for advanced concurrent map needs
  • Use channels for ownership transfer rather than shared mutable state
  • Document which structs are concurrency-safe and which are not