Kodewick
Start learning →
Lesson 20 of 24Contents

Buffered channels and close

Lesson 20 of 24 · Part 5 · Concurrency · about 15 minutes

The channels in the last lesson have no room for a value: a sender waits until someone comes to receive. Give make one more number and the channel gets that many slots. That's a buffered channel. While there's a free slot, a send drops the value off and moves on, without waiting for a receiver:

prog.go
1package main2 3import "fmt"4 5func main() {6	orders := make(chan string, 3)7	orders <- "coffee"8	orders <- "sandwich"9	orders <- "salad"10	fmt.Println(<-orders)11	fmt.Println(<-orders)12	fmt.Println(<-orders)13}
Output
coffee
sandwich
salad

There isn't a single goroutine here besides main, which sends three times and receives three times, and there's no deadlock: the three values wait in the channel's slots. They come out in the order they went in, first in first out, like a queue. Press Step and see orders drawn as three slots that fill up one by one, then empty one by one.

len and cap

cap(ch) is how many slots there are, and len(ch) is how many values are in them right now:

prog.go
1package main2 3import "fmt"4 5func main() {6	queue := make(chan int, 5)7	queue <- 1018	queue <- 1029	fmt.Println(len(queue), cap(queue))10	<-queue11	fmt.Println(len(queue), cap(queue))12	plain := make(chan int)13	fmt.Println(len(plain), cap(plain))14}
Output
2 5
1 5
0 0

An unbuffered channel, like the ones in the last lesson, has a cap of 0: it can't hold a single value, which is why a send waits for a receive.

Full means waiting

Once the slots are used up, a send waits again, until someone takes a value out and frees a slot. If nobody ever does, it's a deadlock:

prog.go
1package main2 3import "fmt"4 5func main() {6	orders := make(chan string, 3)7	orders <- "coffee"8	orders <- "sandwich"9	orders <- "salad"10	orders <- "cake"11	fmt.Println(<-orders)12}
Output
fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan send]:
main.main(...)
	/tmp/sandbox/prog.go:10 +0x1d
exit status 2

What happened: Every goroutine is waiting (on a channel, a lock or a WaitGroup) and none can go on: a deadlock.

At the fourth send all three slots are full. main stops at line 10 to wait for a slot, but no other goroutine will ever take a value out: fatal error: all goroutines are asleep - deadlock!.

With someone taking values out, nothing gets stuck for good; the faster side just has to pause now and then. The kitchen makes five dishes in a row, the pass only holds two plates, and the waiter carries them out one at a time:

prog.go
1package main2 3import "fmt"4 5func main() {6	counter := make(chan string, 2)7	go func() {8		for _, dish := range []string{"soup", "salad", "steak", "rice", "dessert"} {9			counter <- dish10		}11	}()12	for range 5 {13		fmt.Println("Served", <-counter)14	}15}
Output
Served soup
Served salad
Served steak
Served rice
Served dessert

Press Step and watch the kitchen's lane: the first two dishes go into the slots and it moves straight on to the next. Once the slots are full it turns to "stuck sending" and waits until the waiter takes a plate and frees a slot.

close: no more values

When the sender has sent everything, it can call close(ch) to tell the receivers: no new values are coming. After a close, the values still inside can be received as usual; once they're gone, a receive gets the zero value right away. v, ok := <-ch tells you which is which:

prog.go
1package main2 3import "fmt"4 5func main() {6	ch := make(chan int, 2)7	ch <- 108	ch <- 209	close(ch)10	v, ok := <-ch11	fmt.Println(v, ok)12	v, ok = <-ch13	fmt.Println(v, ok)14	v, ok = <-ch15	fmt.Println(v, ok)16}
Output
10 true
20 true
0 false

ok is true: this value was really sent. ok is false: the channel is closed and empty, and v is just the zero value 0.

for range: receive until closed

for v := range ch receives the values one by one, and the loop ends when the channel is closed and empty. It's the usual way to receive values when you don't know how many are coming:

prog.go
1package main2 3import "fmt"4 5func sendPrices(out chan int) {6	for _, p := range []int{12, 30, 8, 50} {7		out <- p8	}9	close(out)10}11 12func main() {13	prices := make(chan int)14	go sendPrices(prices)15	total := 016	for p := range prices {17		total += p18	}19	fmt.Println("Total", total)20}
Output
Total 100

Press Step and watch main and sendPrices hand values across one at a time. After close(out), prices is marked closed, and main's for range leaves the loop once it has the last value. Guess: what if you forget the close?

Guess first

What happens?

1package main2 3import "fmt"4 5func sendPrices(out chan int) {6	for _, p := range []int{12, 30, 8} {7		out <- p8	}9}10 11func main() {12	prices := make(chan int)13	go sendPrices(prices)14	for p := range prices {15		fmt.Println(p)16	}17	fmt.Println("All received")18}

Sending on a closed channel

You can receive from a closed channel, but not send on it. Try it and you get a panic:

prog.go
1package main2 3import "fmt"4 5func main() {6	orders := make(chan string, 3)7	orders <- "coffee"8	close(orders)9	orders <- "cake"10	fmt.Println(len(orders))11}
Output
panic: send on closed channel

goroutine 1 [running]:
main.main(...)
	/tmp/sandbox/prog.go:9 +0x1d
exit status 2

What happened: Nothing can be sent on a channel once it is closed.

panic: send on closed channel: a send on a channel that's already closed. Closing the same channel twice is a panic too (close of closed channel). So the rule is: the sender closes, after its last send. A receiver shouldn't close, because it can't know whether the sender is done. A channel nobody ranges over doesn't need closing at all; Go cleans it up when it's no longer used.

A cook working through orders

Putting this lesson together: main puts the orders into a buffered channel and closes it when it's done; the cook goes through them with range, and when they're all done, sends back the total minutes spent:

prog.go
1package main2 3import "fmt"4 5func cook(orders <-chan int, done chan<- int) {6	minutes := 07	for m := range orders {8		fmt.Println("Cooking a dish:", m, "minutes")9		minutes += m10	}11	done <- minutes12}13 14func main() {15	orders := make(chan int, 10)16	done := make(chan int)17	go cook(orders, done)18	for _, m := range []int{5, 12, 8} {19		orders <- m20	}21	close(orders)22	fmt.Println("Total:", <-done, "minutes")23}
Output
Cooking a dish: 5 minutes
Cooking a dish: 12 minutes
Cooking a dish: 8 minutes
Total: 25 minutes

orders has 10 slots, so main drops off all three orders and closes it without waiting for the cook. The cook takes the three orders, sees the channel closed, leaves the range, and sends the total into done. cook's parameters are one-way: it only receives from orders and only sends on done.

Practice

Write the code, then press Check. A hint costs nothing; the answer is there when you need it.

Exercise 1

A total that goes through a channel

Finish total: make a buffered channel with exactly enough room for all the prices, put them all in, close it, then take them out with for range and add them up. The program shows the totals of two carts.

It should print
50
130
exercise1.go
1package main2 3import "fmt"4 5func total(prices []int) int {6	sum := 07	// make the channel, put the prices in, close it, then add them up8	return sum9}10 11func main() {12	fmt.Println(total([]int{12, 30, 8}))13	fmt.Println(total([]int{99, 1, 25, 5}))14}
Output
Press Run or Ctrl+Enter
Exercise 2

Fix it: the range that never ends

evens sends the even numbers from 2 up to limit, and sumEvens adds them up. But the program deadlocks. Fix it so it shows 30 and 12.

It should print
30
12
exercise2.go
1package main2 3import "fmt"4 5func evens(limit int, out chan int) {6	for n := 2; n <= limit; n += 2 {7		out <- n8	}9}10 11func sumEvens(limit int) int {12	ch := make(chan int)13	go evens(limit, ch)14	sum := 015	for n := range ch {16		sum += n17	}18	return sum19}20 21func main() {22	fmt.Println(sumEvens(10))23	fmt.Println(sumEvens(7))24}
Output
Press Run or Ctrl+Enter
Exercise 3

How long the cook was busy

Finish cook: use for range to receive each dish's minutes from orders, add them up, and when they're all in, send the total into done. The program works out two evenings of orders.

It should print
25
51
exercise3.go
1package main2 3import "fmt"4 5func cook(orders <-chan int, done chan<- int) {6	// receive all the orders, then send the total minutes into done7	done <- 08}9 10func busyMinutes(minutes []int) int {11	orders := make(chan int, len(minutes))12	done := make(chan int)13	go cook(orders, done)14	for _, m := range minutes {15		orders <- m16	}17	close(orders)18	return <-done19}20 21func main() {22	fmt.Println(busyMinutes([]int{5, 12, 8}))23	fmt.Println(busyMinutes([]int{20, 15, 10, 6}))24}
Output
Press Run or Ctrl+Enter

What you learned

  • make(chan T, n) makes a channel with n slots: sends don't wait while there's room, only when it's full; values come out in the order they went in.
  • len(ch) is how many values are waiting, cap(ch) how many slots there are; an unbuffered channel has cap 0.
  • close(ch): no more values. What's left can still be received; after that a receive gets the zero value.
  • v, ok := <-ch: ok is false when the channel is closed and empty.
  • for v := range ch receives until the channel is closed; forget the close and it waits forever, a deadlock.
  • Sending on a closed channel panics with send on closed channel; the sender closes.
  • Step shows a channel's slots filling up, and when a sender is "stuck sending".
Finished this lesson?Mark it. Progress is saved in this browser, and you can undo it.