---
title: "Select и таймауты"
description: "Select в Go: мультиплексирование каналов, ветка default для неблокирующих операций, случайный выбор готовой ветки и таймауты через time.After."
course: "Продвинутые возможности Go: от замыканий до рефлексии"
url: "https://golangmaster.ru/courses/go-advanced/lessons/06-select/"
published: "2026-09-19"
updated: "2026-09-21"
minutes: 22
---

# Select и таймауты

*Курс «Продвинутые возможности Go: от замыканий до рефлексии», урок 6 из 25. ~22 минут. Обновлено 21.09.2026. Источник: https://golangmaster.ru/courses/go-advanced/lessons/06-select/*

select go — это switch, у которого каждый case стоит в очереди к своему каналу: одна конструкция ждёт сразу несколько каналов и просыпается, когда готова любая из них. Разберём мультиплексирование нескольких каналов, случайный жребий между готовыми ветками, неблокирующие операции с веткой default, воркера с выходом по каналу done и таймауты через time.After — и научимся не ждать вечно. Каждый пример запускается прямо на странице.

### Select в Go: диспетчер среди каналов

В [прошлом уроке](/courses/go-advanced/lessons/05-kanaly/) канал умел ждать ровно одну трубу: приём `<-ch` блокировал горутину, пока по ней не приедет значение. Жизнь быстро ломает такую простоту: сервер одновременно читает запросы из сети и сигналы остановки, воркер — задачи из очереди и команду «хватит», телеметрия смешивает логи, метрики и ошибки из трёх разных труб. Писать по приёму на каждый канал — значит расставить горутину в очередь ожиданий, где, пока вы стоите во второй строке, первая молчит зря.

select в Go — это диспетчер, который висит сразу на нескольких каналах и просыпается на первом готовом. По запросу select go вы найдёте ровно этот механизм: одна конструкция, несколько канальных веток, выполняется та, чья операция сейчас возможна. Это то самое мультиплексирование, о котором мы обещали в конце урока про каналы, — и оно же превратит ни в чём не повинную утечку горутины из [урока про горутины](/courses/go-advanced/lessons/04-gorutiny/) в решаемую задачу.

Синтаксически select — близнец switch, а по роли в арсенале — центральная техника [продвинутого раздела курса](/courses/go-advanced/): ветки выбираются не по значению выражения, а по готовности операций с каналами. Дальше — все правила этого выбора: от случайного жребия до таймаутов.

### Синтаксис select: switch для каналов

Выглядит select так: ключевое слово, фигурные скобки, внутри — ветки case, а каждая ветка — операция с каналом. Приём: `case v := <-ch1:`. Отправка: `case ch2 <- x:`. Ничего канального в ветке стоять не может — ни сравнение, ни вызов функции; компилятор отвергнет программу сразу. Есть и особая ветка `default`, которая срабатывает, когда не готов ни один канал, — о ней отдельный раздел ниже.

Три правила поведения. Первое: select вычисляет все свои канальные операции ровно один раз при входе и блокируется, если ни одна не готова. Второе: как только хотя бы одна операция становится возможной, select выполняет её ветку и выходит — в отличие от switch, прохода по всем веткам не бывает, и fallthrough здесь запрещён. Третье: приёмы `case v := <-ch` и двухзначный `case v, ok := <-ch` живут здесь так же, как в обычном коде, — флаг ok по-прежнему различает настоящий ноль от «канала больше нет».

Ошибиться в синтаксисе почти негде, а вот в ожиданиях — сколько угодно: поведение select при пустых, полных, закрытых и nil-каналах — это отдельная таблица истинности, которую мы разберём построчно. Начнём с главного сценария — ожидания нескольких источников.

### Что может стоять в ветке case

- `case v := <-ch:` — приём со связыванием значения с переменной v, видимой внутри ветки;
- `case v, ok := <-ch:` — приём с флагом закрытия: ok = false означает «канал закрыт и опустошён»;
- `case <-ch:` — голый приём: значение выбрасывается, важен только факт, что канал сработал;
- `case ch <- x:` — отправка: ветка готова, когда канал готов принять значение (есть место в буфере или ждущий приёмник);
- `case <-time.After(d):` — таймер: канал, который «прозвенит» один раз через d — на нём строятся таймауты.

### Мультиплексирование: select ждёт два канала

Запустите несколько раз: select каждый раз ловит новость — она готова через 30 мс, а тикер ещё спит. main был заблокирован на select, проснулся на первой готовой ветке и вышел; второй канал его уже не интересует. Эксперимент: поменяйте задержки местами (новостям — 500 мс, тикеру — 30) и победитель сменится. Заметьте: горутина тикера после выхода main так и останется спать — программа не ждёт никого, кто не нужен.

```go
package main

import (
        "fmt"
        "time"
)

func main() {
        news := make(chan string)
        ticker := make(chan string)
        go func() {
                time.Sleep(30 * time.Millisecond) // новости приходят быстро
                news <- "новость: сервер запущен"
        }()
        go func() {
                time.Sleep(500 * time.Millisecond) // тикер не спешит
                ticker <- "тикер: тик"
        }()
        select {
        case v := <-news:
                fmt.Println("select выбрал:", v)
        case v := <-ticker:
                fmt.Println("select выбрал:", v)
        }
}
```

### Как select принимает решение: блокировка и жребий

Логика выбора трёхступенчатая. Если готова ровно одна ветка — выполняется она, и это самый частый и самый детерминированный случай. Если не готова ни одна — select блокируется и ждёт, пока что-нибудь созреет; в этом он ничем не отличается от одиночного приёма `<-ch`, только ждёт сразу несколько вещей. А вот третья ступень — фирменная: **если готовых веток несколько, select бросает жребий и выполняет равновероятно одну из них**.

Зачем придумана случайность? Чтобы ни один канал не мог раз и навсегда зажать себе все внимание. Представьте ситчаут, где два канала стабильно готовы: при выборе «первого по порядку» второй ветке доставались бы крохи, а жребий держит шансы честными — 50 на 50 при двух готовых, одна третья при трёх. Это встроенная защита от голодания, и она достаётся вам бесплатно.

Из жребия следует практический вывод: программу, где две ветки готовятся одновременно, нельзя писать «под конкретный порядок» — вывод может отличаться от запуска к запуску. Все демо этого урока поэтому построены так, чтобы готовой была ровно одна ветка: рандеву с одной стороны, задержки с запасом в десять раз — с другой. Детерминизм в конкурентном коде делается дизайном, а не молитвой о планировщике.

### Два готовых канала: кто победит?

```go
package main

import "fmt"

func main() {
        a := make(chan string, 1)
        b := make(chan string, 1)
        a <- "из a"
        b <- "из b"
        select {
        case v := <-a:
                fmt.Println(v)
        case v := <-b:
                fmt.Println(v)
        }
}
```

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

- ✅ То «из a», то «из b»: при двух готовых ветках select бросает жребий
- Всегда «из a»: первая ветка важнее
- Всегда «из b»: последняя ветка важнее
- Программа зависнет: оба канала заняты

Обе ветки готовы мгновенно — в буферах лежит по значению, — поэтому работает правило жребия: равновероятный случайный выбор. Запустите в основном движке пять-шесть раз и соберите статистику: встретятся оба варианта. Оговорка для резервного движка песочницы — в callout ниже: упрощённый резерв жребий не бросает.

### Ветка default: неблокирующие операции

select умеет и обратное — не ждать ни секунды. Если среди веток есть `default`, блокировка отменяется: select мгновенно проверяет каналы, выполняет готовую ветку, а когда готовых нет — выполняет default. Так из блокирующих операций получаются **неблокирующие**: попробовать прочитать и уйти, попробовать записать и не застрять.

Классические сценарии три. Try-receive: заглянуть в канал «а нет ли уже работы» и, если пусто, заняться чем-то полезным. Try-send: положить значение, если есть место, а если буфер полон — не стоять в очереди, а, скажем, посчитать отказ. Try-both: проверить сразу несколько направлений и сделать что-нибудь по умолчанию, когда всё молчит. Ветка default — это и есть место для «а что, если никого нет».

Но у неблокирующего режима есть тёмная сторона, о которой стоит сказать жёстко: **select с default внутри `for` — сжигатель процессора**. Цикл «проверь канал — нет — проверь — нет — проверь» крутится тысячи раз в секунду и греет ядро впустую; это называется занятым ожиданием. Если вам нужен периодический опрос, вставляйте в цикл паузу или таймер — о способе через time.After скажем ниже, — а лучше вовсе уберите default и дайте select честно поспать.

### select+default: нечего читать

```go
package main

import "fmt"

func main() {
        ch := make(chan int)
        select {
        case v := <-ch:
                fmt.Println("пришло:", v)
        default:
                fmt.Println("нечего читать")
        }
}
```

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

- ✅ Нечего читать
- Пришло: 0 — приём вернул нулевое значение
- Программа зависнет: канал пуст
- Ошибка компиляции: select без готовых веток

В незабуферизованном пустом канале готовых веток нет — и select немедленно уходит в default: «нечего читать». main не заблокировался ни на мгновение. Уберите ветку default — и тот же код зависнет до лимита времени: без default select обязан ждать.

### select внутри for: воркер и канал done

Поодиночке select выполняется один раз — а сила приходит в связке с циклом. Скелет `for { select { ... } }` — это стандартный воркер Go: бесконечный цикл, который в каждом обороте ждет два события — «прилетела задача» и «прилетела команда остановиться». Задачи едут по рабочему каналу, команда — по каналу done; каждая ветка case обрабатывает своё.

Канал done — это почтовый ящик для стоп-сигнала. В [уроке про горутины](/courses/go-advanced/lessons/04-gorutiny/) горутины запускались и умирали сами по себе; здесь у вас появляется рычаг: main отправляет в done — воркер просыпается в ветке `case <-done:`, делает уборку и выходит через return. Обратите внимание на нюанс, который стоит прочувствовать: после выхода воркера main не знает, что тот закончил, — в демо ниже мы завели третий канал finished, через который воркер отчитывается, а main терпеливо принимает отчёт. Без такого рандеву main мог бы закончить работу раньше воркера и оборвать его финальную строку.

А как остановить сразу пятерых воркеров? Слать каждому по сигналу — развлечение на минуту. Каноничнее **закрыть канал done**: ветка приёма из закрытого канала готова всегда, поэтому close(done) срабатывает как широковещательный стоп для любого числа слушателей. Это уже мостик к разделу о закрытых каналах в select — и он подкреплён таблицей ниже. Ожидание целой бригады воркеров аккуратнее делать через WaitGroup — это тема ближайшего урока про синхронизацию, там же соберём полноценный пул workers.

### Воркер: выход по done-каналу

Запустите. Три задачи уезжают по одной — каждый send из main встречает воркера прямо в его select. Затем уходит команда done: воркер печатает про стоп-сигнал, отчитывается через finished и умирает; main дожидается отчёта и закрывает программу. Порядок строк строго детерминирован — рандеву держит его за руку. Попробуйте убрать строку <-finished и запустите в основном движке несколько раз: финальная строка воркера может не успеть.

```go
package main

import "fmt"

func main() {
        jobs := make(chan int)
        done := make(chan bool)
        finished := make(chan bool)
        go func() {
                for {
                        select {
                        case j := <-jobs:
                                fmt.Println("обработал:", j)
                        case <-done:
                                fmt.Println("воркер: стоп-сигнал получен")
                                finished <- true // отчитались main-у
                                return
                        }
                }
        }()
        for i := 1; i <= 3; i++ {
                jobs <- i // рандеву на каждую задачу
        }
        done <- true // команда завершения
        <-finished   // ждём отчёт, чтобы не обогнать воркера
        fmt.Println("main закончил работу")
}
```

### Допишите воркера

Восстановите скелет воркера: цикл с мультиплексированием задач и стоп-сигнала, выход по команде.

```go
package main

import "fmt"

func main() {
        jobs := make(chan int)
        stop := make(chan bool)
        go func() {
                for {
                        ___ {
                        case j := <-jobs:
                                fmt.Println("задача", j)
                        case <-___:
                                fmt.Println("стоп")
                                ___
                        }
                }
        }()
        jobs <- 1
        jobs <- 2
        stop <- true
}
```

Подсказка: Ключевое слово мультиплексора, имя канала со стоп-сигналом и оператор выхода из горутины.

Правильные ответы в пропусках: `select`, `stop`, `return`

### Таймауты: time.After и терпение с лимитом

Если вы искали по запросу «таймауты golang», то почти каждый найденный рецепт — одна и та же строка внутри select: `case <-time.After(d):`. Функция time.After из пакета time возвращает канал `<-chan time.Time` и обещает: через длительность d в этот канал приедет одно значение, «будильник прозвенит». В ветке select этот канал работает как соперник: либо основная операция успеет, либо прозвенит таймер — и вы уйдёте по ветке таймаута.

Важное отличие от time.Sleep: sleep блокирует горутину целиком — время ползёт, ничего не происходит. А таймер time.After строится и начинает идти **параллельно** с остальными ветками select: вы всё ещё ждёте данные, просто теперь у ожидания есть предел. Это и есть канонический таймаут на операцию с каналом — две-три строки вместо целого сторожевого механизма.

У медали есть оборот. Кто не победил в select — тот продолжил жить: неуспешный поставщик так и стоит в своей отправке, а таймер доживает до звонка и мусорится только потом. Один зависший поставщик — мелочь; сотня забытых — утечка горутин, которая съест память. Честное решение этой проблемы — контекст отмены: в курсе ему посвящён отдельный урок про context, а пока запомните правило — select с таймаутом спасает *ждущего*, но не *отправляющего*. И ещё: таймер нельзя перезапустить — для переиспользуемых будильников есть time.NewTimer с методом Stop, но в этом уроке нам хватит и одноразового.

### Таймаут: не ждать вечно

Запустите: поставщик спит целую секунду, таймаут прозвенел через 100 мс — победила ветка будильника, main не завис и пошёл дальше, хотя горутина поставщика так и стоит в своей отправке. Эксперимент: поставьте поставщику time.Sleep(50 * time.Millisecond) — теперь он успевает раньше будильника, и вывод меняется на «успели: данные». Оба исхода детерминированы: 50 против 100 и 100 против 1000 — запасы в два и десять раз.

```go
package main

import (
        "fmt"
        "time"
)

func main() {
        slow := make(chan string)
        go func() {
                time.Sleep(1 * time.Second) // поставщик безнадёжно медленный
                slow <- "данные"
        }()
        select {
        case v := <-slow:
                fmt.Println("успели:", v)
        case <-time.After(100 * time.Millisecond):
                fmt.Println("таймаут: ждали больше 100 мс")
        }
        fmt.Println("main не завис — идёт дальше")
}
```

### Соберите программу с таймаутом

Соберите из строк программу: select ждёт данные и таймаут, побеждает будильник. Проверьте себя: она печатает «таймаут».

Правильный порядок строк:

1. `package main`
2. `import "fmt"`
3. `import "time"`
4. `func main() {`
5. `ch := make(chan int)`
6. `select {`
7. `case v := <-ch:`
8. `fmt.Println("данные:", v)`
9. `case <-time.After(60 * time.Millisecond):`
10. `fmt.Println("таймаут")`
11. `}`
12. `}`

### Шпаргалка: как ведёт себя select

| Ситуация | Что делает select | Что запомнить |
| --- | --- | --- |
| Готова ровно одна ветка | Выполняет её немедленно | Самый частый и детерминированный случай |
| Готовы несколько веток | Бросает жребий: равновероятно одну из готовых | Защита от голодания каналов — бесплатно |
| Не готова ни одна, есть default | Выполняет ветку default | Неблокирующие операции: try-receive и try-send |
| Не готова ни одна, default нет | Блокируется до первой готовой | В настоящем Go полный затык всех горутин — fatal error deadlock |
| Ветка с nil-каналом | Никогда не готова — пропускается | Так «выключают» остывшие направления |
| Ветка чтения из закрытого канала | Готова всегда | Остаток буфера, потом нулевое значение и ok = false |
| Ветка case <-time.After(d) | Готова один раз через d | Канонический таймаут; таймер не отменяется |

### Закрытый канал в select: ветка, которая всегда готова

В прошлом уроке мы выучили: читать закрытый канал можно — сначала вычитывается остаток буфера, потом приём мгновенно возвращает нулевое значение и ok = false. Перенесите это знание в select, и получится жёсткое правило: **ветка чтения из закрытого канала готова всегда**. Для select «готово» означает «операция может выполниться не блокируясь» — а приём из закрытого канала выполняется мгновенно по определению.

Отсюда две противоположные практики. Злодейская: select в бесконечном for, где одна из веток читает канал, который кто-то закрыл, — ветка становится вечно готовой, цикл превращается в карусель нулевых значений, крутящуюся до лимита времени, а default при этом никогда не дождётся своей очереди. Диагностика простая: программа жрёт процессор и печатает нули — ищите близкий close.

Добрая практика из того же правила — широковещательная остановка. Пять воркеров слушают один канал done в ветках `case <-done:`; main делает `close(done)` — и все пять веток становятся готовыми одновременно, все воркеры выходят в одном мгновении, без пяти отдельных сигналов и без подсчёта «кому уже отправил, кому нет». Канал закрывает та сторона, которая им владеет, — правило из прошлого урока здесь работает без исключений.

### Закрытый канал в select: от буфера до карусели

```go
ch := make(chan int, 2)
ch <- 5
ch <- 6
close(ch) // канал закрыт, но в буфере ещё два значения

for i := 0; i < 3; i++ {
        select {
        case v, ok := <-ch: // закрытый канал: ветка чтения всегда готова
                fmt.Println("ветка:", v, ok) // 5 true, 6 true, 0 false
        default:
                fmt.Println("default не сработает ни разу")
        }
}
// А если такой select стоит в for без счётчика — цикл с закрытым
// каналом станет бесконечной каруселью нулей и будет есть процессор.
```

Три оборота цикла показывают полный сценарий: два значения из буфера с ok = true, затем ноль и ok = false — причём ветка побеждает default на каждом обороте, потому что закрытая ветка готова всегда, а default выбирается только когда готовых нет. Проверьте в песочнице: программа печатает «5 true», «6 true», «0 false» и ни разу — про default. (Блок намеренно без кнопки: в резервном движке нет close.)

### Проверьте себя

**Вопрос:** Канал закрыт и полностью опустошен. Что произойдёт с веткой case v, ok := <-ch в select?

- ✅ Ветка готова всегда: приём вернёт нулевое значение и ok = false
- Ветка никогда не готова: сработает default
- Ветка вызовет панику: читать закрытый канал нельзя
- Ветка заблокируется до первой отправки

close запрещает только новые отправки; чтение закрытого канала мгновенно возвращает ноль и ok = false. Поэтому в select такая ветка выигрывает жребий при каждой проверке — отсюда и широковещательный стоп через close(done), и опасность карусели нулей в бесконечном цикле.

### nil-канал в select: выключатель ветки

Прошлый урок оставил интригу: nil-канал блокирует навсегда — и «в конструкциях выбора планировщик его никогда не выбирает». Время собрать выигрыш. Ветке case нужен канал, готовый к операции; nil-канал не готов никогда — значит, ветка с ним мертва на корню. То, что в обычном коде было причиной зависаний, в select становится **выключателем ветки**.

Паттерн: приёмник владеет переменной канала и в любой момент присваивает ей nil — `secondary = nil` — и ветка `case v := <-secondary:` мгновенно замолкает, хотя сам select не переписан ни на строку. Присвоили другой канал — ветка ожила и слушает уже нового поставщика. Так мультиплексор собирают по частям: источник потерял доверие — выключили его ветку, вместо него подключили другой канал, а когда придёт quit — ушли совсем. Ничего похожего switch вам не предложит: там ветки задаются при компиляции, здесь — в рантайме значением переменной.

Один предостерегающий аккорд. Выключатель работает, пока жива хоть одна другая ветка: select, у которого **все** канальные ветки nil-каналы и нет default, блокируется навсегда — в настоящем Go это fatal error deadlock, в песочнице сработает лимит времени. И не забудьте про origin: нулевое значение канала — nil, забытый make выключает канал целиком, как говорилось в прошлый раз.

### Выключатель ветки: secondary = nil

```go
// primary и quit — каналы, созданные заранее
secondary := make(chan string) // запасной источник, пока слушаем
// ...в какой-то момент источник теряет доверие:
secondary = nil // ветка case <-secondary больше не выбирается никогда

for {
        select {
        case v := <-primary: // живая ветка: работает как раньше
                fmt.Println("основной поток:", v)
        case v := <-secondary: // nil-канал: ветка выключена
                fmt.Println("не сработает никогда:", v)
        case <-quit: // сигнал завершения
                return
        }
}
// Вернуть ветку к жизни можно тем же способом: secondary = другойКанал.
```

Присваивание nil каналу-переменной — это не ошибка, а идиома: select пересматривает готовность веток на каждом входе, и мёртвая ветка просто выпадает из жребия. Мультиплексор остаётся тем же кодом, а набор источников меняется в рантайме. (Блок без кнопки: фрагмент с внешними каналами, в резервном движке его не исполнить.)

> **Честно о двух движках песочницы.** Кнопка «Запустить» работает на основном движке — настоящем Go (yaegi, собран в WASM): select, close и time.After исполняются там как в локальном go run, включая случайный жребий между готовыми ветками. Если WASM не загрузился, песочница включает резервный упрощённый движок, и в нём: select есть, но жребий не бросается — всегда выбирается первая готовая ветка по порядку записи; встроенной close нет, поэтому закрытый канал в select не воспроизвести; time.After отсутствует («у пакета time нет члена After») — таймаут-примеры не запустятся вовсе; time.Sleep урезан до 50 мс, так что задержки демо не соблюдаются; а select без готовых веток и без default резерв отвечает ошибкой «select заблокирован навсегда: deadlock» вместо честного ожидания. Таймауты, predict про жребий и всё про закрытые каналы проверяйте в основном движке.

### Проверьте себя ещё раз

**Вопрос:** В select ни одна ветка не готова, а ветки default нет. Что произойдёт?

- ✅ select заблокируется и будет ждать, пока не готова хотя бы одна ветка
- select выполнит первую ветку по порядку записи
- select немедленно вернёт нулевые значения всех каналов
- select упадёт с паникой канала

Без default select — блокирующий вызов: горутина спит до первой готовой ветки. Если заблокированы вообще все горутины программы, настоящий Go падает с fatal error «all goroutines are asleep - deadlock!», а в песочнице ожидание прервёт лимит времени. Хотите не ждать — добавьте default или таймаут через time.After.

### Карточки урока

- **select — что это и для чего** — Switch для каналов: каждая ветка — приём или отправка. Select ждёт несколько каналов сразу (мультиплексирование) и выполняет первую готовую ветку; fallthrough нет, в ветках только канальные операции.
- **Несколько готовых веток** — Равновероятный случайный жребий — встроенная защита от голодания каналов. Программировать «под конкретный порядок» нельзя: вывод может отличаться от запуска к запуску.
- **Ветка default** — Срабатывает, когда ни одна канальная ветка не готова, — превращает select в неблокирующие операции (try-receive, try-send). В for с default — занятое ожидание, сжигающее процессор.
- **select внутри for: воркер и done** — for { select { case j := <-jobs ... case <-done: return } } — скелет воркера. done — канал со стоп-сигналом; close(done) останавливает всех слушателей разом (широковещательный стоп).
- **Таймаут: time.After(d)** — Возвращает канал, который прозвенит один раз через d. case <-time.After(d) в select даёт предел ожиданию операции. Таймер идёт параллельно, не отменяется и не спасает отправителя от утечки.
- **nil-канал и закрытый канал в select** — Ветка с nil-каналом никогда не готова — идиома отключения ветки (secondary = nil). Ветка чтения из закрытого канала готова всегда: остаток буфера, потом ноль и ok = false — отсюда и карусель нулей в бесконечном цикле.

## Коротко: что запомнить

- select — switch для каналов: каждая ветка — приём или отправка, select ждёт несколько каналов сразу и выполняет первую готовую.
- Готова одна ветка — она; готовы несколько — равновероятный жребий; не готова ни одна без default — блокировка до первой готовой.
- Ветка default делает select неблокирующим: try-receive и try-send; select с default в for — занятое ожидание, которое сжигает процессор.
- for { select { ... case <-done: return } } — скелет воркера; канал done даёт управляемую остановку, close(done) — широковещательную.
- case <-time.After(d) — канонический таймаут: будильник-канал идёт параллельно с ожиданием и прозвенит один раз через d.
- Ветка с nil-каналом никогда не готова: присваиванием nil источники «выключают», а присваиванием канала — включают обратно.
- Ветка чтения из закрытого канала готова всегда — остаток буфера, потом ноль и ok = false; в бесконечном for это карусель нулей.

## Домашнее задание

Соберите fan-in: два производителя шлют в два отдельных канала по пять сообщений с разными задержками time.Sleep, а main читает оба канала одним select в цикле for, печатая, из какого канала пришло сообщение. Завершайте приём по каналу done после десяти принятых сообщений, а затем добавьте в select ветку case <-time.After(500 * time.Millisecond) — пусть программа гарантированно не зависнет, даже если вы закомментируете одного производителя. Прогоните в основном движке песочницы и убедитесь, что порядок сообщений от запуска к запуску меняется — это жребий select.

## Вопросы и ответы по уроку

### Чем select отличается от switch простыми словами?

Switch выбирает ветку по значению выражения — условия известны до выполнения. Select выбирает ветку по готовности операций с каналами в момент выполнения: что созрело, то и работает. При нескольких готовых ветках switch строго упорядочен, а select бросает жребий; fallthrough в select запрещён.

### Что будет, если в select нет ни готовой ветки, ни default?

Select заблокирует горутину, пока хотя бы одна ветка не станет готовой, — это норма, а не ошибка. Падает настоящий Go только когда заблокированы все горутины программы: fatal error «all goroutines are asleep». Если блокировка нежелательна — добавьте default для неблокирующего режима или таймаут case <-time.After(d).

### Как сделать таймаут на чтение из канала?

Обернуть приём в select и добавить ветку case <-time.After(d): кто первый готов — тот и победил. Для сложных сценариев (отмена, дедлайн, таймаут на всю цепочку вызовов) в Go используется пакет context — ему посвящён отдельный урок курса, а ветка time.After остаётся каноническим решением в одну строку.

### Почему select с default в цикле грузит процессор?

Такой цикл не ждёт никогда: он проверяет каналы миллионы раз в секунду и каждый раз уходит в default — это занятое ожидание (busy loop). Лечится по обстоятельствам: убрать default и дать select спать, вставить паузу или тикер time.After между проверками, либо пересмотреть дизайн в сторону блокирующего воркера с done-каналом.
