Goroutines and sync.WaitGroup
A goroutine is a function running concurrently with the rest of your program. You start one by putting go in front of a function call: go work(). It costs a few kilobytes of stack, so a program can run hundreds of thousands of them — the Go runtime multiplexes them onto a small number of OS threads for you.
The catch every beginner hits: when main returns, the program exits, and every goroutine still running is killed mid-sentence. You need a way to wait. sync.WaitGroup is a counter: Add(1) before starting each goroutine, Done() when it finishes, and Wait() blocks until the counter is back to zero.
package main
import (
"fmt"
"sync"
)
func square(n int) int { return n * n }
func main() {
nums := []int{2, 3, 4, 5}
results := make([]int, len(nums))
var wg sync.WaitGroup
for i, n := range nums {
wg.Add(1)
go func() {
defer wg.Done()
results[i] = square(n) // each goroutine owns one slot
}()
}
wg.Wait()
fmt.Println(results)
}[4 9 16 25]Each goroutine writes to its own index, so there is no shared write and the printed order is always the input order — whichever goroutine happens to finish first.
Replace the wg.Add(1) / go func / defer wg.Done() trio with wg.Go(func() { results[i] = square(n) }) — the shorthand added in Go 1.25.
i and n were one shared variable for the whole loop, and every goroutine above could see the last value. Old code works around it with go func(i, n int) { … }(i, n). With go 1.22 or later in go.mod, each iteration gets fresh variables and the closure is safe as written.panic: sync: negative WaitGroup counter
package main
import "sync"
func main() {
var wg sync.WaitGroup
wg.Done()
wg.Wait()
}panic: sync: negative WaitGroup counter
goroutine 1 [running]:
sync.(*WaitGroup).Add(0x38342f788000, 0xffffffffffffffff)
/usr/local/go/src/sync/waitgroup.go:118 +0x264
sync.(*WaitGroup).Done(...)
/usr/local/go/src/sync/waitgroup.go:156
main.main()
./main.go:7 +0x3c
exit status 2Done() is just Add(-1). Calling it more times than you called Add drives the counter below zero, which can only mean the bookkeeping is wrong, so the runtime panics instead of guessing.
Call wg.Add(1) exactly once per goroutine, before the go statement (never inside the goroutine — Wait might run before it), and defer wg.Done() as its first line. Or use wg.Go(f), which does both for you.
package main
import "sync"
func main() {
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
}()
wg.Wait()
}