Буфер, close и range
В прошлом уроке у окна раздачи не было полки: повар держал тарелку, пока официант её не заберёт. Поэтому горутина не может отправить значение в небуферизованный канал, а потом сама его принять. Теперь поставим у окна полку на несколько тарелок. Повар кладёт на неё блюда и идёт дальше, пока на полке есть место.
Буфер канала
Такая полка — буфер канала, и его размер — второй аргумент make. Отправка в такой канал ждёт, только когда буфер полон:
package main
import "fmt"
func main() {
ch := make(chan int, 2)
ch <- 1
ch <- 2
fmt.Println(<-ch, <-ch) // 1 2
}Давайте разберём пример по строкам:
make(chan int, 2)даёт полку на два значения.ch <- 1иch <- 2кладут на неё1и2и не ждут: место есть.fmt.Println(<-ch, <-ch)—mainсама забирает их в том же порядке и печатает1 2.
Буфер меняет правила ожидания:
- отправка ждёт, только когда буфер полон;
- приём берёт самое старое значение и ждёт, только когда буфер пуст.
Третья отправка в этот канал до приёма остановила бы программу с взаимной блокировкой: полка полна, а получателя нет.
С буфером или без
Два вида каналов дают разные обещания.
Буферизованный канал
Буфер сглаживает разницу в скорости: отправитель кладёт несколько значений и продолжает работу, не дожидаясь получателя.
Небуферизованный канал
Небуферизованный канал даёт другое обещание: когда отправка закончилась, получатель уже принял значение.
Закрытие канала
Как официанту понять, что блюд больше не будет? Повар вешает на окно табличку «Заказы кончились». В Go это вызов close(ch): он сообщает получателям, что значений больше не будет. После закрытия приём работает так:
- значения, которые уже лежат в буфере, по-прежнему можно принять;
- когда закрытый канал пуст, приём не ждёт, а сразу возвращает нулевое значение.
Вторая переменная ok отличает это нулевое значение от отправленного нуля, как при чтении из map:
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Разберём оба приёма:
- Первый приём забирает
5, которая лежала на полке, иokравноtrue. - Второй приём находит пустую полку и табличку: он возвращает
0иfalse, ничего не дожидаясь.
Канал закрывает отправитель, когда отправил всё. Закрывать канал нужно, только когда получатель должен узнать, что значений больше не будет.
Паника на закрытом канале
Здесь аналогия с табличкой заканчивается. На кухне можно повесить табличку и всё-таки поставить ещё одну тарелку. В Go закрытый канал так не прощает:
- отправка в закрытый канал вызывает панику
send on closed channel; - повторный
closeвызывает паникуclose of closed channel.
Этот вопрос часто задают на собеседовании. Ответ: отправка в закрытый канал — паника, а приём из закрытого и пустого — сразу нулевое значение и ok == false.
range по каналу
Цикл for v := range ch принимает значения одно за другим и заканчивается, когда канал закрыт и пуст. Так получатель узнаёт, что отправитель закончил работу:
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Давайте разберём, что здесь происходит:
- Горутина отправляет
10,20и30. - Потом она вешает табличку:
close(ch). - Цикл в
mainпринимает три значения и складывает их. - Нулевое значение после закрытия
rangeне принимает: цикл заканчивается сразу после последнего отправленного значения.
Поэтому сумма — ровно 60.
Частая ошибка
По каналу прошли циклом range, а закрыть его забыли:
ch := make(chan int, 2)
ch <- 1
ch <- 2
for v := range ch { // ✗
fmt.Println(v)
}fatal error: all goroutines are asleep - deadlock!Цикл так и не узнаёт, что значений больше не будет:
- Цикл печатает
1и2, а потом ждёт следующее значение. - Таблички нет, а отправить значение больше некому.
- Среда выполнения останавливает программу.
Исправление — close(ch) после последней отправки.
Главное
- Буфер хранит значения: отправка ждёт, только когда он полон, а приём — когда он пуст.
close(ch)закрывает канал; значения из буфера можно дочитать, а потом приём сразу даёт нулевое значение иok == false.- Отправка в закрытый канал и повторный
closeвызывают панику. for v := range chзаканчивается, когда канал закрыт и пуст; безcloseцикл после последнего значения ждёт вечно.
Подпишись, чтобы пройти курс
Курс бесплатный. Войди через Telegram и подпишись на канал — откроются все уроки.