Skip to content

Channels

A channel is a typed conduit that lets goroutines communicate and synchronise by passing values. One goroutine sends; another receives. The channel handles the hand-off safely, so you don't need locks to move data between goroutines.

Go's motto: don't communicate by sharing memory; share memory by communicating. Prefer passing values over a channel to guarding shared variables with a mutex.

Create one with make, send with ch <- v, receive with v := <-ch:

A common use is returning a result from a goroutine — the channel both delivers the value and synchronises, so no shared variable is needed:

total := make(chan int)
go func() {
    sum := 0
    for _, n := range []int{1, 2, 3, 4, 5} {
        sum += n
    }
    total <- sum        // send the result back to main
}()
fmt.Println(<-total)    // output: 15

Unbuffered channels synchronise

The channel above is unbuffered: a send blocks until another goroutine is ready to receive, and vice versa. The exchange is a rendezvous — both sides meet at the same instant. That makes an unbuffered channel a synchronisation tool, not just a pipe: the receive can't complete before the send happens.

done := make(chan struct{})
go func() {
    fmt.Println("working")
    done <- struct{}{}      // signal completion
}()
<-done                       // blocks until the goroutine signals
fmt.Println("finished")
// output:
// working
// finished

A chan struct{} is the idiomatic "signal only, no data" channel.

Buffered channels hold values

make(chan T, n) gives the channel a buffer of n. Sends block only when the buffer is full; receives block only when it's empty. This decouples sender and receiver in bursts.

jobs := make(chan string, 2)   // a small job queue
jobs <- "email #1"             // doesn't block — buffer has room
jobs <- "email #2"             // doesn't block — now full
fmt.Println(len(jobs), cap(jobs))   // output: 2 2
fmt.Println(<-jobs, <-jobs)          // output: email #1 email #2

len is how many values are buffered right now; cap is the buffer size. Use a buffer when you knowingly want slack; reach for unbuffered by default, since it gives you synchronisation for free.

Closing a channel

close(ch) marks that no more values will be sent. Receivers can still drain whatever's buffered, then get the zero value. The comma-ok receive distinguishes a real value from "closed and empty":

ch := make(chan int, 2)
ch <- 10
close(ch)

v, ok := <-ch
fmt.Println(v, ok)   // output: 10 true   — a real value
v, ok = <-ch
fmt.Println(v, ok)   // output: 0 false   — closed and drained

Rules: only the sender should close, and only once. Sending on a closed channel panics; closing an already-closed channel panics. Closing is a broadcast — every receiver sees it.

Ranging over a channel

for v := range ch receives values until the channel is closed and drained, then ends the loop. It's the clean way to consume a stream:

greetings := make(chan string)

go func() {                          // producer
    for _, name := range []string{"alice", "bob", "carol"} {
        greetings <- "hello, " + name
    }
    close(greetings)                 // no more values; without this, range blocks forever
}()

for g := range greetings {           // consumer — ends when the channel closes
    fmt.Println(g)
}
// output:
// hello, alice
// hello, bob
// hello, carol

Channel direction in signatures

A function parameter can restrict a channel to send-only (chan<- T) or receive-only (<-chan T). This documents intent and lets the compiler stop misuse.

func produce(out chan<- int) { out <- 42; close(out) }   // can only send
func consume(in <-chan int)  { fmt.Println(<-in) }        // can only receive

ch := make(chan int, 1)
produce(ch)
consume(ch)            // output: 42

Deadlocks and nil channels

If every goroutine is blocked waiting on a channel, the runtime detects it and aborts:

func main() {
    ch := make(chan int)
    <-ch        // nothing will ever send
}
// fatal error: all goroutines are asleep - deadlock!

A nil channel (never make-d) blocks forever on both send and receive — occasionally useful in select to disable a case, but otherwise a bug.

Quick reference

Operation Meaning
make(chan T) unbuffered — send/receive rendezvous
make(chan T, n) buffered — blocks only when full/empty
ch <- v / v := <-ch send / receive
v, ok := <-ch ok is false once closed and drained
close(ch) no more sends; sender-only, once
for v := range ch receive until closed
chan<- T / <-chan T send-only / receive-only parameter
send on closed channel panic
all goroutines blocked fatal: deadlock

Sources