Lightweight concurrent functions started with the go keyword.
Goroutine Leaks & the Race Detector
Goroutines are cheap, but they're not free. A goroutine that never exits holds its stack, its captured variables, and any channel it's blocked on for the lifetime of the program. Over hours and days, leaks eat your memory.
What a leak looks like
The classic shape — a sender blocks forever because no one is reading:
func find(needle string, haystack []string) string {
out := make(chan string) // unbuffered
for _, s := range haystack {
go func(s string) {
if strings.Contains(s, needle) {
out <- s // blocks if no receiver
}
}(s)
}
return <-out // returns on the first match…
// …but every still-running goroutine is now stuck forever.
}
The first match wins, the function returns, and every other goroutine is parked on the send and never collected.
Three fixes
Fix 1: buffer the channel so sends don't block.
out := make(chan string, len(haystack))
Works for small fixed sizes. Wastes memory on large ones.
Fix 2: select with a done channel so blocked goroutines can be told to bail.
defer cancel() is what releases the still-running searchers — the canonical
Go pattern for fan-out work.
Fix 3: use errgroup + context when this gets gnarly (covered in the
errgroup lesson).
Spotting leaks in tests
Add the go.uber.org/goleak package to your test suite:
func TestNoLeaks(t *testing.T) {
defer goleak.VerifyNone(t)
// run your code
}
It fails the test if any non-stdlib goroutines are still alive at teardown.
The race detector
A data race is two goroutines touching the same memory with at least one write, with no synchronization. Race conditions are bugs waiting to happen — they may pass every test on your laptop and corrupt prod the day it hits 8 cores.
Go has a built-in detector. Just pass -race:
go test -race ./...
go run -race main.go
go build -race -o myapp .
When it spots a race, the output points at the offending file + line for both the read and the write:
WARNING: DATA RACE
Read at 0x00c0000180c8 by goroutine 7:
main.counter()
/tmp/race.go:13 +0x44
Previous write at 0x00c0000180c8 by goroutine 6:
main.counter()
/tmp/race.go:14 +0x5b