Как решить задачу слияния данных из нескольких каналов в Go?
Что проверяют
Умеете ли вы написать fan-in и знаете ли, кто и когда закрывает результирующий канал.
Как отвечать
Слияние делается горутиной на каждый входной канал и общим выходным. Ключевая деталь: выходной канал закрывается один раз, после того как закончились все входные, для чего используется sync.WaitGroup.
func merge(ctx context.Context, chans ...<-chan int) <-chan int {
out := make(chan int)
var wg sync.WaitGroup
wg.Add(len(chans))
for _, c := range chans {
go func(c <-chan int) {
defer wg.Done()
for v := range c {
select {
case out <- v:
case <-ctx.Done():
return
}
}
}(c)
}
go func() { wg.Wait(); close(out) }()
return out
}Что стоит проговорить:
- закрывает отправитель, и здесь отправителей несколько, поэтому закрытие вынесено в отдельную горутину после
wg.Wait(). Закрыть из каждой горутины значит получить панику; ctxобязателен: без него горутина, застрявшая на отправке в канал, который никто не читает, останется навсегда. Это самая частая утечка горутин;- порядок между каналами не сохраняется, гарантируется только порядок внутри каждого;
- буфер у
outсглаживает скачки, но не заменяет отмену.
Вариант с select по фиксированному числу каналов подходит, когда их два или три и нужно выключать ветки: исчерпанный канал присваивают nil, и он перестаёт быть готовым.
Где спотыкаются
Закрывают выходной канал в каждой горутине и получают панику.
Забывают про контекст и оставляют горутины висеть на отправке.
Ждут сохранения общего порядка сообщений.
Захватывают переменную цикла без передачи параметром (в версиях до Go 1.22).