почему read из nil-канала может навсегда остановить горутину
В го можно объявить канал и не инициализировать его:
var ch chan int
На первый взгляд это просто пустой канал. Но у nil-канала есть особое поведение.
Попробуем отправить в него значение:
ch <- 10
Или прочитать: value := <-ch Обе операции заблокируют goroutine навсегда. Никакой паники не будет. Просто ожидание без возможности продолжить.
А что произойдёт с select?
Вот пример: var ch chan int select { case value := <-ch: fmt.Println(value) default: fmt.Println("ничего нет") }
Здесь выполнится default.
Почему?
Потому что операция чтения из nil-канала никогда не готова к выполнению. А теперь уберём default:
select { case value := <-ch: fmt.Println(value) }
Теперь goroutine заблокируется навсегда.
Но у этого поведения есть полезное применение
Представим, что у нас есть два канала: for ch1 != nil || ch2 != nil { select { case value, ok := <-ch1: if !ok { ch1 = nil continue } fmt.Println("канал 1:", value) case value, ok := <-ch2: if !ok { ch2 = nil continue } fmt.Println("канал 2:", value) } }
Когда канал закрывается, мы присваиваем ему nil.
После этого соответствующая ветка select больше не выбирается, и цикл продолжает читать из оставшегося канала.
Это удобный способ динамически отключать ветки select.
Что важно запомнить?
Чтение из nil-канала блокируется навсегда.
Запись в nil-канал тоже блокируется навсегда.
select игнорирует операции с nil-каналами.
default выполнится, если остальные операции не готовы.
Присваивание nil каналу можно использовать для отключения ветки select.
Иногда nil в go - это не просто отсутствие значения. Это ещё и способ управлять поведением конкурентного кода.