Каналы: 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 одного канала, проверяемый компилятором.

Типовые вопросы

  1. Что вернёт <-ch после close(ch)?
    • Сначала оставшиеся буферизированные значения, затем бесконечно zero value типа. Для различения нужен v, ok := <-ch.
  2. Кто закрывает канал?
    • Sender, обычно единственный владелец production. Receiver не знает, завершились ли все senders.
  3. Нужно ли закрывать каждый канал?
    • Нет. Закрывают, когда receivers должны узнать о конце потока; сборщик мусора освободит недостижимый канал.
  4. Что даёт буфер?
    • Ограниченное разъединение скоростей sender и receiver; не даёт бесконечную пропускную способность и не устраняет гонки данных вокруг передаваемых объектов.
  5. Что произойдёт при send в закрытый канал?
    • Panic. recover не является обычной координацией; надо устранить неоднозначность ownership.
  6. Почему range ch может зависнуть?
    • Range заканчивается только при close; если sender исчез без закрытия или cancellation, receiver ждёт вечно.

Практика

  • Реализуйте 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 и отмена

Источники