Разберите ошибку в коде: какой участник должен закрывать канал, чтобы несколько отправителей не завершили его преждевременно?
package main
import (
"fmt"
"sync"
)
func main() {
ch := make(chan int)
var wg sync.WaitGroup
for i := 0; i < 2; i++ {
wg.Add(1)
go func(v int) {
defer wg.Done()
ch <- v
close(ch)
}(i)
}
go func() { wg.Wait(); close(ch) }()
for v := range ch { fmt.Println(v) }
}
Канал должен закрывать отдельный координатор после завершения всех отправителей. В данном коде каждый отправитель закрывает общий канал самостоятельно, поэтому другой отправитель может получить панику при отправке в уже закрытый канал, а координатор — панику при повторном закрытии.
Правильная схема: отправители только отправляют значения и завершают работу, а одна координирующая горутина вызывает Wait и затем единственный close.
Каналы в Go предназначены не только для передачи данных, но и для координации жизненного цикла конвейера. Операция close сообщает получателям, что новых значений больше не будет, поэтому получатель может безопасно завершить range по каналу.
Такой подход уменьшает необходимость в общей переменной-состоянии и ручных блокировках вокруг очереди. Однако канал имеет одного логического владельца операции закрытия: тот, кто знает, что все отправители закончили работу.
В исходном коде канал закрывается каждым из двух отправителей, а затем ещё и горутиной-координатором. После первого close отправка второго отправителя в этот канал вызывает панику send on closed channel.
Даже если отправители успеют передать значения, координатор после wg.Wait() попытается закрыть уже закрытый канал и получит панику close of closed channel. Удаление только закрытия координатора тоже неверно: получатель может закончить чтение после первого закрытия, пока другой отправитель ещё заблокирован на отправке.
Исправленная схема выглядит так:
WaitGroup учитывает всех отправителей. Координатор ждёт, пока каждый из них выполнит Done, и только после этого закрывает канал. Значит, после close уже не останется легитимных отправок.
Закрытие канала не удаляет уже переданные значения: получатель сначала прочитает буфер и значения, ожидающие выдачи, а затем увидит завершение канала. Закрытый канал нельзя снова закрыть, и в него нельзя отправлять; чтение из него допустимо и после закрытия.
Обычно отправитель или владелец производящего этапа отвечает за закрытие канала, но при нескольких отправителях эту роль нужно передать координатору. sync.Once может предотвратить двойной вызов close, но не устраняет главную проблему: он не делает безопасными отправки, которые продолжаются после закрытия.
Если завершение должно быть досрочным, одного close(ch) недостаточно: закрытие канала данных не является универсальным механизмом отмены отправителей. Для отмены обычно используют context.Context или отдельный канал сигнала, а завершение канала данных выполняют после остановки всех производителей.
В конвейере обработки файлов несколько горутин читали задания и отправляли результаты в общий канал. Одна из горутин закрывала канал, когда сама заканчивала чтение своей части входа. При высокой нагрузке другие горутины ещё отправляли результаты, поэтому сервис периодически завершался с send on closed channel.
Рассматривались два варианта. Защитить close через sync.Once было недостаточно: двойного закрытия не возникало, но отправка после первого закрытия всё равно паниковала. Заставить потребителя закрывать канал было ещё опаснее, поскольку потребитель не владел информацией о состоянии всех производителей.
Выбранное решение — отдельная горутина-координатор с WaitGroup: каждый производитель вызывает Done через defer, а координатор закрывает канал после Wait. В результате потребитель получает все результаты, range завершается штатно, а преждевременные закрытия исчезают.
Может ли получатель закрыть канал, если он больше не хочет читать?
Технически получатель может вызвать close, если он достоверно знает, что отправителей больше нет. Но обычно такой информацией владеет не получатель, а производитель или координатор. Если отправитель продолжит работу, его следующая отправка вызовет панику, поэтому досрочное закрытие со стороны потребителя обычно является ошибкой проектирования.
Зачем нужен отдельный координатор, если WaitGroup уже используется?
WaitGroup только сообщает, что счётчик завершённых горутин достиг нуля; он сам не закрывает канал. Закрытие после Wait должно находиться в отдельной горутине, потому что основной поток в это время обычно занят чтением канала. Иначе можно получить взаимную блокировку: основной поток ждёт значения через range, а код, который должен закрыть канал после Wait, не исполняется.
Как отличить нулевое значение от завершения закрытого канала?
Однозначное чтение v := <-ch не позволяет отличить отправленный ноль от нулевого значения, возвращённого закрытым каналом. Нужно использовать двухзначную форму: v, ok := <-ch; ok == false означает, что канал закрыт и значений больше нет. Это особенно важно для каналов чисел, логических значений и указателей, где нулевое значение может быть корректными данными.