Introduction

Go maps are not safe for concurrent use. When one goroutine writes to a map while another reads or writes, Go's race detector flags it as a data race. In production (without -race), this can cause the dreaded "concurrent map writes" panic that crashes the application.

This is a classic concurrency bug in Go, especially in caching layers, shared configuration stores, and in-memory data structures.

Symptoms

  • go test -race reports "WARNING: DATA RACE" with map read/write at different goroutines
  • Production panic: "fatal error: concurrent map writes" or "concurrent map read and map write"
  • Random panics that are hard to reproduce because they depend on goroutine scheduling

Common Causes

  • Multiple goroutines read and write the same map without synchronization
  • Map is passed by reference to goroutines that modify it independently
  • Global or package-level map accessed by multiple request handlers

Step-by-Step Fix

  1. 1.Protect map access with sync.Mutex: Use a mutex to serialize map operations.
  2. 2.```go
  3. 3.type Cache struct {
  4. 4.mu sync.RWMutex
  5. 5.data map[string]string
  6. 6.}

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

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

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

  1. 1.Use sync.Map for specific use cases: sync.Map is optimized for read-heavy workloads with stable key sets.
  2. 2.```go
  3. 3.var config sync.Map

// Store config.Store("database_url", "postgres://localhost:5432/mydb") config.Store("max_connections", "100")

// Load val, ok := config.Load("database_url") if ok { fmt.Println(val.(string)) }

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

  1. 1.Run the race detector in tests: Enable -race flag to catch data races during testing.
  2. 2.```bash
  3. 3.# Run tests with race detection:
  4. 4.go test -race ./...

# Run a specific test: go test -race -run TestCache

# Build with race detection for manual testing: go build -race -o myapp ./cmd/myapp ```

  1. 1.Use channel-based communication instead of shared state: Prefer channels over shared maps.
  2. 2.```go
  3. 3.type cacheRequest struct {
  4. 4.key string
  5. 5.response chan string
  6. 6.}

func cacheWorker(cmds <-chan cacheRequest) { data := make(map[string]string) for cmd := range cmds { cmd.response <- data[cmd.key] } }

func main() { cmds := make(chan cacheRequest) go cacheWorker(cmds)

resp := make(chan string) cmds <- cacheRequest{key: "user:1", response: resp} val := <-resp fmt.Println(val) } ```

Prevention

  • Run go test -race on all tests as part of CI/CD pipeline
  • Never share maps between goroutines without explicit synchronization
  • Prefer sync.RWMutex for read-heavy maps, sync.Map for write-once-read-many
  • Consider channel-based architecture to avoid shared mutable state