Channels
Learn how to create channels with make(chan T), send and receive values, the difference between buffered and unbuffered channels, and the comma-ok idiom.
Introduction
Goroutines can run independently, but they usually need to talk to each other — to hand off a result, signal completion, or coordinate work. Channels are Go's built-in way to do that safely. The Go proverb sums it up well: "Do not communicate by sharing memory; instead, share memory by communicating." Channels are how that communication happens.
- How to create a channel with make(chan T).
- How to send and receive values through a channel.
- The difference between buffered and unbuffered channels.
- How to close a channel and detect that it is closed.
- How to range over a channel to consume all its values.
Creating Channels
A channel is a typed conduit for values, created with the built-in make function. chan int, for example, is a channel that only carries int values.
ch := make(chan int) // unbuffered channel of intnames := make(chan string, 3) // buffered channel of string, capacity 3Sending and Receiving
The <- operator both sends and receives, depending on which side of the channel it appears. ch <- value sends value into the channel; value := <-ch receives a value from the channel.
package main
import "fmt"
func worker(ch chan string) { ch <- "work is done" // send}
func main() { ch := make(chan string)
go worker(ch)
result := <-ch // receive (blocks until a value arrives) fmt.Println(result)}Click Run to see what this code prints.
This example needs no time.Sleep at all: the receive on the main goroutine blocks until the worker goroutine sends a value, which is exactly the synchronization channels give you for free.
Buffered vs Unbuffered Channels
An unbuffered channel (make(chan T)) has no storage capacity — a send blocks until another goroutine is ready to receive, and vice versa. This makes unbuffered channels a synchronization point as much as a data pipe. A buffered channel (make(chan T, n)) can hold up to n values without a receiver being ready; a send only blocks once the buffer is full.
package main
import "fmt"
func main() { buffered := make(chan int, 2)
buffered <- 1 // does not block, buffer has room buffered <- 2 // does not block, buffer now full
fmt.Println(<-buffered) fmt.Println(<-buffered)}Click Run to see what this code prints.
| Type | Behavior |
|---|---|
| Unbuffered — make(chan T) | Send blocks until a receiver is ready; strong synchronization guarantee. |
| Buffered — make(chan T, n) | Send only blocks once n unreceived values are queued; looser coupling. |
Closing a Channel
A sender can close a channel with close(ch) to signal that no more values will be sent. Receivers can still drain any values already in the buffer after a close, but sending on a closed channel panics.
package main
import "fmt"
func main() { ch := make(chan int, 3) ch <- 1 ch <- 2 ch <- 3 close(ch)
for i := 0; i < 3; i++ { fmt.Println(<-ch) }}Click Run to see what this code prints.
The Comma-Ok Idiom
Receiving from a channel supports a second return value that reports whether the channel is still open. This is the same comma-ok pattern used for map lookups and type assertions.
package main
import "fmt"
func main() { ch := make(chan int, 2) ch <- 10 close(ch)
v, ok := <-ch fmt.Println(v, ok) // value still buffered, channel open-ish: ok is true
v, ok = <-ch fmt.Println(v, ok) // channel drained and closed: ok is false}Click Run to see what this code prints.
Once a closed channel is fully drained, every further receive returns the zero value of its type along with ok set to false — this is how a receiver detects "there is truly nothing left to read."
Ranging Over a Channel
A for...range loop over a channel receives values one at a time and automatically stops when the channel is closed and drained — no manual comma-ok checking required.
package main
import "fmt"
func generateSquares(n int) <-chan int { ch := make(chan int) go func() { defer close(ch) for i := 1; i <= n; i++ { ch <- i * i } }() return ch}
func main() { for square := range generateSquares(5) { fmt.Println(square) }}Click Run to see what this code prints.
generateSquares returns a receive-only channel (<-chan int), a common and idiomatic way to expose a channel that callers should only read from, never send to or close.
Common Mistakes
- Sending on a closed channel, which panics immediately.
- Closing a channel more than once, which also panics.
- Closing a channel from the receiving side instead of the sending side.
- Forgetting that an unbuffered send blocks forever if no one ever receives, causing a deadlock.
- Ranging over a channel that is never closed, causing the loop to hang forever.
Best Practices
- Only the sender should close a channel, never the receiver.
- Use directional channel types (chan<- T, <-chan T) in function signatures to document intent.
- Prefer unbuffered channels for pure synchronization; use buffered channels to decouple producer and consumer speed.
- Always have a clear plan for how and when a channel gets closed.
- Use the comma-ok idiom or range instead of assuming a channel always has data.
Frequently Asked Questions
No. You only need to close a channel when receivers need to know it is finished, such as when ranging over it. Channels are garbage collected like any other value once nothing references them.
It blocks forever. A nil channel (the zero value of a channel variable) is never ready to send or receive, which is occasionally used deliberately to disable a case in a select statement.
Yes, channels are safe for concurrent use by multiple goroutines sending and receiving at the same time — that safety is built into the channel implementation itself.
Key Takeaways
- Channels are typed conduits created with make(chan T) for communicating between goroutines.
- Unbuffered channels synchronize sender and receiver; buffered channels allow some slack.
- close(ch) signals that no more values will be sent, and sending afterward panics.
- The comma-ok idiom (v, ok := <-ch) detects whether a channel is closed and drained.
- range over a channel automatically receives values until the channel closes.
Summary
Channels turn goroutine coordination from a shared-memory hazard into a straightforward message-passing model. Whether you need tight synchronization with an unbuffered channel or a bit of slack with a buffered one, channels are the idiomatic way goroutines exchange data safely. Next, you will learn select — the tool for working with multiple channels at once.