Как организовать прослушивание канала в отдельной горутине, чтобы избежать блокировок?
Что проверяют
Понимаете ли вы, что блокировка горутины на чтении канала это нормальное и дешёвое состояние, а проблема в утечках и в отсутствии условия выхода. Следом спрашивают, кто закрывает канал и как дождаться завершения слушателя.
Как отвечать
Базовый вариант: for msg := range ch в отдельной горутине. Пока сообщений нет, горутина припаркована планировщиком и не тратит CPU. Цикл завершается, когда отправитель вызывает close(ch).
Если нужна отмена извне, добавьте select:
- чтение с проверкой:
case msg, ok := <-ch:, приok == falseканал закрыт, выходим; - отмена:
case <-ctx.Done(): return, контекст приходит из вызывающего кода с таймаутом или отменой; - ожидание завершения:
sync.WaitGroupили каналdone, а если слушателей несколько и нужны ошибки,errgroupизgolang.org/x/sync.
Правила владения каналом:
- закрывает отправитель, и только один. Отправка в закрытый канал и повторный
closeвызывают панику; - чтение из
nil-канала блокирует навсегда. Это полезно, чтобы выключить веткуselect, и опасно, если канал забыли создать; - буфер (
make(chan T, n)) сглаживает всплески, но не решает проблему медленного потребителя.
Если отправитель не должен блокироваться, неблокирующая отправка делается через select с default: сообщение отбрасывается или учитывается в метрике. Это осознанное решение о потере данных, его стоит проговорить.
Утечки горутин ищут через профиль goroutine в net/http/pprof, а в тестах их ловит goleak.
Где спотыкаются
Ставят default с time.Sleep в цикл чтения. Получается активное ожидание: лишняя задержка на каждом сообщении и пустая работа процессора, хотя обычное блокирующее чтение делает то же бесплатно.
Закрывают канал со стороны читателя или из нескольких отправителей. Рано или поздно это паника send on closed channel.
Не дают горутине способа завершиться и дожидаются её через time.Sleep в main. В сервисе такая горутина живёт вечно и держит память, а в тестах результат зависит от везения.