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.Protect map access with sync.Mutex: Use a mutex to serialize map operations.
- 2.```go
- 3.type Cache struct {
- 4.mu sync.RWMutex
- 5.data map[string]string
- 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.Use sync.Map for specific use cases: sync.Map is optimized for read-heavy workloads with stable key sets.
- 2.```go
- 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.Run the race detector in tests: Enable -race flag to catch data races during testing.
- 2.```bash
- 3.# Run tests with race detection:
- 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.Use channel-based communication instead of shared state: Prefer channels over shared maps.
- 2.```go
- 3.type cacheRequest struct {
- 4.key string
- 5.response chan string
- 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