Lesson 15 of 30 intermediate 4 min read

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.

producer channel consumer
A channel decouples producer from consumer — they share data through the pipe, not through shared variables.

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 <- v sends, <-ch receives.
  • Unbuffered channels synchronize sender and receiver; buffered ones hold a few values and apply backpressure.
  • close signals the end of a stream; range receives 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.