This tutorial is for developers learning concurrency in Go: it explains what channels are, how send and receive work, why blocking matters, and how close, for range, select, and directional types fit together. The sections move from syntax to behavior, then to patterns and mistakes so you can use channels safely with goroutines. For the go keyword and scheduling basics, start with goroutines in Go; this page stays focused on channels rather than a full concurrency course.
Go 1.24 on Linux.
Quick answer: what is a channel in Go?
A channel is a typed way for goroutines to send and receive values. You create one with make(chan T), send with ch <- v, and receive with v := <-ch.
What is a channel in Go?
Typed communication between goroutines
A value of type chan T carries values of type T. The compiler keeps producers and consumers aligned on the same type, so a channel is a typed pipe between goroutines.
Why channels are useful
Channels combine communication with synchronization: an unbuffered send does not finish until another goroutine receives, so you can coordinate stages of work without ad‑hoc mutexes for every handoff. Buffered channels add a small queue when a fixed amount of decoupling helps; capacity, blocking, and choosing n are covered in the buffered section below and in the linked guide there.
Create a channel
Declare a channel with chan T, then allocate with make. The zero value of chan T is nil; sends and receives on nil block forever unless select picks another case.
package main
import "fmt"
func main() {
unbuf := make(chan string)
buf := make(chan int, 3)
var nilCh chan int
fmt.Println(unbuf != nil, cap(buf), nilCh == nil)
}You should see true, 3, and true.
Syntax overview:
Send and receive from a channel
The arrow shows which way data moves. ch <- v sends v on ch; v := <-ch receives one value from ch into v. On the left of chan, <- attaches to the channel type; in statements, read <- as “data flows from right to left into the channel or variable.”
package main
import (
"fmt"
"time"
)
func main() {
ch := make(chan string)
go func() {
ch <- "ping"
ch <- "pong"
ch <- "done"
}()
fmt.Println(<-ch)
fmt.Println(<-ch)
fmt.Println(<-ch)
time.Sleep(10 * time.Millisecond)
}You should see ping, pong, then done.
Blocking behavior
Channel operations can block. That behavior is what makes unbuffered channels useful for handoffs and signals between goroutines.
Send blocks until a receive
With an unbuffered channel (make(chan T) or make(chan T, 0)), a send does not complete until another goroutine receives the value.
package main
import "fmt"
func main() {
ch := make(chan string)
go func() {
ch <- "done"
}()
msg := <-ch
fmt.Println(msg)
fmt.Println("after receive, the send has also completed")
}You should see done, then a line stating that the receive finished and the send has completed. The send unblocks only when main receives, so the two lines always appear in that order.
This is why a basic Go channel example often starts a goroutine to send and receives from main.
Receive blocks until a value is available
A receive blocks when no value is ready yet (and the channel is not closed).
package main
import (
"fmt"
"time"
)
func main() {
ch := make(chan string)
go func() {
time.Sleep(200 * time.Millisecond)
ch <- "work finished"
}()
fmt.Println("waiting...")
msg := <-ch
fmt.Println(msg)
}You should see waiting..., a short pause, then work finished.
The receive waits until the goroutine sends. That lets one goroutine wait for another without a busy loop.
Blocking as synchronization
Because an unbuffered send and receive wait for each other, a channel can be used as a simple one-shot signal.
package main
import "fmt"
func main() {
done := make(chan struct{})
go func() {
fmt.Println("step one finished")
done <- struct{}{}
}()
<-done
fmt.Println("step two starts")
}You should see step one finished, then step two starts. main does not print step two until the goroutine sends on done.
For larger programs, this is often combined with context so goroutines can also stop early when cancellation or timeout happens.
On an open unbuffered channel, send waits for receive and receive waits for send, so the two sides rendezvous.
Buffered vs unbuffered channels
Unbuffered make(chan T): each send synchronizes with a receive—no storage between them.
Buffered make(chan T, n): up to n sends can complete without a waiting receiver until the buffer fills; receives block when the buffer is empty. Sends block when len(ch) == cap(ch).
package main
import "fmt"
func main() {
ch := make(chan int, 2)
ch <- 1
ch <- 2
fmt.Println(len(ch), <-ch, len(ch))
}You should see 2, 1, then 1.
For diagrams of blocking, deadlocks, and choosing n, read the dedicated guide on golang buffered channel behavior.
Close and range over channels
Only the sender (the goroutine that knows no more values will be sent) should call close(ch). After close, receivers still drain any buffered values; then for v := range ch stops. Use v, ok := <-ch: when the channel is closed and empty, ok is false and v is the zero value of the element type.
package main
import "fmt"
func main() {
ch := make(chan int, 3)
for _, v := range []int{10, 20, 30} {
ch <- v
}
close(ch)
for v := range ch {
fmt.Println(v)
}
}You should see 10, 20, and 30 on separate lines.
Channel direction
Function parameters can restrict direction: chan<- T is send‑only, <-chan T is receive‑only. That documents intent and stops callers from accidentally receiving from a channel you only meant to write to.
package main
import "fmt"
func sendOnly(ch chan<- int) { ch <- 42 }
func recvOnly(ch <-chan int) int { return <-ch }
func main() {
ch := make(chan int, 1)
sendOnly(ch)
fmt.Println(recvOnly(ch))
}You should see 42.
select with channels
select waits until one of its channel cases can proceed (or runs default immediately if no case is ready). It is the standard way to wait on multiple channels, implement timeouts, or poll without busy waiting.
package main
import "fmt"
func main() {
a := make(chan int)
b := make(chan int)
go func() { a <- 1 }()
go func() { b <- 2 }()
received := 0
for received < 2 {
select {
case v := <-a:
fmt.Println("from a", v)
received++
case v := <-b:
fmt.Println("from b", v)
received++
}
}
}You should see two lines labeled from a and from b in some order.
Timeout with select
package main
import (
"fmt"
"time"
)
func main() {
ch := make(chan string)
select {
case <-ch:
fmt.Println("msg")
case <-time.After(10 * time.Millisecond):
fmt.Println("timeout")
}
}With nothing sending on ch, you should see timeout.
Non-blocking send or receive
A default branch runs when no channel operation is ready:
package main
import "fmt"
func main() {
ch := make(chan int)
select {
case v := <-ch:
fmt.Println(v)
default:
fmt.Println("no value ready")
}
}You should see no value ready.
Common channel patterns
Keep these ideas small in production code; expand them using the linked tutorials.
- Worker pool: fixed goroutines read jobs from a
jobschannel and send results on aresultschannel; see concurrency in Go for broader patterns. - Fan-out / fan-in: split work across workers, then merge on one channel; see fan-out fan-in in Go.
- Done channel: a
chan struct{}closed to broadcast shutdown (often replaced bycontexttoday). - Semaphore: a buffered channel of size
nholdsntokens; send to acquire and receive to release to cap concurrency. - Pipeline: stages connected by channels, each doing one transformation; combine with clear close rules so
rangeterminates.
For several receivers on one stream, read multiple receivers on one channel.
Common mistakes
Deadlock from send without receiver: every unbuffered send needs a concurrent receive (or a buffer with free space). If main sends on an unbuffered channel and nothing else receives, the program blocks.
Reading forever from a channel that is never closed: for v := range ch blocks until close(ch). If the producer never closes, the loop never ends.
Closing from the wrong side: receivers should not close a channel the sender still writes to; close when sends are finished, from that owner.
Sending on a closed channel: this panics. Close once, after the last send.
Nil vs closed: a closed, drained channel yields ok == false on receive. A nil channel blocks sends and receives; in select, it is sometimes used to disable a case until you assign a real channel.
Assuming a buffered channel never blocks: when the buffer is full, send still blocks until space appears.
len and cap
len(ch) is the number of values queued in the buffer (often 0 on an unbuffered channel during a synchronous handoff). cap(ch) is the buffer size, or 0 for unbuffered channels. These are observability helpers, not synchronization primitives.
package main
import "fmt"
func main() {
ch := make(chan int, 5)
for i := 0; i < 3; i++ {
ch <- i
}
fmt.Println(len(ch), cap(ch))
}You should see 3 and 5.
Go channel cheat sheet
| Goal | Pattern |
|---|---|
| Create unbuffered channel | make(chan T) |
| Create buffered channel | make(chan T, n) |
| Send value | ch <- v |
| Receive value | v := <-ch |
| Receive with closed check | v, ok := <-ch |
| Loop until closed | for v := range ch |
| Close channel | sender closes when no more values |
| Send-only parameter | chan<- T |
| Receive-only parameter | <-chan T |
| Wait on multiple channels | select |
| Timeout | select with time.After or context |
| Limit concurrency | buffered channel as semaphore |
| Multiple workers | worker pool (jobs/results channels) |
Summary
Channels in Go are typed queues for communicating between goroutines: you allocate with make, move data with <-, and rely on blocking rules to synchronize unbuffered handoffs. Buffered channels add bounded queueing; closing enables for range and comma‑ok receives. Directional types clarify APIs, select multiplexes operations and timeouts, and patterns like worker pools and pipelines compose channels with goroutines. Avoid deadlocks by pairing operations, close only from the sender when sends are done, never send on a closed channel, and treat nil channels as permanently blocking unless select says otherwise. Follow the internal links in the sections above for deeper topics; external references are below.
References
- A Tour of Go: Channels
- Go 101: channels
- The Go Memory Model (ordering with channels)

