Что такое блокировка (deadlock) при работе с каналами в Go и как она возникает?

Ответ

Блокировка (deadlock) в Go — это ситуация, когда одна или несколько горутин бесконечно ожидают события, которое никогда не произойдет. Чаще всего это связано с неправильной работой с каналами.

Основные причины блокировок с каналами:

  1. Отправка в небуферизованный канал без получателя. Небуферизованный канал (make(chan T)) требует, чтобы отправитель и получатель встретились одновременно. Если горутина пытается отправить данные, а получателя нет, она блокируется навсегда.

    func main() {
        ch := make(chan int)
        ch <- 1 // Deadlock! Нет другой горутины, готовой принять значение.
    }
  2. Отправка в заполненный буферизованный канал. Буферизованный канал (make(chan T, N)) блокирует отправку только тогда, когда его буфер полон, и нет готового получателя.

    func main() {
        ch := make(chan int, 1) // Буфер размером 1
        ch <- 1 // OK, значение помещено в буфер
        ch <- 2 // Deadlock! Буфер полон, и никто не читает из канала.
    }
  3. Чтение из пустого канала, который никто не закроет. Горутина, читающая из канала, будет заблокирована, пока в него не придут данные или пока канал не будет закрыт.

Способы избежать блокировки:

  • Запускать отправителей и получателей в разных горутинах. Это основной паттерн конкурентной работы в Go.
  • Использовать select с default для неблокирующей отправки/чтения:
    select {
    case ch <- value:
        // Успешно отправлено
    default:
        // Канал заблокирован, выполняем альтернативное действие
    }
  • Использовать select с таймаутом, чтобы избежать вечного ожидания:
    select {
    case val := <-ch:
        // Получили значение
    case <-time.After(1 * time.Second):
        // Не дождались за 1 секунду
    }
  • Правильно закрывать каналы. Канал закрывается отправителем, чтобы сообщить получателям, что данных больше не будет.