Goroutines and channels
Concurrency as a language feature: cheap threads, channel communication, and the patterns for waiting, timing out and cancelling.
Goroutines and channels
A goroutine is a function running concurrently, scheduled onto a small pool of operating-system threads. Its initial stack is a few kilobytes, so thousands are routine.
func worker(jobs <-chan int, results chan<- int) {
for j := range jobs { // exits when jobs is closed
results <- j * j
}
}
func main() {
jobs := make(chan int, 10)
results := make(chan int, 10)
for w := 0; w < 3; w++ {
go worker(jobs, results) // three workers, one queue
}
for i := 1; i <= 5; i++ {
jobs <- i
}
close(jobs) // only the sender closes
for i := 0; i < 5; i++ {
fmt.Println(<-results)
}
}go f(x)returns immediately; the goroutine runs concurrently and the main function does not wait for it.- An unbuffered channel synchronises: a send blocks until a receive is ready. A buffered channel blocks only when the buffer is full.
close(ch)means "no more values". Receiving from a closed channel returns the zero value at once; sending to a closed channel panics.- Declare direction in signatures with
<-chan Tandchan<- Tso the compiler enforces who may send and who may close. - By default goroutines share memory. Use channels to pass data, and a mutex only for state that is genuinely shared.
⚠️
A goroutine blocked forever is a leak, and leaks accumulate until the process is killed. Every blocking receive needs an exit path: a channel that gets closed, a
select with ctx.Done(), or a timeout.Coordinating work
| Tool | Use it for |
|---|---|
sync.WaitGroup | Waiting for a known set of goroutines to finish |
sync.Mutex / RWMutex | Protecting shared mutable state |
| Buffered channel | A bounded queue, which also provides backpressure |
select | Waiting on several channels, with default or time.After for timeouts |
context.Context | Cancellation and deadlines propagated down a call chain |
errgroup.Group | A wait group that stops early and returns the first error |
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
select {
case v := <-ch:
fmt.Println("got", v)
case <-ctx.Done():
fmt.Println("timeout:", ctx.Err())
}
// run the race detector before you trust any concurrent code
// go test -race ./...When several goroutines write to a channel, close it only after every writer has finished — typically with a WaitGroup and a small goroutine whose only job is to wait then close.
FAQ
What happens if I read from a closed channel?
You get the zero value immediately, and the comma-ok form reports
false. That is why only the sender closes: a receiver that closes can cause a still-running sender to panic.How do I find data races?
Run tests with
go test -race. The detector reports the two conflicting accesses at the moment the race occurs, which is far cheaper than debugging a corrupted result weeks later.Related
Slices and maps Go syntax, types and functions
Last refreshed 2026-09-18.