← Назад к вопросам

Что выведет код? nil канал

2.0 Middle🔥 281 комментариев
#Конкурентность и горутины

Условие

Определите, что произойдет при выполнении следующего кода:

package main

import "fmt"

func main() {
    var ch chan int
    
    select {
    case val := <-ch:
        fmt.Println(val)
    default:
        fmt.Println("default")
    }
}

Вопросы

  1. Что выведет программа?
  2. Что произойдёт, если убрать default case?
  3. Как используются nil каналы в практике?

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

nil канал в select - полное решение

Ответы на вопросы

1. Что выведет программа?

default

Объяснение:

  • var ch chan int - объявляем переменную типа канал, но не инициализируем её
  • Неинициализированный канал имеет значение nil
  • При select из nil канала - операция не блокируется, вместо этого пропускается
  • Так как нет других успешных операций в select, выполняется default ветка

2. Что произойдет, если убрать default case?

var ch chan int

select {
case val := <-ch:
    fmt.Println(val)
}

Результат: Программа зависнет (deadlock).

Почему?

  • select из nil канала никогда не будет готов к выполнению
  • Это не паника, а просто вечная блокировка
  • Go runtime обнаружит deadlock и выведет ошибку:
    fatal error: all goroutines are asleep - deadlock!
    

Подробный анализ поведения nil каналов

Операции с nil каналом

var ch chan int  // ch == nil

// Чтение из nil канала
val := <-ch      // блокируется навсегда (deadlock, если это основная горутина)

// Отправка в nil канал  
ch <- 42         // паника: send on nil channel

// Закрытие nil канала
close(ch)        // паника: close of nil channel

// select с nil каналом
select {
case val := <-ch:  // эта ветка НИКОГДА не выполнится
    fmt.Println(val)
default:
    fmt.Println("ok")  // выполнится всегда
}

Таблица поведения nil каналов

ОперацияРезультатПримечание
<-ch (вне select)DeadlockБлокируется навсегда
<-ch (в select)ПропускаетсяНе готов к выполнению
ch <- valПаникаsend on nil channel
close(ch)Паникаclose of nil channel
len(ch)0Работает
cap(ch)0Работает

Практический пример: отключаемые каналы

Проблема: Как отключить канал в select без паники?

// ❌ ПЛОХО - не работает
func worker(chA, chB chan int) {
    for {
        select {
        case val := <-chA:
            processA(val)
        case val := <-chB:
            processB(val)
        }
    }
}

// Если нужно отключить chA, делаем chA = nil
// chA = nil  <- теперь select из chA будет пропускаться

Полный пример с динамическим отключением:

package main

import (
    "fmt"
    "time"
)

func main() {
    chA := make(chan int)
    chB := make(chan int)
    
    go func() {
        time.Sleep(100 * time.Millisecond)
        chA <- 1
        chA <- 2
    }()
    
    go func() {
        time.Sleep(150 * time.Millisecond)
        chB <- 10
        chB <- 20
    }()
    
    received := 0
    
    for {
        select {
        case val, ok := <-chA:
            if !ok {
                fmt.Println("chA closed")
                chA = nil  // отключаем chA, теперь select будет пропускать эту ветку
            } else {
                fmt.Printf("From A: %d\\n", val)
                received++
                if received >= 4 {
                    return
                }
            }
        
        case val, ok := <-chB:
            if !ok {
                fmt.Println("chB closed")
                chB = nil  // отключаем chB
            } else {
                fmt.Printf("From B: %d\\n", val)
                received++
                if received >= 4 {
                    return
                }
            }
        }
    }
}

// Вывод:
// From A: 1
// From A: 2
// From B: 10
// From B: 20

Практическое применение: отключение каналов

func multiplexChannels(chA, chB, chC chan string) {
    // Изначально оба канала активны
    activeCh := chA
    inactiveCh := chB
    
    for {
        select {
        case val := <-activeCh:
            fmt.Println("Active:", val)
            // Переключаемся на другой канал
            activeCh, inactiveCh = inactiveCh, activeCh
        
        case <-chC:  // канал для выхода
            return
        }
    }
}

Концепция nil каналов в архитектуре

nil канал используется как "отключатель":

func process(done <-chan struct{}) {
    ticker := time.NewTicker(1 * time.Second)
    defer ticker.Stop()
    
    for {
        select {
        case <-ticker.C:
            // Основная работа
            doWork()
        
        case <-done:
            // Graceful shutdown
            return
        }
    }
}

// Можно динамически отключать ticker:
func processOptional(skipTicker bool) {
    var tickerCh <-chan time.Time
    ticker := time.NewTicker(1 * time.Second)
    defer ticker.Stop()
    
    if !skipTicker {
        tickerCh = ticker.C
    }
    // Если skipTicker == true, tickerCh == nil
    // и select будет пропускать этот case
    
    for {
        select {
        case <-tickerCh:  // nil канал пропускается
            doWork()
        case <-done:
            return
        }
    }
}

Best Practices

  1. Избегай nil каналов при чтении вне select - всегда deadlock
  2. nil канал в select безопасен - просто пропускается
  3. Используй для отключения веток - вместо булевых флагов в select
  4. Нельзя писать/закрывать nil канал - всегда паника
  5. Проверяй before использования - если канал может быть nil

Сравнение подходов

Подход 1: Булев флаг (не рекомендуется)

var disabled bool
for {
    if !disabled {
        select {
        case val := <-ch:
            process(val)
        }
    }
}

Подход 2: nil канал (рекомендуется)

var ch <-chan int
for {
    select {
    case val := <-ch:  // если ch == nil, пропускается
        process(val)
    }
}

Трюк: использование nil для выключения случайного select case

func selectRandom(chA, chB chan int, skipB bool) {
    // Отключаем chB если нужно
    if skipB {
        chB = nil
    }
    
    select {
    case val := <-chA:
        fmt.Println("A:", val)
    case val := <-chB:  // если chB == nil, эта ветка пропускается
        fmt.Println("B:", val)
    }
}

Почему это важно в production коде?

  • Динамическое управление горутинами - отключаем обработчики на лету
  • Graceful degradation - если части системы падают
  • Сложные паттерны синхронизации - multiplex, demux, fairness
  • Уменьшение copy-paste - вместо множества булевых условий
Что выведет код? nil канал | PrepBro