Канал это типизированная труба между горутинами. «Не общайтесь через общую память, делитесь памятью через общение».
Создание и виды
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 |
| close | panic | успех | 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с кольцевым буфером, мьютексом и очередями ждущих отправителей и получателей.