Каналы в Go
Канал часто объясняют как очередь, и это половина правды. Вторая половина важнее: канал передаёт владение данными. Отправил структуру в канал - забудь про неё, теперь она принадлежит получателю. Именно поэтому в Go говорят «не общайтесь через общую память, делите память общением».
Стержень: канал - это одновременно передача данных и точка синхронизации; буфер меняет только момент блокировки.
// Формулировки: «чем буферизованный канал отличается от небуферизованного?», «кто должен закрывать канал?», «что вернёт приём из закрытого?»
С буфером и без
Небуферизованный канал - это рандеву. Отправитель блокируется, пока получатель не заберёт, получатель блокируется, пока кто-то не отправит. Момент передачи - точка синхронизации: после неё обе стороны точно знают, что вторая дошла до этого места.
Буферизованный канал разрывает эту связь. Отправка блокируется, только когда буфер полон, приём - только когда буфер пуст. Отправитель может убежать вперёд на размер буфера, и никакой синхронизации на каждой передаче уже нет.
Отсюда практический выбор. Небуферизованный берут, когда нужна именно синхронизация или когда важно знать, что сообщение принято. Буфер - когда надо сгладить неравномерность: производитель выдаёт всплесками, потребитель работает ровно.
// Размер буфера - не «чем больше, тем лучше». Большой буфер прячет проблему: система работает, очередь растёт, задержка растёт, и всё это невидимо, пока память не кончится. Буфер на единицы или десятки сглаживает всплеск, буфер на миллион маскирует то, что потребитель просто не справляется.
- небуферизованный канал
- передача происходит только при встрече отправителя и получателя
Закрытие: правила без исключений
Закрывает канал отправитель, и только он. Причина простая: получатель не знает, будет ли ещё отправка, а отправитель знает. Если он закроет канал, а потом кто-то отправит - будет паника, поэтому закрытие принадлежит той стороне, которая контролирует конец потока.
Приём из закрытого канала не блокируется и не паникует - он возвращает нулевое значение типа. Проверено на канале с одним элементом внутри: первый приём даёт 1 и ok равно true, второй и третий - 0 и false. Форма v, ok := <-ch и нужна для того, чтобы отличить настоящий ноль от «канал закрыт».
range по каналу читает, пока канал не закроют, и корректно завершается на закрытии. Если отправитель забыл закрыть, range будет ждать вечно - и это одна из самых частых утечек горутин.
// Когда отправителей несколько, закрывать канал напрямую нельзя ни одному из них: любой рискует закрыть его раньше соседа. Стандартный приём - отдельная горутина ждёт WaitGroup всех отправителей и закрывает канал после того, как все завершились.
ch := make(chan int, 2)
ch <- 1
close(ch)
v, ok := <-ch // 1, true - данные из буфера
v, ok = <-ch // 0, false - канал закрыт и пуст
v, ok = <-ch // 0, false - и так сколько угодно разКаналы как элемент дизайна
Направление указывают в сигнатуре: chan<- int только для отправки, <-chan int только для приёма. Компилятор проследит, и читателю сразу видно, кто здесь производитель, а кто потребитель. Функция, возвращающая <-chan Result, честно говорит: закрывать буду я, ты только читай.
Канал пустых структур chan struct{} - идиома сигнала. Пустая структура занимает ноль байт, данных не передаётся, передаётся сам факт события: «начинай», «стоп», «готово». Закрытие такого канала работает как широковещательное оповещение - все, кто читает, разблокируются разом.
Каналы хороши для передачи владения и для конвейеров. Для защиты разделяемого состояния они обычно хуже мьютекса: канал вокруг счётчика - это медленнее, сложнее и труднее читается.
// Полезное правило выбора, которое любят на собеседовании: передаёшь данные между задачами - канал, защищаешь состояние от одновременного доступа - мьютекс. Каналы не заменяют sync, они решают другую задачу.
- chan struct{}
- канал-сигнал: данных нет, передаётся факт события; закрытие оповещает всех разом
Как отвечать: «Кто должен закрывать канал и что вернёт приём из закрытого?»
Закрывает отправитель, и только он: получатель не знает, будет ли ещё отправка, а отправитель знает. Плюс отправка в закрытый канал паникует, так что право закрытия обязано лежать у того, кто контролирует конец потока. Приём из закрытого канала при этом полностью безопасен: он не блокируется и не паникует, а сразу возвращает нулевое значение типа. Чтобы отличить настоящий ноль от закрытия, используют форму с двумя результатами: v, ok := <-ch, где ok равно false после закрытия. Я это проверял: если в буфере лежал элемент, первый приём отдаёт его с ok true, а все следующие - ноль и false. range по каналу на закрытии корректно завершается, и это основной способ читать поток. Отдельный случай - несколько отправителей: тогда закрывать не может ни один из них, потому что каждый рискует опередить остальных. Стандартный приём - отдельная горутина ждёт WaitGroup всех отправителей и закрывает канал после них.
Названо правило с причиной, точно описано поведение приёма с проверенными значениями, упомянут range и отдельно разобран случай нескольких отправителей - тот самый следующий вопрос.
На чём валят
- −Закрывают канал на стороне получателя - отправитель следом паникует.
- −Забывают закрыть канал, и range на другой стороне висит вечно.
- −Не различают ноль-значение и закрытие: без формы v, ok это одно и то же.
- −Дают нескольким отправителям закрывать один канал вместо горутины-закрывашки.
- −Ставят огромный буфер и прячут за ним то, что потребитель не справляется.
- −Городят каналы вокруг общего счётчика там, где нужен обычный мьютекс.
Проверьте себя
Пять вопросов из банка по этой подтеме. Всего их 11, остальные разбираются в тренажёре.
- Канал закрыли через close(ch). Что вернёт v, ok := <-ch, когда буфер пуст?A)Панику при попытке приёма из закрытого каналаB)Блокировку навсегда — закрытый канал больше ничего не отдаётC)Нулевое значение типа и ok=falseD)Последнее отправленное значение и ok=true
показать ответ и разбор
+C)Нулевое значение типа и ok=false// разбор: Приём из закрытого канала не блокирует и не паникует: пока в буфере есть значения — отдаёт их с ok=true, а когда пусто — сразу возвращает нулевое значение типа и ok=false. Этот сигнал «канал закрыт» и лежит в основе for v := range ch (цикл выходит на закрытии). Паникует только ОТПРАВКА в закрытый канал, не приём.
- Кто по идиоме Go должен закрывать канал?A)Получатель — он знает, что больше не хочет читатьB)Отправитель, и ровно один — тот, кто в канал пишетC)close идемпотентен — повторные вызовы просто игнорируютсяD)Никто вручную — рантайм закрывает канал по выходу из области видимости
показать ответ и разбор
+B)Отправитель, и ровно один — тот, кто в канал пишет// разбор: Закрывает отправитель, а если отправителей несколько — их координируют так, чтобы close позвал ровно один. Закрытие — сигнал «данных больше не будет», и слать его логично со стороны, которая закончила писать. Получателю закрывать нельзя: отправитель, записав в уже закрытый канал, словит панику. Повторный close — тоже паника.
- make(chan int, 2), два значения уже отправлены без чтения. Что делает третья отправка?A)Блокирует отправителя, пока кто-то не прочитает и не освободит местоB)Проходит успешно: внутренний буфер канала растёт динамически по мере надобностиC)Паникует из-за переполнения буфера каналаD)Перезаписывает самое старое значение — буфер работает как кольцо
показать ответ и разбор
+A)Блокирует отправителя, пока кто-то не прочитает и не освободит место// разбор: Буфер канала фиксирован в make и не растёт. Пока есть свободное место, отправка не блокирует; как только len == cap, следующая отправка блокирует горутину до тех пор, пока приёмник не заберёт значение. Ни паники, ни перезаписи, ни роста — только backpressure. Именно это делает каналы механизмом регулирования нагрузки.
- Что вернут len(ch) и cap(ch) для буферизованного канала?A)Число отправленных за всё время значений и размер буфераB)Число ждущих в буфере значений и размер буфераC)Число горутин-читателей и число горутин-писателейD)Оба вернут размер буфера — len для канала не определён
показать ответ и разбор
+B)Число ждущих в буфере значений и размер буфера// разбор: len — сколько значений прямо сейчас лежит в буфере, cap — его вместимость: у make(chan int, 3) с двумя непрочитанными значениями получится 2 и 3. У небуферизованного канала оба равны нулю. Опираться на len в логике опасно: между проверкой и отправкой другая горутина успевает изменить состояние, и решение принимается по устаревшему числу.
- Зачем в сигнатуре функции указывать направление канала — chan<- int вместо chan int?A)Направленный канал работает быстрее за счёт упрощённой синхронизацииB)Так канал передаётся в другую горутину без копированияC)Компилятор запретит функции неположенную операцию с каналомD)Направление задаёт размер буфера при передаче канала
показать ответ и разбор
+C)Компилятор запретит функции неположенную операцию с каналом// разбор: Направление — контракт, проверяемый на компиляции: продюсеру передают chan<- (только отправка), консьюмеру <-chan (только чтение). Попытка прочитать из канала-только-на-запись или закрыть его в потребителе не соберётся. Это дёшево ловит типичную ошибку с закрытием канала не той стороной, которую иначе видно только в проде по панике.
дальше
Теорию прочитали. Навык ставится повторением
В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.