Day 58 — Race Conditions in Network Servers
Learning Objectives
- Find races in the Week 5 TCP proxy using
-race - Understand the three most common race patterns in network code
- Fix races without over-locking
Common Race Patterns
Race 1: Shared connection state
Two goroutines (read + write) both check conn.closed
Fix: sync/atomic or sync.Once for close
Race 2: Concurrent map writes
Handler goroutine reads map while health checker writes
Fix: sync.RWMutex around the map
Race 3: Deadline race
Read goroutine sets read deadline
Write goroutine also sets deadline (via SetDeadline)
They race to set the deadline
Fix: separate SetReadDeadline / SetWriteDeadline
Race 4: Goroutine closure variable capture
for i := range items { go func() { fmt.Println(i) }() }
All goroutines see the last value of i
Fix: pass i as argument: go func(id int) { fmt.Println(id) }(i)
Exercise: Race Audit Checklist
# Run with race detector
go test -race -v ./...
go build -race . && ./proxy # run the binary with race detection
# Common findings and fixes:
# DATA RACE: concurrent map read and write
# Fix: add sync.RWMutex
# DATA RACE: conn.closed written from two goroutines
# Fix: use atomic.Bool
# DATA RACE: per-connection stats map
# Fix: use sync.Map or per-connection struct with single owner goroutine
Annotated fixes for the Week 5 proxy:
// Before (race): both goroutines write to same conn field
type connection struct {
conn net.Conn
closed bool // RACE: read and written from two goroutines
bytesIn int64 // RACE: written from read goroutine, read from stats goroutine
bytesOut int64
}
// After (fixed):
type connection struct {
conn net.Conn
closed atomic.Bool // atomic — safe from multiple goroutines
bytesIn atomic.Int64 // atomic counter — no lock needed
bytesOut atomic.Int64
}
Real-World Context
The race detector finds bugs that appear 1 in 10,000 times in production — intermittent corruptions and occasional panics. Running -race in CI is mandatory for any networked service. A data race in a proxy can silently corrupt responses — security-relevant because it could mix up authenticated and unauthenticated responses.