Kodewick
Start learning →
Lesson 19 of 24Contents

Channels

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

In the last lesson, a goroutine could only leave its result in a variable, and main had to guess with time.Sleep when it was done. A channel is the better way: it's like a pipe. One goroutine puts a value in at one end, and another takes it out at the other.

prog.go
1package main2 3import "fmt"4 5func main() {6	ch := make(chan string)7	go func() {8		ch <- "photo.jpg downloaded"9	}()10	msg := <-ch11	fmt.Println(msg)12}
Output
photo.jpg downloaded
CodeMeaning
make(chan string)Make a channel that carries strings.
ch <- "…"Send: put a value into the channel. The arrow points into it.
msg := <-chReceive: take a value out of the channel. The arrow points out of it.

A channel's type is chan plus the element type: chan int, chan string, chan bool. Like a map, it has to be made with make before you use it. A channel you never made is nil, and sending to it or receiving from it waits forever (it doesn't panic).

Receiving waits

If there's no value yet when you do <-ch, the receiver stops right there and waits until someone sends. Here total adds up an order in another goroutine while main waits for it:

prog.go
1package main2 3import "fmt"4 5func total(prices []int, out chan int) {6	sum := 07	for _, p := range prices {8		sum += p9	}10	out <- sum11}12 13func main() {14	out := make(chan int)15	go total([]int{12, 30, 8}, out)16	fmt.Println("Waiting for the total...")17	fmt.Println("Total", <-out)18}
Output
Waiting for the total...
Total 50

Press Step: at <-out the main lane turns to "stuck receiving", with a dashed arrow pointing at out to show which channel it's waiting on. Only now does total get its turn; it adds up the order and sends the sum, and main gets the value and carries on. No guessing: the moment the result arrives, main moves, not a moment sooner or later.

Sending waits too

It works the other way as well. A channel made with make(chan T) has no room to hold a value, so a sender waits until someone comes to receive. Only when the two meet does the value change hands. The kitchen finishes a dish and has to wait for a waiter to pick it up:

prog.go
1package main2 3import (4	"fmt"5	"time"6)7 8func main() {9	dishes := make(chan string)10	go func() {11		fmt.Println("Kitchen: the omelet is ready")12		dishes <- "omelet"13	}()14	time.Sleep(time.Second)15	fmt.Println("Waiter: coming for it")16	dish := <-dishes17	fmt.Println("Waiter: took the", dish)18}
Output
Kitchen: the omelet is ready
Waiter: coming for it
Waiter: took the omelet

Press Step and watch the kitchen's lane: after its first line it reaches dishes <- "omelet" and turns to "stuck sending". It waits there until the waiter (main) wakes up and reaches <-dishes. (Sleep on this page doesn't really wait.)

Waiting for a goroutine with a channel

Often there's no result to pass; you just want to know the work is done. Then let the goroutine send a value on a channel when it finishes, and when main receives it, it knows:

prog.go
1package main2 3import "fmt"4 5func download(file string, done chan bool) {6	fmt.Println("Downloaded", file)7	done <- true8}9 10func main() {11	done := make(chan bool)12	go download("photo.jpg", done)13	<-done14	fmt.Println("main is done")15}
Output
Downloaded photo.jpg
main is done

<-done on its own line throws the value away; it only waits. Compare it with last lesson's time.Sleep: however long download takes, main waits exactly that long. And Downloaded always comes before main is done: whatever a goroutine did before sending is finished by the time the receiver gets the value.

Collecting several results

Several workers square numbers and all send to the same channel. main receives once per number and adds them up:

prog.go
1package main2 3import "fmt"4 5func main() {6	nums := []int{3, 5, 8, 10}7	results := make(chan int)8	for _, n := range nums {9		go func() {10			results <- n * n11		}()12	}13	sum := 014	for range len(nums) {15		sum += <-results16	}17	fmt.Println("Sum of squares", sum)18}
Output
Sum of squares 198

On a real computer the four results may arrive in a different order each run, but the sum is always the same. Guess:

Guess first

What does this show?

1package main2 3import "fmt"4 5func main() {6	ch := make(chan int)7	go func() {8		ch <- 109	}()10	go func() {11		ch <- 2012	}()13	fmt.Println(<-ch + <-ch)14}

To show each result in the original order, give each worker a channel of its own and receive from them in order:

prog.go
1package main2 3import "fmt"4 5func main() {6	nums := []int{3, 5, 8}7	chans := make([]chan int, len(nums))8	for i, n := range nums {9		chans[i] = make(chan int)10		go func() {11			chans[i] <- n * n12		}()13	}14	for i, ch := range chans {15		fmt.Println(nums[i], "squared is", <-ch)16	}17}
Output
3 squared is 9
5 squared is 25
8 squared is 64

Press Step and see all three workers stuck sending, each arrow pointing at its own channel. main receives from chans[0] first, then chans[1], and so on. Whoever finishes first, the lines come out in the order of nums.

Deadlock

A send waits for a receive, and a receive waits for a send. What if nobody ever comes?

prog.go
1package main2 3import "fmt"4 5func main() {6	ch := make(chan int)7	ch <- 428	fmt.Println(<-ch)9}
Output
fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan send]:
main.main(...)
	/tmp/sandbox/prog.go:7 +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.

main stops at line 7 to send and waits for a receiver. But the only one who could receive is main itself, on the next line, and there are no other goroutines. Go sees that every goroutine is waiting and none can ever move again, and reports fatal error: all goroutines are asleep - deadlock!. The goroutine 1 [chan send]: below says main is stuck sending. Press Step up to that line and the lane turns red: "all stuck".

Too few workers does the same: if three workers send three values and main receives four times, the fourth receive waits forever. Lesson 23 shows more kinds of deadlock.

Directional channels

When you pass a channel to a function, you can say whether the function only sends or only receives: chan<- int can only send, <-chan int can only receive. Get it backward and the compiler stops you:

prog.go
1package main2 3import "fmt"4 5func square(nums []int, out chan<- int) {6	for _, n := range nums {7		out <- n * n8	}9	fmt.Println(<-out)10}11 12func main() {13	out := make(chan int)14	go square([]int{2, 3}, out)15	fmt.Println(<-out, <-out)16}
Output
# command-line-arguments
./prog.go:9:16: invalid operation: cannot receive from send-only channel out (variable of type chan<- int)

cannot receive from send-only channel out: inside square, out can only send. Delete line 9 and it works. A plain chan int passed to a chan<- int parameter becomes one-way automatically.

Practice

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

Exercise 1

Two orders at once

total is done: it sends the sum into out. In main, make two channels, start two goroutines to add up the two orders, then receive the results and show them: Order A total 70 and Order B total 155.

It should print
Order A total 70
Order B total 155
exercise1.go
1package main2 3import "fmt"4 5func total(prices []int, out chan int) {6	sum := 07	for _, p := range prices {8		sum += p9	}10	out <- sum11}12 13func main() {14	orderA := []int{25, 40, 5}15	orderB := []int{120, 35}16	// start two goroutines, then receive the totals17	fmt.Println("Order A total", 0)18	fmt.Println("Order B total", 0)19}
Output
Press Run or Ctrl+Enter
Exercise 2

Fix it: one receive too many

sumOfSquares starts a worker per number to square it, then adds up the results. The first list works, but the second one deadlocks. Fix it so it shows 30 and 77.

It should print
30
77
exercise2.go
1package main2 3import "fmt"4 5func sumOfSquares(nums []int) int {6	results := make(chan int)7	for _, n := range nums {8		go func() {9			results <- n * n10		}()11	}12	sum := 013	for range 4 {14		sum += <-results15	}16	return sum17}18 19func main() {20	fmt.Println(sumOfSquares([]int{1, 2, 3, 4}))21	fmt.Println(sumOfSquares([]int{4, 5, 6}))22}
Output
Press Run or Ctrl+Enter
Exercise 3

File sizes in order

Start a goroutine per file that turns its size in bytes into KB (divide by 1024) and sends it on the file's own channel. main receives in file order and shows one line per file, like photo.jpg 2048 KB.

It should print
photo.jpg 2048 KB
song.mp3 5120 KB
notes.txt 10 KB
exercise3.go
1package main2 3import "fmt"4 5func main() {6	files := []string{"photo.jpg", "song.mp3", "notes.txt"}7	sizes := []int{2097152, 5242880, 10240}8	chans := make([]chan int, len(files))9	// make each file's channel and start its goroutine10	for i := range chans {11		fmt.Println(files[i], sizes[i], "KB")12	}13}
Output
Press Run or Ctrl+Enter

What you learned

  • ch := make(chan int) makes a channel; ch <- v sends, v := <-ch receives.
  • An unbuffered channel: a receive waits until someone sends, a send waits until someone receives, and the value changes hands when they meet.
  • Wait for a goroutine with a channel: done <- true and <-done, no guessing.
  • Several workers sending on one channel: receive once per worker; for a fixed order, give each worker its own channel and receive in order.
  • When nobody can ever move: fatal error: all goroutines are asleep - deadlock!.
  • Directional channels: chan<- int only sends, <-chan int only receives.
  • Step shows lanes "stuck sending" and "stuck receiving", with a dashed arrow to the channel each is waiting on.
Finished this lesson?Mark it. Progress is saved in this browser, and you can undo it.