Frontend Labs
⋯

17. The sync package: mutexes, WaitGroup, Once, atomics

17.1 sync.Mutex and sync.RWMutex

type SafeCounter struct {
	mu sync.Mutex
	n  map[string]int
}

func (c *SafeCounter) Inc(key string) {
	c.mu.Lock()
	defer c.mu.Unlock()
	c.n[key]++
}

sync.RWMutex allows any number of concurrent readers OR one writer (never both) — use it when reads vastly outnumber writes, exactly as Day 14's per-endpoint request counter does.

17.2 sync.WaitGroup

var wg sync.WaitGroup
for _, url := range urls {
	wg.Add(1)
	go func(u string) {
		defer wg.Done()
		check(u)
	}(url)
}
wg.Wait() // blocks until all Done() calls have happened

Common bug: calling wg.Add(1) inside the goroutine instead of before go starts it — this creates a race where Wait() might return before all goroutines have even registered themselves.

17.3 sync.Once

var once sync.Once
var instance *Config

func GetConfig() *Config {
	once.Do(func() {
		instance = loadConfig() // guaranteed to run exactly once, ever
	})
	return instance
}

17.4 The sync/atomic package

For simple counters, atomics avoid mutex overhead entirely:

var requests atomic.Int64
requests.Add(1)
current := requests.Load()

17.5 Run with -race regularly

go test -race ./...
go run -race main.go

The race detector instruments memory accesses and catches races that actually execute during that run. It cannot prove a program is race-free, but running it in CI and on representative workloads finds many bugs that otherwise show up only under load.

17.6 Real-world example

Day 14's concurrent request counter, hammered by 100 goroutines and run under -race, is a tiny reproduction of the bug class behind "works on my machine, corrupts data under load in prod" — and it's why many Go teams run at least part of their test suite with -race before merging.

Applied in the series: the TCP/IP guide's lbdrain and the HTTPS guide's resilience labs use atomic.Bool, atomic.Uint64, and mutex-guarded maps in real servers.


Generated from real-life-golang-guide.md · 8 Oct 2026 16:36