Prevent Data Races on Shared State in Go
Catches maps, counters, and slices shared across goroutines without a mutex or atomic, plus mutex-guarded state that escapes its lock.
What it flags
type RouteStats struct {
mu sync.Mutex
counts map[string]int
total int
}
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
h.stats.Record(h.route)
h.next.ServeHTTP(w, r)
}
func (s *RouteStats) Record(route string) {
s.mu.Lock()
s.counts[route]++
s.mu.Unlock()
s.total++
}
func (s *RouteStats) Snapshot() (map[string]int, int) {
s.mu.Lock()
defer s.mu.Unlock()
return s.counts, s.total // callers range over the live map while Record writes to it
}type RouteStats struct {
mu sync.Mutex
counts map[string]int
total int
}
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
h.stats.Record(h.route)
h.next.ServeHTTP(w, r)
}
func (s *RouteStats) Record(route string) {
s.mu.Lock()
defer s.mu.Unlock()
s.counts[route]++
s.total++
}
func (s *RouteStats) Snapshot() (map[string]int, int) {
s.mu.Lock()
defer s.mu.Unlock()
return maps.Clone(s.counts), s.total
}Why it matters
Racing reads can observe stale or torn values, and concurrent map access can crash the whole process with a fatal error that recover cannot catch. The race detector only reports races on code paths that run concurrently during the test, so a race behind a rarely hit handler or a background refresh ships and fails under production load. Review is often the only point where someone asks which goroutines can reach a value.
Agent instructions
cubic checks every pull request against these instructions. Paste them into AGENTS.md or Cursor rules and your AI coding tool follows them while it writes code.
AGENTS.md
Add this section to AGENTS.md at the repository root. Codex, Cursor, and other AI coding tools read it.
## Prevent Data Races on Shared State in Go Applies to `**/*.go`. Review Go code that shares state across goroutines for reads and writes without synchronization. Flag changes that: - Read or write a map from more than one goroutine (HTTP handlers, `go` statements, callbacks, package-level caches) without a `sync.Mutex`, `sync.RWMutex`, or `sync.Map`. - Update counters, flags, or package-level variables shared across goroutines with plain `++`, `+=`, or assignment instead of a mutex or a `sync/atomic` type such as `atomic.Int64`. - `append` to a shared slice from multiple goroutines, such as `results = append(results, r)` inside `go func()`. - Guard a field with a mutex in some methods but read or write it without the lock in others, including getters and `len` calls. - Return or keep a reference to a mutex-guarded map, slice, or pointer after unlocking, so callers read it while another goroutine writes it. - Check and act in separate critical sections, such as reading under `RLock`, unlocking, and then writing based on the stale read. Do not flag: - Closures that capture loop variables in modules whose `go.mod` declares `go 1.22` or later, where each iteration has its own variable. - Goroutines writing distinct indexes of a pre-sized slice (`results[i] = r`) when the slice is read only after `Wait`. - State written before the goroutines start and only read afterward.
Checks files matching **/*.go.
More Go agents
See all Go code review standards
Run this agent on every pull request
cubic reviews every pull request against the agents you turn on and comments on the lines that break them.