Каналы: ownership, send/receive и close
Канал передаёт значения между goroutine и синхронизирует успешные send/receive. Он не заменяет проектирование: нужно заранее определить владельца, количество producers, условие завершения и реакцию на отмену. Канал обычно закрывает единственный sender-владелец, а receiver только наблюдает закрытие.
Зачем это на интервью
Здесь проверяют точное поведение channel operations: когда они блокируются, что возвращает receive после close, почему send on closed channel паникует и как API с направленными каналами выражает ownership.
Минимум для E4
- Отличать небуферизированный rendezvous от буфера конечной ёмкости.
- Знать: receive из закрытого канала возвращает zero value и
ok=false; send и повторныйcloseпаникуют. - Использовать
for v := range chтолько когда владелец гарантированно закроетch. - В сигнатурах использовать
<-chan Tиchan<- T, когда сторона не должна делать обе операции.
package main
import "fmt"
func produce(out chan<- int) { // только эта функция владеет закрытием
defer close(out)
for i := 1; i <= 3; i++ {
out <- i
}
}
func main() {
values := make(chan int, 2)
go produce(values)
for v := range values {
fmt.Println(v)
}
}Буфер не меняет семантику порядка или ownership: он позволяет send идти вперёд, пока есть место, и создаёт ограниченную очередь. Небуферизированный send завершается лишь при готовом receive; send в заполненный буфер и receive из пустого открытого канала блокируются.
Углубление для E5/Senior
Закрытие — широковещательный сигнал «новых значений не будет», а не отмена работы и не освобождение памяти. Его используют для завершения range и fan-in, но не для передачи одного события, если много сторон могут попытаться закрыть. Для жизненного цикла запроса предпочтителен context: он несёт отмену и deadline, а channel остаётся транспортом значений.
Буфер — часть backpressure policy. Размер по умолчанию не должен быть «на всякий случай»: большой буфер маскирует отставание consumer и удерживает память, маленький ограничивает in-flight работу. При нескольких producers закрытие координирует отдельная goroutine после WaitGroup, либо канал вообще не закрывают, если consumer завершается по context/счётчику.
Ключевые понятия
- Unbuffered channel — синхронная передача rendezvous между sender и receiver.
- Buffered channel — FIFO-очередь конечной ёмкости; элементы доставляются по порядку.
- Close — переводит канал в состояние без будущих send и разблокирует receivers после опустошения буфера.
- Comma ok —
v, ok := <-ch;ok=falseотличает закрытие от легитимного zero value. - Направленный канал — ограниченный view одного канала, проверяемый компилятором.
Типовые вопросы
- Что вернёт
<-chпослеclose(ch)?- Сначала оставшиеся буферизированные значения, затем бесконечно zero value типа. Для различения нужен
v, ok := <-ch.
- Сначала оставшиеся буферизированные значения, затем бесконечно zero value типа. Для различения нужен
- Кто закрывает канал?
- Sender, обычно единственный владелец production. Receiver не знает, завершились ли все senders.
- Нужно ли закрывать каждый канал?
- Нет. Закрывают, когда receivers должны узнать о конце потока; сборщик мусора освободит недостижимый канал.
- Что даёт буфер?
- Ограниченное разъединение скоростей sender и receiver; не даёт бесконечную пропускную способность и не устраняет гонки данных вокруг передаваемых объектов.
- Что произойдёт при send в закрытый канал?
- Panic.
recoverне является обычной координацией; надо устранить неоднозначность ownership.
- Panic.
- Почему
range chможет зависнуть?- Range заканчивается только при
close; если sender исчез без закрытия или cancellation, receiver ждёт вечно.
- Range заканчивается только при
Практика
- Реализуйте producer с
chan<- intи consumer с<-chan int. Готово: только producer вызываетclose, consumer корректно завершает range. - Передайте нулевое значение через канал и закройте его. Готово: тест различает данные и закрытие через comma-ok.
- Постройте fan-in двух producers. Готово: общий output закрывается ровно один раз после завершения обоих,
go test -raceпроходит.
Частые ошибки и ловушки
- Закрывать канал «чтобы освободить память».
- Использовать zero value как признак закрытия без
ok. - Давать нескольким senders право
close. - Ставить большой buffer вместо ограничения producer или отмены.
- Отправлять в канал после того, как другой путь может его закрыть.
Связанные темы
Конкурентность · select и nil-каналы · Fan-out/fan-in · Pipeline и отмена