Skip to content
Theory · Goroutines and channels · lesson 6 of 13

Buffers, close and range

In the previous lesson the serving hatch had no shelf: the cook held the plate until the waiter took it. That is why a goroutine cannot send a value into an unbuffered channel and then receive it itself. Now let's put a shelf for several plates at the hatch. The cook puts dishes on it and moves on while there is room.

A channel's buffer

That shelf is the channel's buffer, and its size is the second argument of make. A send into such a channel waits only when the buffer is full:

GO
package main

import "fmt"

func main() {
    ch := make(chan int, 2)
    ch <- 1
    ch <- 2
    fmt.Println(<-ch, <-ch) // 1 2
}

Let's walk through the example line by line:

  1. make(chan int, 2) gives a shelf for two values.
  2. ch <- 1 and ch <- 2 put 1 and 2 on it and do not wait: there is room.
  3. fmt.Println(<-ch, <-ch)—main itself takes them in the same order and prints 1 2.

A buffer changes the waiting rules:

  • a send waits only when the buffer is full;
  • a receive takes the oldest value and waits only when the buffer is empty.

A third send into this channel before a receive would stop the program with a deadlock: the shelf is full, and there is no receiver.

With a buffer or without

The two kinds of channels make different promises.

A buffered channel

A buffer smooths out a difference in speed: the sender puts in several values and goes on without waiting for the receiver.

An unbuffered channel

An unbuffered channel gives a different promise: when a send has finished, the value has already been received.

Closing a channel

How does the waiter learn that no more dishes will come? The cook hangs an "Orders finished" sign on the hatch. In Go, that is the call close(ch): it tells receivers that no more values will come. After the close, receiving works like this:

  • values already in the buffer can still be received;
  • when a closed channel is empty, a receive does not wait and returns the zero value right away.

A second variable ok tells it apart from a zero that was sent, as when reading from a map:

GO
ch := make(chan int, 1)
ch <- 5
close(ch)
v, ok := <-ch
fmt.Println(v, ok) // 5 true
v, ok = <-ch
fmt.Println(v, ok) // 0 false

Let's go through both receives:

  1. The first receive takes the 5 that was on the shelf, and ok is true.
  2. The second receive finds an empty shelf and the sign: it returns 0 and false without waiting.

The sender closes the channel when it has sent everything. A channel needs closing only when the receiver must learn that no more values will come.

Panics on a closed channel

This is where the sign picture stops working. In a kitchen you can hang the sign and still put out one more plate. In Go, a closed channel does not forgive that:

  • a send into a closed channel panics with send on closed channel;
  • a second close panics with close of closed channel.

Interviewers like to ask this. The answer: a send into a closed channel is a panic, while a receive from a closed, empty channel gives the zero value at once with ok == false.

range over a channel

The loop for v := range ch receives values one by one and ends when the channel is closed and empty. This is how the receiver learns that the sender has finished:

GO
ch := make(chan int)
go func() {
    for i := 1; i <= 3; i++ {
        ch <- i * 10
    }
    close(ch)
}()
sum := 0
for v := range ch {
    sum += v
}
fmt.Println(sum) // 60

Let's see what happens here:

  1. The goroutine sends 10, 20 and 30.
  2. Then it hangs the sign: close(ch).
  3. The loop in main receives three values and adds them up.
  4. range does not receive a zero value after the close: the loop ends right after the last value sent.

So the sum is exactly 60.

Common mistake

A channel was read with a range loop, but nobody closed it:

GO
ch := make(chan int, 2)
ch <- 1
ch <- 2
for v := range ch { // ✗
    fmt.Println(v)
}
TEXT
fatal error: all goroutines are asleep - deadlock!

Here is what happens:

  1. The loop prints 1 and 2 and then waits for the next value.
  2. There is no sign, and nobody is left to send a value.
  3. The runtime stops the program.

The fix is close(ch) after the last send.

Key points

  • A buffer holds values: a send waits only when it is full, and a receive only when it is empty.
  • close(ch) closes a channel; buffered values can still be read, and then a receive gives the zero value at once with ok == false.
  • A send into a closed channel and a second close both panic.
  • for v := range ch ends when the channel is closed and empty; without close, the loop waits forever after the last value.

Subscribe to take the course

The course is free. Sign in with Telegram and subscribe to the channel to open every lesson.

Sign in another way