Goroutines and Channels: Go's Concurrency Model
By Eduardo Dionisio
Go's concurrency story is its defining feature. Goroutines give you lightweight concurrent execution; channels give you a safe way to pass data between them. The philosophy is summed up in one line: don't communicate by sharing memory, share memory by communicating.
Goroutines are cheap
go func() {
fmt.Println("running concurrently")
}()
// the program continues immediately — it does not waitA goroutine is a few kilobytes of stack that grows as needed, multiplexed onto OS threads by the runtime. Spawning thousands is routine. This is different from an OS thread, which is heavy and expensive.
Channels: the safe hand-off
ch := make(chan int) // unbuffered: send blocks until a receive is ready
buf := make(chan int, 10) // buffered: sends block only when the buffer is full
go func() { ch <- 42 }() // send
timeout := <-ch // receiveAn unbuffered channel synchronizes: the send blocks until a receiver is ready, and vice versa. A buffered channel decouples the two until the buffer fills. Knowing which you want is the first design decision.
select: waiting on many things at once
select {
case v := <-results:
fmt.Println("got", v)
case <-time.After(2 * time.Second):
fmt.Println("timed out")
default:
fmt.Println("nothing ready")
}select lets one goroutine wait on multiple channels, timeouts, and even a non-blocking default. This single construct replaces a whole class of manual synchronization bugs. Combined with context.Context for cancellation, it is how idiomatic Go programs stay correct under load.