сеньорчикОткрыть в Telegram
← вся теориятеория к собесу · Горутины и каналы

Каналы в 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, остальные разбираются в тренажёре.

  1. #go_channels1 / 5
    Канал закрыли через close(ch). Что вернёт v, ok := <-ch, когда буфер пуст?
    A)Панику при попытке приёма из закрытого канала
    B)Блокировку навсегда — закрытый канал больше ничего не отдаёт
    C)Нулевое значение типа и ok=false
    D)Последнее отправленное значение и ok=true
    показать ответ и разбор
    +C)Нулевое значение типа и ok=false

    // разбор: Приём из закрытого канала не блокирует и не паникует: пока в буфере есть значения — отдаёт их с ok=true, а когда пусто — сразу возвращает нулевое значение типа и ok=false. Этот сигнал «канал закрыт» и лежит в основе for v := range ch (цикл выходит на закрытии). Паникует только ОТПРАВКА в закрытый канал, не приём.

  2. #go_channels2 / 5
    Кто по идиоме Go должен закрывать канал?
    A)Получатель — он знает, что больше не хочет читать
    B)Отправитель, и ровно один — тот, кто в канал пишет
    C)close идемпотентен — повторные вызовы просто игнорируются
    D)Никто вручную — рантайм закрывает канал по выходу из области видимости
    показать ответ и разбор
    +B)Отправитель, и ровно один — тот, кто в канал пишет

    // разбор: Закрывает отправитель, а если отправителей несколько — их координируют так, чтобы close позвал ровно один. Закрытие — сигнал «данных больше не будет», и слать его логично со стороны, которая закончила писать. Получателю закрывать нельзя: отправитель, записав в уже закрытый канал, словит панику. Повторный close — тоже паника.

  3. #go_channels3 / 5
    make(chan int, 2), два значения уже отправлены без чтения. Что делает третья отправка?
    A)Блокирует отправителя, пока кто-то не прочитает и не освободит место
    B)Проходит успешно: внутренний буфер канала растёт динамически по мере надобности
    C)Паникует из-за переполнения буфера канала
    D)Перезаписывает самое старое значение — буфер работает как кольцо
    показать ответ и разбор
    +A)Блокирует отправителя, пока кто-то не прочитает и не освободит место

    // разбор: Буфер канала фиксирован в make и не растёт. Пока есть свободное место, отправка не блокирует; как только len == cap, следующая отправка блокирует горутину до тех пор, пока приёмник не заберёт значение. Ни паники, ни перезаписи, ни роста — только backpressure. Именно это делает каналы механизмом регулирования нагрузки.

  4. #go_channels4 / 5
    Что вернут len(ch) и cap(ch) для буферизованного канала?
    A)Число отправленных за всё время значений и размер буфера
    B)Число ждущих в буфере значений и размер буфера
    C)Число горутин-читателей и число горутин-писателей
    D)Оба вернут размер буфера — len для канала не определён
    показать ответ и разбор
    +B)Число ждущих в буфере значений и размер буфера

    // разбор: len — сколько значений прямо сейчас лежит в буфере, cap — его вместимость: у make(chan int, 3) с двумя непрочитанными значениями получится 2 и 3. У небуферизованного канала оба равны нулю. Опираться на len в логике опасно: между проверкой и отправкой другая горутина успевает изменить состояние, и решение принимается по устаревшему числу.

  5. #go_channels5 / 5
    Зачем в сигнатуре функции указывать направление канала — chan<- int вместо chan int?
    A)Направленный канал работает быстрее за счёт упрощённой синхронизации
    B)Так канал передаётся в другую горутину без копирования
    C)Компилятор запретит функции неположенную операцию с каналом
    D)Направление задаёт размер буфера при передаче канала
    показать ответ и разбор
    +C)Компилятор запретит функции неположенную операцию с каналом

    // разбор: Направление — контракт, проверяемый на компиляции: продюсеру передают chan<- (только отправка), консьюмеру <-chan (только чтение). Попытка прочитать из канала-только-на-запись или закрыть его в потребителе не соберётся. Это дёшево ловит типичную ошибку с закрытием канала не той стороной, которую иначе видно только в проде по панике.

дальше

Теорию прочитали. Навык ставится повторением

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