---
title: "select"
description: "Горутина ждёт ответ от сервиса. Пока ждёт, может случиться одно из трёх: ответ пришёл, пользователь отменил запрос, истекло время. Приём из одного канала ждёт только одно событие…"
canonical: https://graphlms.ru/go/book/select
course: "Конкурентность Go"
updated: 2026-10-12
lang: ru
publisher: GraphLMS
---

# select

> Источник: https://graphlms.ru/go/book/select — курс «Конкурентность Go», GraphLMS.

## О чём глава

Горутина ждёт ответ от сервиса. Пока ждёт, может случиться одно из трёх: ответ пришёл,
пользователь отменил запрос, истекло время. Приём из одного канала ждёт только одно
событие. Чтобы ждать несколько сразу и реагировать на то, которое наступит первым, в
Go есть `select`.

`select` блокирует горутину, пока не станет возможна хотя бы одна из перечисленных
операций с каналами, и выполняет ровно одну из них. Из этой конструкции собираются
таймауты, отмена, неблокирующие попытки, слияние потоков и приоритеты. В главе
разберём правила выбора, `default`, таймауты и таймеры, выключение веток через
`nil`-каналы и то, как подготовиться к задачам блока 1.

## Перед чтением

Нужны правила буфера, закрытия и `nil`-каналов из [главы «Каналы»](/go/book/channels).
Ветки вида `case <-ctx.Done():` пока читайте как «ждём сигнала отмены»: это приём из
канала, который закрывается при отмене. Полностью контекст разобран в
[главе context](/go/book/context).

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

## Правила выбора

По форме `select` похож на `switch`, но каждая ветка — операция с каналом:

```go
select {
case v := <-in:
    process(v)
case out <- result:
    // отправили
case <-done:
    return
}
```

Как `select` выбирает ветку:

- готова ровно одна ветка — выполняется она;
- готовы несколько — выбирается одна из них **случайно**, с равной вероятностью;
  порядок записи веток в коде не важен;
- не готова ни одна, но есть `default` — выполняется `default`;
- не готова ни одна и `default` нет — горутина ждёт, пока какая-нибудь ветка не станет
  готовой.

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

Две горутины-генератора пишут в свои каналы, а цикл с `select` сливает оба потока.
Закончившийся источник отслеживаем по `ok == false` и считаем, сколько источников
ещё открыто:

```go play
package main

import "fmt"

func gen(vals ...int) <-chan int {
	ch := make(chan int)
	go func() {
		defer close(ch)
		for _, v := range vals {
			ch <- v
		}
	}()
	return ch
}

func main() {
	a, b := gen(1, 2, 3), gen(10, 20)
	sum := 0
	open := 2
	for open > 0 {
		select {
		case v, ok := <-a:
			if !ok {
				a = nil // закрыт: больше не выбираем
				open--
				continue
			}
			sum += v
		case v, ok := <-b:
			if !ok {
				b = nil
				open--
				continue
			}
			sum += v
		}
	}
	fmt.Println("sum =", sum)
}
```

```text
sum = 36
```

В каком порядке придут значения из `a` и `b`, заранее неизвестно, и от запуска к
запуску он меняется. Сумма от порядка не зависит. Корректные программы с `select`
строят именно так: проверяют то, что не зависит от порядка.

## default: не ждать

`default` превращает `select` в неблокирующую попытку. Если ни одна ветка не готова
прямо сейчас, выполняется `default`:

```go play
package main

import "fmt"

func main() {
	ch := make(chan int, 1)
	ch <- 42

	for i := 0; i < 2; i++ {
		select {
		case v := <-ch:
			fmt.Println("получили:", v)
		default:
			fmt.Println("пусто")
		}
	}
}
```

```text
получили: 42
пусто
```

На первой итерации в буфере лежит значение, ветка приёма готова. На второй канал пуст,
и `select` сразу уходит в `default`.

Так делают неблокирующую отправку (`case ch <- v:` плюс `default`: отправить, если есть
место, иначе отбросить), проверку «не пришёл ли сигнал» между шагами работы и
предпочтение одной ветки перед другой (об этом ниже).

Ловушка: `select` с `default` внутри цикла без другого ожидания крутится вхолостую и
загружает ядро на 100%. Если нужно «ждать данных, но реагировать на отмену», `default`
не нужен: добавьте ветку `<-ctx.Done()` или `<-done`, и `select` будет ждать сразу оба
канала, не тратя процессор.

## Таймауты

`time.After(d)` возвращает канал, в который через время `d` придёт текущее время. Если
поставить его веткой в `select`, получится таймаут:

```go play
package main

import (
	"fmt"
	"time"
)

func main() {
	ch := make(chan int)
	done := make(chan struct{})
	defer close(done)
	go func() {
		time.Sleep(30 * time.Millisecond) // «работа» дольше таймаута
		select {
		case ch <- 7:
		case <-done: // получатель ушёл по таймауту, отправлять некому
		}
	}()

	select {
	case v := <-ch:
		fmt.Println("значение:", v)
	case <-time.After(10 * time.Millisecond):
		fmt.Println("таймаут")
	}
}
```

```text
таймаут
```

Работа длится 30 мс, таймаут — 10 мс, поэтому выбирается вторая ветка. Поменяйте
задержки местами, и программа напечатает `значение: 7`. Обратите внимание на `select`
внутри горутины: без ветки `<-done` она осталась бы ждать на `ch <- 7` после того, как
`main` ушёл по таймауту. В этой программе это не заметно, потому что `main` сразу
завершается, но в долгоживущем сервисе каждый такой таймаут оставлял бы зависшую
горутину.

Различайте два вида таймаутов. Таймаут **каждого ожидания** («если 500 мс не было
событий, хватит») заново отсчитывается при каждом событии. Таймаут **всей операции**
(«на весь запрос не больше двух секунд») задают один раз, и для него удобнее
[context с дедлайном](/go/book/context).

## Время и таймеры

Немного словаря пакета `time`. `time.Duration` — промежуток времени:
`500*time.Millisecond` — полсекунды. `time.Sleep(d)` усыпляет текущую горутину.
`time.NewTimer(d)` создаёт таймер, который сработает один раз: через `d` в канал `t.C`
придёт значение. `time.NewTicker(d)` создаёт тикер, который присылает значения в `t.C`
каждые `d`, пока его не остановят.

У таймера два метода управления. `t.Stop()` отменяет срабатывание, `t.Reset(d)`
назначает новый срок. Ни один из них не закрывает канал `t.C`, поэтому горутина,
которая ждёт `<-t.C` у остановленного таймера, будет ждать вечно. Если ей нужно уметь
выйти, рядом в `select` должна стоять ветка `done` или `ctx.Done()`.

Поведение таймеров изменилось в Go 1.23, и это стоит знать, потому что старый код и
старые статьи до сих пор встречаются:

- **До Go 1.23** канал таймера был буферизованным. Если таймер уже сработал, а значение
  никто не забрал, после `Reset` в канале оставалось старое значение, и следующий приём
  срабатывал сразу. Поэтому перед `Reset` писали
  `if !t.Stop() { <-t.C }` — остановить и вычерпать старое значение.
- **С Go 1.23** (если в `go.mod` указана версия `go 1.23` или новее) после возврата из
  `Stop` или `Reset` старое значение из `t.C` уже не придёт. Вычерпывать канал не нужно.
  Кроме того, таймер, на который больше никто не ссылается, сборщик мусора убирает,
  даже если его не остановили.

`time.After` создаёт новый таймер при каждом вызове. В цикле, который крутится тысячи
раз в секунду, это тысячи таймеров; до Go 1.23 каждый жил до своего срабатывания. С
Go 1.23 это уже не утечка памяти, но лишние выделения остаются, и в горячем цикле
удобнее один таймер с `Reset`.

Проверьте понимание на задаче 03 (генератор с таймаутом бездействия) до того, как её
решать. Таймаут простоя 500 мс. Генератор стартовал в момент 0, последнее значение
получатель забрал в момент 200 мс, после этого никто не читает. Когда генератор
должен остановиться? Около 700 мс: срок отсчитывается от последней **успешной**
отправки. Попытка отправить, которую никто не принял, срок продлевать не должна.

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

В главе про [каналы](/go/book/channels) мы видели, что операция с `nil`-каналом ждёт
вечно. Внутри `select` это означает, что ветка с `nil`-каналом никогда не готова.
Присвоив переменной канала `nil`, вы выключаете ветку, не меняя код `select`.

Зачем это нужно, видно на функции `merge`, которая сливает два потока в один:

```go play
package main

import "fmt"

func gen(vals ...int) <-chan int {
	ch := make(chan int)
	go func() {
		defer close(ch)
		for _, v := range vals {
			ch <- v
		}
	}()
	return ch
}

// merge сливает два потока в один и закрывает выход, когда оба исчерпаны.
func merge(a, b <-chan int) <-chan int {
	out := make(chan int)
	go func() {
		defer close(out)
		for a != nil || b != nil { // пока жив хотя бы один источник
			select {
			case v, ok := <-a:
				if !ok {
					a = nil // a закрыт: выключаем ветку
					continue
				}
				out <- v
			case v, ok := <-b:
				if !ok {
					b = nil
					continue
				}
				out <- v
			}
		}
	}()
	return out
}

func main() {
	count := 0
	for range merge(gen(1, 2, 3), gen(4, 5)) {
		count++
	}
	fmt.Println("получено значений:", count)
}
```

```text
получено значений: 5
```

Без обнуления закрытый канал `a` был бы готов всегда, `select` выбирал бы его ветку
снова и снова, и цикл крутился бы вхолостую, получая нули. С обнулением ветка просто
исчезает из выбора. Этот `merge` ещё не умеет останавливаться, если потребитель ушёл
раньше времени; как это исправить, разобрано в главе [«Паттерны»](/go/book/patterns).

## Предпочтение одной ветки

`select` не знает приоритетов. Если готовы и `<-done`, и `<-data`, выбор случаен. Когда
одну ветку нужно проверять первой, это делают вложенным `select`: сначала неблокирующая
проверка важной ветки, потом общее ожидание.

```go
for {
    select {
    case v := <-high:
        use(v)
        continue // high был готов: обработали и снова проверяем high
    default:
    }
    select { // high пуст: ждём любой из двух
    case v := <-high:
        use(v)
    case v := <-low:
        use(v)
    }
}
```

Это предпочтение в момент первой проверки, а не строгий приоритет. Если оба канала
станут готовы, пока горутина ждёт во втором `select`, выбор снова случаен. И если
`high` готов постоянно, `low` может не получить ни одного шанса — это называется
голоданием. Задача 04 построена именно на таком цикле; закрытие каналов и выход из
цикла там нужно добавить самостоятельно.

## Пустой select

```go
select {}   // ждёт вечно
```

У `select` без веток нет ни одной ветки, которая может стать готовой, поэтому он ждёт
вечно. Иногда так пишут в конце `main`, когда вся работа идёт в фоновых горутинах. Если
заблокируются вообще все горутины, рантайм завершит программу с сообщением
`fatal error: all goroutines are asleep - deadlock!`. Для сервисов чаще ждут сигнал
остановки (см. graceful shutdown в главе [context](/go/book/context)), чтобы было как
завершиться.

## Перед задачей 01: рекурсия и «или»-канал

Задача 01 просит функцию `or`, которая по любому числу сигнальных каналов возвращает
один канал, закрывающийся при срабатывании любого из входных. Для двух каналов это
один `select`:

```go
func or2(a, b <-chan struct{}) <-chan struct{} {
	out := make(chan struct{})
	go func() {
		defer close(out)
		select {
		case <-a:
		case <-b:
		}
	}()
	return out
}
```

Для произвольного числа каналов `select` с переменным числом веток не напишешь: число
`case` фиксируется при компиляции. Выход — рекурсия. Рекурсивная функция решает задачу,
вызывая саму себя для задачи поменьше, и у неё обязательно есть условие остановки.
Простейший пример:

```go
func count(n int) int {
	if n <= 0 {
		return 0           // условие остановки
	}
	return 1 + count(n-1) // та же задача, но меньше
}
```

Для `count(3)` вызовы идут `3 → 2 → 1 → 0`, а результаты возвращаются обратно:
`0 → 1 → 2 → 3`. В `or` уменьшается число каналов: «или» от пяти каналов можно выразить
через «или» от части каналов и `select` по нескольким. Подумайте, какие у рекурсии
случаи остановки (0 и 1 канал описаны в условии).

Есть тонкость, о которой легко забыть. Когда сработал один канал, `select` во
вспомогательных горутинах на других уровнях рекурсии всё ещё ждёт свои каналы, и если
они не сработают никогда (`sig(3*time.Hour)`), горутины зависнут. Каждой
вспомогательной горутине нужен способ узнать, что результат уже получен. Тесты задачи
этого не проверяют, но условие требует.

## Частые ошибки

**Расчёт на порядок веток.** «`done` написан первым, значит, проверится первым» —
неверно. При готовности обеих веток выбор случаен.

**Нет ветки отмены.** `select`, который ждёт только данные, висит вечно, если данные
перестали приходить. В долгоживущем цикле почти всегда нужна ветка `<-ctx.Done()` или
`<-done`, иначе горутину нечем остановить.

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

**Закрытый канал в цикле с `select`.** Ветка закрытого канала готова всегда. Без проверки
`ok` и обнуления цикл будет бесконечно получать нулевые значения.

## Мысленная модель

`select` — это «дождись первого из нескольких событий». Горутина паркуется сразу на всех
каналах из веток и просыпается, когда одна из операций может выполниться. Поведение
настраивается двумя способами: `default` (не ждать вообще) и `nil`-канал (не ждать
конкретного события). Этого хватает почти на всю управляющую логику конкурентной
программы.

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

Создайте два канала `int` с буфером 1. Положите в первый `3`, во второй `7`, закройте
оба. В цикле с `select` прочитайте все значения, выключая исчерпанные каналы через
`nil`, и напечатайте сумму. Можно ли рассчитывать, что первой прочитается тройка?

### Проверка

Сумма — `10`. Какой канал прочитается первым, не определено: в начале готовы оба,
и `select` выбирает случайно.

```go
sum := 0
for a != nil || b != nil {
    select {
    case v, ok := <-a:
        if !ok {
            a = nil
            continue
        }
        sum += v
    case v, ok := <-b:
        if !ok {
            b = nil
            continue
        }
        sum += v
    }
}
fmt.Println(sum)
```

Фрагмент ставится после создания и заполнения `a` и `b`. Без обнуления цикл никогда бы
не закончился: закрытый пустой канал всегда готов.

## Что дальше

Ветка `<-ctx.Done()` в `select` — стандартный способ сделать операцию отменяемой; она
разобрана в главе [context](/go/book/context). Слияние потоков, конвейеры и пулы,
построенные на `select`, — в главе [«Паттерны»](/go/book/patterns).

`select` — половина [блока 1](/go/topics/1) задач (задачи 01–04): «или»-канал, отправка с выходом по
сигналу, таймаут бездействия и предпочтение одного канала. В блоках 3, 4 и 7 связка
`select` с `<-ctx.Done()` встречается почти в каждой горутине.
