Канал передаёт не только значение, но и синхронизацию. Чтобы конвейер завершался без утечек, нужно заранее решить, кто пишет, кто закрывает и кто отменяет работу.
Кто владеет каналом
Канал закрывает отправитель, не получатель. Получатель не знает, появится ли следующее значение, и закрытие во время записи другой горутины вызовет panic.
jobs := make(chan Job)
go func() {
defer close(jobs)
for _, job := range loadJobs() {
jobs <- job
}
}()
Если отправителей несколько, канал закрывают после завершения всех через WaitGroup. Никто не закрывает канал «на всякий случай» при отмене: сначала прекращают запись, затем закрывают.
Select для ожидания и отмены
select ждёт несколько операций одновременно:
select {
case result := <-results:
return result, nil
case err := <-errs:
return nil, err
case <-ctx.Done():
return nil, ctx.Err()
}
Если канал закрыт, получение возвращает нулевое значение и ok == false. Без этой проверки нулевой результат можно принять за настоящий.
Таймер на каждой итерации создают заново или переиспользуют через NewTimer с корректной остановкой. До Go 1.23 забытые таймеры действительно копились; теперь сборщик освобождает потерянные ссылки на них. Но создание таймера на каждом круге всё равно остаётся лишней работой: в долгом или горячем цикле один таймер останавливают и перезапускают.
Fan-out и fan-in
Fan-out запускает работу параллельно, fan-in собирает результаты в один канал:
Fan-out распределяет задачи между воркерами, fan-in собирает их ответы в один канал результатов.
results := make(chan Result, len(urls))
for _, url := range urls {
go func(url string) {
results <- fetch(url)
}(url)
}
for range urls {
result := <-results
consume(result)
}
Буфер len(urls) позволяет всем отправителям завершиться, даже если читатель выйдет по ошибке. Без буфера оставшиеся отправители встанут навсегда.
Буфер не отменяет работу. Для отмены нужен context, а внешняя операция должна уметь его принимать.
Направление и владение данными
Канал хорош для передачи владения объектом. После отправки предыдущий владелец больше не меняет объект. Иначе синхронизация есть, а гонка на содержимое остаётся.
Для разделяемого состояния канал не заменяет мьютекс. Если нужен доступ по чтению и записи из нескольких мест, состояние защищают отдельно или отдают одной горутине-владельцу.
Завершение
Рабочая схема завершения:
- прекратить создание новых задач;
- отменить текущие через
context; - дождаться отправителей через
WaitGroup; - закрыть канал результатов;
- дочитать результаты и вернуть итог.
Проверка на утечки идёт через goleak, а гонки через go test -race. Паника send on closed channel обычно означает, что порядок закрытия нарушен.
Коротко
- Канал закрывает только отправитель после завершения всех записей.
selectвыбирает результат, ошибку или отмену, но не отменяет внешнюю работу сам.- Буфер помогает отправителям завершиться, а не останавливает их.
- После передачи объекта прежний владелец его не меняет.
- Завершение - это протокол, а не один
close.