Channels

A channel is a typed pipe that goroutines use to send values to each other. Sending and receiving also synchronise the goroutines involved, so a channel both moves data and coordinates timing. Go's motto sums it up: "Don't communicate by sharing memory; share memory by communicating." This lesson covers unbuffered and buffered channels, closing and ranging, directional channel types, and the mistakes that cause deadlocks.

Sending and receiving

Create a channel with make(chan T). ch <- v sends a value; v := <-ch receives one:

GO
package main

import "fmt"

func square(n int, out chan int) {
	out <- n * n // send
}

func main() {
	results := make(chan int)

	go square(4, results)
	go square(9, results)

	a := <-results // receive: waits until a value arrives
	b := <-results
	fmt.Println(a + b)
}
Output
97

On an unbuffered channel, a send waits until another goroutine receives, and a receive waits until another goroutine sends. The two goroutines meet at that moment, which is why main doesn't need a WaitGroup here: it can't get past the receives until both results have arrived. (Which square arrives first can vary, but the sum is always the same.)

Closing and ranging

A sender can close a channel to say "no more values are coming". A for … range loop over a channel receives until the channel is closed and empty:

GO
package main

import "fmt"

func generate(n int, out chan<- int) {
	for i := 1; i <= n; i++ {
		out <- i * 10
	}
	close(out) // tells the receiver we're done
}

func main() {
	ch := make(chan int)
	go generate(4, ch)

	for v := range ch {
		fmt.Print(v, " ")
	}
	fmt.Println()

	v, ok := <-ch // receiving from a closed channel: zero value, ok == false
	fmt.Println(v, ok)
}
Output
10 20 30 40
0 false

Rules for closing: only the sender should close a channel, never the receiver; closing is only needed when receivers must know there's no more data (as with range); and sending on a closed channel, or closing one twice, panics.

Buffered channels

make(chan T, n) creates a channel with room for n values. Sends only block when the buffer is full, and receives only block when it's empty:

GO
package main

import "fmt"

func main() {
	jobs := make(chan string, 3)
	jobs <- "resize"
	jobs <- "compress"
	jobs <- "upload" // no receiver yet, but there's room: no blocking
	fmt.Println(len(jobs), cap(jobs))

	close(jobs)
	for j := range jobs {
		fmt.Println("doing", j)
	}
}
Output
3 3
doing resize
doing compress
doing upload

A buffer smooths out bursts between a fast producer and a slower consumer, or lets a fixed number of goroutines report results without waiting for the receiver. Don't add buffers just to make a deadlock disappear — that usually hides a design problem.

Directional channel types

A function parameter can be restricted to sending (chan<- T) or receiving (<-chan T). The compiler then stops the function from using the channel the wrong way, and the signature documents the data flow:

GO
package main

import (
	"fmt"
	"strings"
)

func producer(words []string) <-chan string { // returns a receive-only channel
	out := make(chan string)
	go func() {
		defer close(out)
		for _, w := range words {
			out <- w
		}
	}()
	return out
}

func upper(in <-chan string) <-chan string {
	out := make(chan string)
	go func() {
		defer close(out)
		for w := range in {
			out <- strings.ToUpper(w)
		}
	}()
	return out
}

func main() {
	for w := range upper(producer([]string{"pipes", "and", "filters"})) {
		fmt.Println(w)
	}
}
Output
PIPES
AND
FILTERS

This is a small pipeline: each stage is a goroutine that receives from one channel and sends to the next. The pattern of a function that starts a goroutine and returns its output channel is very common.

Deadlock

If every goroutine is blocked waiting on a channel, nothing can ever proceed. The runtime detects when all goroutines are stuck and stops the program:

GO
package main

import "fmt"

func main() {
	ch := make(chan int)
	ch <- 1 // blocks forever: no other goroutine will ever receive
	fmt.Println(<-ch)
}
Output
fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan send]:
…
exit status 2

Typical causes: sending on an unbuffered channel in the same goroutine that should receive, forgetting to close a channel that someone is ranging over, or receiving more values than are sent. When only some goroutines are stuck, the runtime can't tell — those goroutines simply leak.

nil channels

The zero value of a channel type is nil. Sending to or receiving from a nil channel blocks forever. That sounds useless, but it's handy in select statements for switching a case off (next lesson). Always create channels with make before use.

Channel operations summary

Operationnil channelOpen channelClosed channel
Send ch <- vBlocks foreverBlocks until received (or buffer has room)Panics
Receive <-chBlocks foreverBlocks until a value is availableRemaining values, then zero value with ok == false
close(ch)PanicsCloses itPanics

Try it yourself

Start one goroutine per word that sends back the word's length. Receive exactly as many results as you started, and add them up.

Try it yourself · main.go
Edit the code, copy it into main.go in a new folder and run it with go run main.go (Go 1.27 or later). How to install Go
Output
total letters: 25
Next: waiting on several channels with select.