Skip to content

Intermediate · Concurrency: goroutines, channels and context · topic 27 of 46 · 40 min

Channels: passing data between goroutines

Explanation

A channel is a typed pipe that one goroutine sends values into and another receives from. On an unbuffered channel, the sender waits until a receiver takes the value; a buffered channel holds a few values first. The sender closes a channel to say "no more values", and range on a channel reads until it is closed. If every goroutine is stuck waiting, Go stops with "all goroutines are asleep - deadlock!".

Why channels?

Goroutines often need to hand results or work to each other. Sharing a variable needs locks and care. Channels give a safer way: one goroutine sends a value, another receives it, and the hand-over itself is synchronised. Go's motto is: "Do not communicate by sharing memory; instead, share memory by communicating."

Think of it like this: An unbuffered channel is like handing a parcel directly to a person: you wait at the door until someone takes it. A buffered channel is like a letter box with room for 3 letters: you can drop letters and leave, until the box is full.

Making, sending and receiving

go
ch := make(chan string)       // unbuffered channel of strings

go func() {
	ch <- "order 101 ready"     // send: arrow points INTO the channel
}()

msg := <-ch                     // receive: arrow points OUT of the channel
fmt.Println(msg)               // order 101 ready
The receive waits until a value arrives, so main does not exit early.

Unbuffered versus buffered

Unbuffered make(chan T)Buffered make(chan T, 3)
Send blocks whenNo receiver is readyThe buffer is full
Receive blocks whenNo sender is readyThe buffer is empty
Gives youA guaranteed hand-over (both sides meet)Some slack between a fast sender and a slower receiver
go
jobs := make(chan int, 2)
jobs <- 1     // does not block: buffer has room
jobs <- 2     // does not block: buffer now full
// jobs <- 3  // would block until someone receives
fmt.Println(len(jobs), cap(jobs)) // 2 2
A buffer is not a fix for a design problem; it only adds slack.

Closing and ranging

close(ch) tells receivers that no more values will come. A for range loop over a channel receives values until the channel is closed and empty, then ends. The two-value receive v, ok := <-ch gives ok == false once the channel is closed and drained.

go
func produce(n int, out chan<- int) {
	for i := 1; i <= n; i++ {
		out <- i * i
	}
	close(out) // the SENDER closes
}

func main() {
	squares := make(chan int)
	go produce(4, squares)
	for v := range squares {   // stops when squares is closed
		fmt.Print(v, " ")
	}
	// 1 4 9 16
}
chan<- int is a send-only channel type, so produce cannot receive by mistake.

Rules that cause panics and hangs

ActionResult
Send on a closed channelpanic: send on closed channel
Close an already closed channelpanic: close of closed channel
Receive from a closed channelReturns the zero value immediately (ok is false)
Send or receive on a nil channelBlocks forever
Every goroutine blocked on channels or locksfatal error: all goroutines are asleep - deadlock!
go
func main() {
	ch := make(chan int)
	ch <- 1          // nobody will ever receive
	fmt.Println(<-ch)
}
// fatal error: all goroutines are asleep - deadlock!
main blocks forever on the send; Go detects that no goroutine can make progress.
  • Only the sender closes, and only when it is sure nothing else will send. Receivers never close.
  • You do not have to close every channel. Close only when receivers need to know "no more values" (for example to end a range loop).
  • With several senders, use a WaitGroup and close the channel once after all of them are done.

Directional channel types

chan<- T can only send; <-chan T can only receive. A normal chan T converts to either automatically when you pass it to a function. This documents who does what, and the compiler stops misuse.

A worker pool

go
func worker(id int, jobs <-chan int, results chan<- string, wg *sync.WaitGroup) {
	defer wg.Done()
	for j := range jobs {
		results <- fmt.Sprintf("worker %d did job %d", id, j)
	}
}

func main() {
	jobs := make(chan int, 10)
	results := make(chan string, 10)
	var wg sync.WaitGroup

	for w := 1; w <= 3; w++ {   // 3 workers
		wg.Add(1)
		go worker(w, jobs, results, &wg)
	}
	for j := 1; j <= 6; j++ {   // 6 jobs
		jobs <- j
	}
	close(jobs)                 // workers' range loops will end

	go func() {
		wg.Wait()
		close(results)          // close once, after all workers finish
	}()
	for r := range results {
		fmt.Println(r)
	}
}
The standard pattern: a fixed number of workers limits how much runs at once (for example, how many requests hit a bank API).

Need help?

Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.

Try:

Watch the lesson

Channels: passing data between goroutines · 1/7

Channels

Channels let goroutines pass values safely. Share memory by communicating.

Prefer a video by a teacher? Find free videos on “golang channels buffered unbuffered close range worker pool” on YouTube ↗

In real life: Processing a day's orders with 4 workers

An e-commerce back office generates GST invoices for 50,000 orders every night. Starting 50,000 goroutines that all hit the database at once would overload it. Instead, a channel of order IDs feeds 4 workers, so at most 4 database queries run at a time, and a results channel collects totals for the report.

go
package main

import (
	"fmt"
	"sync"
)

func main() {
	orders := make(chan int)
	totals := make(chan int)
	var wg sync.WaitGroup

	for range 4 {
		wg.Add(1)
		go func() {
			defer wg.Done()
			for id := range orders {
				totals <- id * 100 // pretend: build invoice, return amount
			}
		}()
	}

	go func() {
		for id := 1; id <= 10; id++ {
			orders <- id
		}
		close(orders)
	}()

	go func() {
		wg.Wait()
		close(totals)
	}()

	sum := 0
	for t := range totals {
		sum += t
	}
	fmt.Println("sum:", sum) // 5500
}
Sum of 100+200+...+1000 is 5500, whatever order the workers finish in.

Practice (0/4 done)

Task 1 · Question

What happens? func main() { ch := make(chan int) ch <- 5 fmt.Println(<-ch) }

Task 2 · Question

What does this print? ch := make(chan int, 1) ch <- 5 fmt.Println(<-ch)

Task 3 · Question

What does this print? ch := make(chan int, 2) ch <- 1 close(ch) a, ok1 := <-ch b, ok2 := <-ch fmt.Println(a, ok1, b, ok2)

Task 4 · Practice

Build a pipeline of three stages connected by channels: generate the numbers 1 to 10, square each one in a second goroutine, and sum them in main. Each stage closes its output channel when done.

Quick quiz

Question 1

1.Who should close a channel?

Question 2

2.What is the type of a receive-only channel of ints?

Question 3

3.What happens when you send on a closed channel?

Question 4

4.What does a buffered channel of capacity 3 allow?

Question 5

5.What is the Go proverb about concurrency?

Read more (free)

Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.

Who should close a channel?

Answer: The sender, when it will send no more

Only the sending side knows when there are no more values.

What is the type of a receive-only channel of ints?

Answer: <-chan int

<-chan int can only receive; chan<- int can only send.

What happens when you send on a closed channel?

Answer: panic: send on closed channel

Sending on a closed channel always panics.

What does a buffered channel of capacity 3 allow?

Answer: Up to 3 sends without a waiting receiver

Sends block only when the buffer is full.

What is the Go proverb about concurrency?

Answer: Share memory by communicating

Pass data through channels instead of sharing variables where you can.

A goroutine panics without recover. What happens?Related

Answer: The whole program crashes

An unrecovered panic in any goroutine terminates the process.

From Concurrency interview questions and coding rounds

Why set a channel variable to nil inside a fan-in loop?Related

Answer: Because operations on a nil channel block forever, so that case is disabled

A nil channel case can never be selected.

From select: waiting on several channels

How do several threads send on the same channel?Related

Answer: Clone the Sender for each thread

Sender can be cloned; each clone can move into a thread.

From Message passing with channels

What happens to running goroutines when main returns?Related

Answer: They are stopped because the program exits

The program ends when main returns.

From Goroutines and WaitGroup

Pass the quiz (60% or more) or mark this topic as done when you are ready.

Your progress is saved on this device only.

Next steps

  1. 3 more topics in “Concurrency: goroutines, channels and context” before its exam: select: waiting on several channels, Mutex, Once, atomic and the race detector, context: timeouts and cancellation.
  2. Module 6 exam: Concurrency · 15 questions · 25 min, without help. 5 topics of this module still to finish first.
  3. Then the next module, Standard library essentials. 46 topics left in the course before the final exam.

← Previous: Goroutines and WaitGroup