Урок по треку Go 8 мин

Каналы и select

Буферизованные и небуферизованные каналы, закрытие, nil-канал, select, таймауты, дедлоки.

Канал это типизированная труба между горутинами. «Не общайтесь через общую память, делитесь памятью через общение».

Создание и виды

ch := make(chan int)      // небуферизованный
bch := make(chan int, 3)  // буфер на 3 элемента
var nilCh chan int        // nil-канал
  • Небуферизованный: отправка ждёт получателя, получение ждёт отправителя. Это точка синхронизации (рандеву).
  • Буферизованный: отправка блокируется, только когда буфер полон, получение, когда пуст.
  • Направленные типы: chan<- int только отправка, <-chan int только получение. Удобно в сигнатурах.

Закрытие

close(ch) говорит «данных больше не будет». После закрытия:

  • получение сначала отдаёт оставшееся в буфере, затем сразу возвращает нулевое значение;
  • v, ok := <-ch даёт ok == false, когда канал закрыт и пуст;
  • for v := range ch завершается после закрытия и опустошения;
  • отправка в закрытый канал вызывает panic;
  • повторное закрытие вызывает panic, закрытие nil-канала тоже.

Закрывает отправитель, а не получатель. Если отправителей много, закрывают после того, как все закончили (WaitGroup плюс отдельная горутина).

Таблица поведения

Операцияnil-каналоткрытыйзакрытый
отправкаблок навсегдаблок или успехpanic
получениеблок навсегдаблок или значениенулевое значение, ok=false
closepanicуспехpanic

select

select ждёт первую готовую операцию из нескольких:

select {
case v := <-in:
    fmt.Println("получили", v)
case out <- x:
    fmt.Println("отправили")
case <-time.After(time.Second):
    fmt.Println("таймаут")
case <-ctx.Done():
    return ctx.Err()
}
  • Если готово несколько веток, выбор случайный и равномерный. Приоритета по порядку нет.
  • default делает select неблокирующим.
  • select {} блокирует навсегда.
  • Ветку можно «выключить», присвоив каналу nil: операция с nil-каналом никогда не готова. Так сливают два канала, пока оба не закроются.

Дедлоки

Если все горутины спят на каналах, рантайм падает с fatal error: all goroutines are asleep - deadlock!. Классика:

ch := make(chan int)
ch <- 1 // некому читать: deadlock

Если хоть одна горутина жива (например, ждёт таймер или сеть), рантайм дедлок не заметит, это будет просто зависание.

Каналы для сигналов

chan struct{} для сигналов без данных: закрытие такого канала будит всех получателей сразу. Так устроены ctx.Done() и «закрой, чтобы остановить всех воркеров».

Таймеры

time.After в цикле создаёт новый таймер на каждой итерации. С Go 1.23 неостановленные таймеры собираются GC, и Reset/Stop стали безопаснее (канал таймера стал синхронным), но для долгих циклов всё равно удобнее один time.NewTimer или time.Ticker.

Частые ошибки

  • Закрывать канал со стороны получателя или дважды.
  • Забыть закрыть канал, по которому идёт range: горутина зависнет.
  • Считать, что select выбирает первую по порядку ветку.
  • Отправлять в небуферизованный канал без читателя в той же горутине.

Что спросят на собесе

  • Чем буферизованный канал отличается от небуферизованного.
  • Что будет при чтении, записи и закрытии nil и закрытого канала.
  • Как работает select, если готово несколько веток.
  • Кто и когда должен закрывать канал.
  • Как устроен канал внутри: hchan с кольцевым буфером, мьютексом и очередями ждущих отправителей и получателей.

Закрепите за 5 минут

После урока в тренажёре 3-5 проверочных вопросов, дальше модуль уходит в повторения.

Вопросы модуля (26)