Before this:Goroutines
Channels
Key takeaways
A channel is a typed pipe that connects goroutines: one sends with ch <- v
and another receives with v := <-ch. An unbuffered channel makes sender
and receiver rendezvous; a buffered one holds a few values. Channels let
goroutines share data without shared-memory bugs — the heart of Go’s concurrency
model.
Goroutines run independently; channels are how they talk. This lesson covers Go’s central concurrency tool.
Creating and using a channel
ch := make(chan int) // a channel that carries ints
go func() {
ch <- 42 // send 42 into the channel
}()
v := <-ch // receive; v is 42
The arrow points the way the data flows: ch <- v sends into the channel,
<-ch receives out of it. Because this channel is unbuffered, the send blocks
until the receive happens — the two goroutines meet at that moment. That built-in
synchronization is exactly why you don’t need locks to hand a value from one
goroutine to another.
Buffered channels
Give make a size and the channel can hold values without an immediate receiver:
results := make(chan []byte, 8) // holds up to 8 before a send blocks
A buffered channel lets a fast producer stay ahead of a slower consumer — useful in a signal pipeline where samples arrive in bursts. When the buffer is full, the next send blocks until room opens up, which naturally applies backpressure.
Ranging and closing
A sender can close a channel to signal “no more values,” and a receiver can
range over it until it’s drained:
go func() {
for _, frame := range frames {
out <- frame
}
close(out) // no more frames coming
}()
for frame := range out { // loops until out is closed and empty
process(frame)
}
Closing is a broadcast that the stream has ended — only the sender should close, and only once.
Why this is safer
The classic concurrency bug is a data race: two goroutines touching the same
variable at once, one of them writing. Channels sidestep it by passing a value’s
ownership along the pipe — at any moment, only one goroutine holds it. That’s the
meaning of Go’s motto, share memory by communicating. When channels aren’t the
right fit, Go’s sync package offers locks — the subject of the next lesson.
Quick check: on an unbuffered channel, what happens when you send a value?
Recap
- A channel carries typed values between goroutines:
ch <- vsends,<-chreceives. - Unbuffered channels synchronize sender and receiver; buffered ones hold a few values and apply backpressure.
closesignals the end of a stream;rangereceives until it’s drained.- Channels prevent data races by passing ownership instead of sharing variables.
Next up: coordinating multiple channels with select, and when to reach for locks instead.
Frequently asked questions
What is the difference between a buffered and an unbuffered channel?
An unbuffered channel has no storage — a send blocks until another goroutine is ready to receive, so the two rendezvous. A buffered channel (make(chan T, n)) holds up to n values, so a send only blocks when the buffer is full. Unbuffered channels synchronize two goroutines; buffered channels let a fast producer run ahead of a slower consumer up to the buffer size.
Do I always need channels for concurrency?
No. Channels are the idiomatic way to pass data between goroutines, but sometimes plain shared state guarded by a mutex from the sync package is simpler — for example, a counter many goroutines increment. The next lesson covers when to reach for sync instead. The guideline is to use channels to move data and ownership, and mutexes to protect small pieces of shared state.