Перейти к содержимому
Теория · Горутины и каналы · урок 6 из 13

Буфер, close и range

В прошлом уроке у окна раздачи не было полки: повар держал тарелку, пока официант её не заберёт. Поэтому горутина не может отправить значение в небуферизованный канал, а потом сама его принять. Теперь поставим у окна полку на несколько тарелок. Повар кладёт на неё блюда и идёт дальше, пока на полке есть место.

Буфер канала

Такая полка — буфер канала, и его размер — второй аргумент make. Отправка в такой канал ждёт, только когда буфер полон:

GO
package main

import "fmt"

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

Давайте разберём пример по строкам:

  1. make(chan int, 2) даёт полку на два значения.
  2. ch <- 1 и ch <- 2 кладут на неё 1 и 2 и не ждут: место есть.
  3. fmt.Println(<-ch, <-ch) — main сама забирает их в том же порядке и печатает 1 2.

Буфер меняет правила ожидания:

  • отправка ждёт, только когда буфер полон;
  • приём берёт самое старое значение и ждёт, только когда буфер пуст.

Третья отправка в этот канал до приёма остановила бы программу с взаимной блокировкой: полка полна, а получателя нет.

С буфером или без

Два вида каналов дают разные обещания.

Буферизованный канал

Буфер сглаживает разницу в скорости: отправитель кладёт несколько значений и продолжает работу, не дожидаясь получателя.

Небуферизованный канал

Небуферизованный канал даёт другое обещание: когда отправка закончилась, получатель уже принял значение.

Закрытие канала

Как официанту понять, что блюд больше не будет? Повар вешает на окно табличку «Заказы кончились». В Go это вызов close(ch): он сообщает получателям, что значений больше не будет. После закрытия приём работает так:

  • значения, которые уже лежат в буфере, по-прежнему можно принять;
  • когда закрытый канал пуст, приём не ждёт, а сразу возвращает нулевое значение.

Вторая переменная ok отличает это нулевое значение от отправленного нуля, как при чтении из 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

Разберём оба приёма:

  1. Первый приём забирает 5, которая лежала на полке, и ok равно true.
  2. Второй приём находит пустую полку и табличку: он возвращает 0 и false, ничего не дожидаясь.

Канал закрывает отправитель, когда отправил всё. Закрывать канал нужно, только когда получатель должен узнать, что значений больше не будет.

Паника на закрытом канале

Здесь аналогия с табличкой заканчивается. На кухне можно повесить табличку и всё-таки поставить ещё одну тарелку. В Go закрытый канал так не прощает:

  • отправка в закрытый канал вызывает панику send on closed channel;
  • повторный close вызывает панику close of closed channel.

Этот вопрос часто задают на собеседовании. Ответ: отправка в закрытый канал — паника, а приём из закрытого и пустого — сразу нулевое значение и ok == false.

range по каналу

Цикл for v := range ch принимает значения одно за другим и заканчивается, когда канал закрыт и пуст. Так получатель узнаёт, что отправитель закончил работу:

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

Давайте разберём, что здесь происходит:

  1. Горутина отправляет 10, 20 и 30.
  2. Потом она вешает табличку: close(ch).
  3. Цикл в main принимает три значения и складывает их.
  4. Нулевое значение после закрытия range не принимает: цикл заканчивается сразу после последнего отправленного значения.

Поэтому сумма — ровно 60.

Частая ошибка

По каналу прошли циклом range, а закрыть его забыли:

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!

Цикл так и не узнаёт, что значений больше не будет:

  1. Цикл печатает 1 и 2, а потом ждёт следующее значение.
  2. Таблички нет, а отправить значение больше некому.
  3. Среда выполнения останавливает программу.

Исправление — close(ch) после последней отправки.

Главное

  • Буфер хранит значения: отправка ждёт, только когда он полон, а приём — когда он пуст.
  • close(ch) закрывает канал; значения из буфера можно дочитать, а потом приём сразу даёт нулевое значение и ok == false.
  • Отправка в закрытый канал и повторный close вызывают панику.
  • for v := range ch заканчивается, когда канал закрыт и пуст; без close цикл после последнего значения ждёт вечно.

Подпишись, чтобы пройти курс

Курс бесплатный. Войди через Telegram и подпишись на канал — откроются все уроки.

Войти другим способом