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 T and chan<- T so 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

ToolUse it for
sync.WaitGroupWaiting for a known set of goroutines to finish
sync.Mutex / RWMutexProtecting shared mutable state
Buffered channelA bounded queue, which also provides backpressure
selectWaiting on several channels, with default or time.After for timeouts
context.ContextCancellation and deadlines propagated down a call chain
errgroup.GroupA 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.

Slices and maps Go syntax, types and functions

Last refreshed 2026-09-18.