1 · Before you read — commit to an answer
ch := make(chan int)
close(ch)
close(ch)
fmt.Println("done")
panic: close of closed channel
Commit before reading — this is mistake #1 of four.
Closing a Channel Twice
That pretest is the first mistake. Only close once, and only from the sender.
Writing to a Closed Channel
ch := make(chan int)
close(ch)
ch <- 42 // PANIC: send on closed channel
The Loop Variable Trap
2 · Worked example — read every step
// BAD: all goroutines share the same loop variable
for _, url := range urls {
go func() {
fmt.Println(url) // prints the LAST url, multiple times
}()
}
// GOOD: pass as parameter
for _, url := range urls {
go func(u string) {
fmt.Println(u) // correct: each goroutine gets its own copy
}(url)
}
Note: Go 1.22+ changed loop variable semantics, so the bad version now works correctly. But passing as a parameter is still clearer and works in all versions.
Race Conditions
// BAD: concurrent writes to shared variable
counter := 0
for i := 0; i < 1000; i++ {
go func() {
counter++ // DATA RACE
}()
}
Detect with go test -race. Fix with channels, sync.Mutex, or sync/atomic.
3 · Fill the gaps
A version-proof fan-out: each goroutine must get its own copy, and every result must be drained.
results := make(chan string, len(hosts))
for _, host := range hosts {
go func(h string) {
results <- ping()
}()
}
for range hosts {
fmt.Println()
}
Next: the Build section — make your reporter concurrent. Every mistake above will try to happen there.