select is a control structure that blocks until one of several channel operations is ready, choosing randomly if more than one is ready at once — it’s how a goroutine waits on multiple channels simultaneously. The standard library’s sync package covers coordination that channels aren’t suited for: sync.Mutex guards a shared variable from concurrent access, and sync.WaitGroup blocks until a set of goroutines has finished.
select: waiting on multiple channels
Each case in a select is a channel send or receive. select blocks until one case is ready, and a default case makes the whole select non-blocking.
select {
case msg := <-ch1:
fmt.Println("from ch1:", msg)
case msg := <-ch2:
fmt.Println("from ch2:", msg)
case <-time.After(2 * time.Second):
fmt.Println("timed out")
default:
fmt.Println("nothing ready right now") // makes select non-blocking
}sync.WaitGroup: waiting for goroutines to finish
A WaitGroup counts outstanding goroutines. Add increments the count before launching a goroutine, each goroutine calls Done when finished (typically via defer), and Wait blocks until the count returns to zero.
var wg sync.WaitGroup
for i := 0; i < 3; i++ {
wg.Add(1)
go func(n int) {
defer wg.Done()
fmt.Println("worker", n, "done")
}(i)
}
wg.Wait() // blocks until all 3 workers call Done()
fmt.Println("all workers finished")sync.Mutex: protecting shared data
Goroutines share memory by default, which means a variable written by multiple goroutines needs protection from concurrent access (a data race). sync.Mutex provides Lock/Unlock to make a critical section run one goroutine at a time; sync.Once guarantees a function runs exactly once no matter how many goroutines call it.
var mu sync.Mutex
counter := 0
func increment() {
mu.Lock()
defer mu.Unlock()
counter++ // safe: only one goroutine at a time reaches here
}