Channels
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.
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}
photo.jpg downloaded
| Code | Meaning |
|---|---|
make(chan string) | Make a channel that carries strings. |
ch <- "…" | Send: put a value into the channel. The arrow points into it. |
msg := <-ch | Receive: 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:
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}
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:
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}
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:
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}
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:
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}
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:
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}
Each goroutine sends one value, and main receives twice and adds them. Which arrives first isn't fixed, but 10 + 20 and 20 + 10 are both 30.
To show each result in the original order, give each worker a channel of its own and receive from them in order:
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}
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?
1package main2 3import "fmt"4 5func main() {6 ch := make(chan int)7 ch <- 428 fmt.Println(<-ch)9}
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan send]:
main.main(...)
/tmp/sandbox/prog.go:7 +0x1d
exit status 2What 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:
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}
# 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.
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.
Order A total 70 Order B total 155
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}
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.
30 77
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}
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.
photo.jpg 2048 KB song.mp3 5120 KB notes.txt 10 KB
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}
What you learned
ch := make(chan int)makes a channel;ch <- vsends,v := <-chreceives.- 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 <- trueand<-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<- intonly sends,<-chan intonly receives. - Step shows lanes "stuck sending" and "stuck receiving", with a dashed arrow to the channel each is waiting on.