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 RACEfromgo test -racefatal 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.Protect map with sync.RWMutex:
- 2.```go
- 3.type Cache struct {
- 4.mu sync.RWMutex
- 5.items map[string]string
- 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.Use sync.Map for specific use cases:
- 2.```go
- 3.// sync.Map is optimized for:
- 4.// 1. Entries written once but read many times
- 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.Use channels instead of shared maps:
- 2.```go
- 3.type CacheServer struct {
- 4.items map[string]string
- 5.sets chan setReq
- 6.gets chan getReq
- 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.Run with race detector in CI:
- 2.```bash
- 3.# Test with race detection
- 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 -racebefore merging - Use
sync.RWMutexfor read-heavy maps,sync.Mutexfor write-heavy - Prefer
sync.Maponly for append-mostly, read-many patterns - Consider
github.com/orcaman/concurrent-mapfor advanced concurrent map needs - Use channels for ownership transfer rather than shared mutable state
- Document which structs are concurrency-safe and which are not