A race condition occurs when goroutines access shared state concurrently and the result depends on timing or ordering. It can cause crashes, data corruption, stale writes, or a send on a channel that has already been closed. Prevent it by making shared access orderly rather than assuming concurrent operations will happen in a safe sequence. section 8.2.4 section 6.9
First identify the shared resource and the operations that touch it. A map used by HTTP handlers is shared because each request runs in its own goroutine. For direct protection of shared state, use a mutex: Lock grants one goroutine exclusive access, and Unlock lets another proceed. Use an RWMutex when reads can proceed together while writes remain exclusive. section 8.2.4 section 8.2
Listing 8.7 RWMutex protection (link/shortener.go)
package link
import (
"sync"
. . .
)
// Shortener shortens and resolves [Link] values from an in-memory storage.
// It's safe for concurrent use from multiple goroutines.
type Shortener struct {
muLinks sync.RWMutex #1
links map[Key]Link
}
func (s *Shortener) Shorten(_ context.Context, lnk Link) (Key, error) {
. . .
s.muLinks.Lock() #2
defer s.muLinks.Unlock() #3
if _, ok := s.links[lnk.Key]; ok {
return "", fmt.Errorf(. . .)
}
if s.links == nil {
s.links = map[Key]Link{}
}
s.links[lnk.Key] = lnk
return lnk.Key, nil
}
func (s *Shortener) Resolve(_ context.Context, key Key) (Link, error) {
. . .
s.muLinks.RLock() #4
defer s.muLinks.RUnlock() #5
lnk, ok := s.links[key]
if !ok {
return Link{}, fmt.Errorf(. . .)
}
return lnk, nil
}
The Shortener keeps an in-memory map safe by locking writes with Lock and Unlock, while Resolve uses RLock and RUnlock for concurrent reads. Deferring unlocks keeps every return path from leaving the resource locked. section 8.2.4
Choose atomic types for simple shared counters. An HTTP test server handles requests in separate goroutines, so incrementing and reading a normal counter would race. atomic.Int64 provides Add for increments and Load for reading the final count without exposing the counter to unsynchronized access. section 7.6.2 section 7.8
Count concurrent HTTP requests safely
Run a client with five goroutines sending 10 requests to a test server. Declare an atomic.Int64 counter, call Add in the server handler, drain the result iterator, then call Load and check that the count is 10. The counter remains safe even though handlers run concurrently. section 7.6.2
Coordinate channel lifetime as carefully as shared memory. In a dispatcher, workers send results while a monitoring goroutine waits for them to finish and only then closes the output channel. Closing early risks a worker sending to a closed channel; closing at completion tells receivers that no more results will arrive. section 6.9
Use these practical checks when concurrent code changes:
- Run tests in parallel when appropriate, but account for state that tests or subtests may share. section 3.4
- Run the race detector to find race-condition issues; test coverage alone does not guarantee correctness. section 3.6
- Prefer mutexes for straightforward shared-state protection rather than adding channels solely to synchronize a map or similar resource. section 8.2.4



















