почему 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 - это не просто отсутствие значения. Это ещё и способ управлять поведением конкурентного кода.