Перейти к содержимому
Теория · Горутины и каналы · урок 8 из 13

select

Представьте официанта, который стоит между двумя окнами раздачи: в одно подают горячее, в другое — напитки. Если он ждёт только у окна с горячим, готовые напитки так и стоят, пока не подадут горячее. Удобнее стоять между окнами и забирать то, что появилось первым.

С каналами то же самое. Приём из канала ждёт, пока в канале не появится значение. Когда значение может прийти по одному из двух каналов, ждать один из них нельзя: <-nums будет ждать вечно, если значение пришло в words.

Ожидание нескольких каналов

Оператор select ждёт сразу несколько операций с каналами и выполняет ту, что готова:

GO
package main

import "fmt"

func main() {
    nums := make(chan int)
    words := make(chan string, 1)
    words <- "go"
    select {
    case n := <-nums:
        fmt.Println("number", n)
    case w := <-words:
        fmt.Println("word", w) // word go
    }
}

Давайте разберём пример по шагам:

  1. nums := make(chan int) создаёт канал, в который никто не отправляет.
  2. words := make(chan string, 1) создаёт канал с буфером, и words <- "go" кладёт в буфер "go".
  3. select смотрит на оба окна: приём из nums не готов, приём из words готов.
  4. select выполняет второй case, и программа печатает word go.

Ветки select

select записывается как switch, но каждый case в нём — операция с каналом. Таких операций две.

Приём

Ветка case w := <-words: выполняется, когда из канала можно принять значение. Переменная, объявленная в приёме, доступна только внутри своего case, как w в примере. Принятое значение можно и не сохранять: case <-done:.

Отправка

Ветка может и отправлять: case ch <- v: выполняется, когда канал готов принять значение.

Как выбирается case

select подчиняется трём правилам:

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

Здесь аналогия с официантом заканчивается: человек подошёл бы к ближнему окну, а select бросает жребий.

Порядок case в коде на выбор не влияет, в отличие от switch, который проверяет ветки сверху вниз. Поэтому программа с несколькими готовыми каналами может печатать разное при разных запусках. Этот вопрос часто задают на собеседовании: какой case выполнится, если готовы оба? Ответ — любой, выбор случайный.

Ветка default

Ветка default выполняется, когда ни одна операция не готова, и тогда select не ждёт:

GO
ch := make(chan int, 1)
select {
case v := <-ch:
    fmt.Println(v)
default:
    fmt.Println("empty") // empty
}

Разберём, что здесь происходит:

  • канал пуст, поэтому выполняется default: официант заглянул в окно, увидел пустую полку и пошёл заниматься другими делами;
  • если перед select отправить в ch значение, выполнится первый case и напечатает его.

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

Частая ошибка

select стоит внутри цикла for, и цикл пытаются закончить через break, когда закроется канал done:

GO
for {
    select {
    case v := <-nums:
        fmt.Println(v)
    case <-done:
        break // ✗
    }
}

break не выходит из цикла:

  1. break внутри select выходит только из select, как из switch.
  2. Цикл for продолжается.
  3. Закрытый канал done готов к приёму всегда, поэтому цикл крутится вечно.

Исправить можно двумя способами:

  • вынести цикл в отдельную функцию и заменить break на return;
  • поставить перед for метку и написать break с её именем.

Главное

  • select ждёт несколько операций с каналами и выполняет одну готовую.
  • Если готовы несколько, выбор случайный; порядок case в коде не важен.
  • default выполняется, когда ничего не готово, и тогда select не ждёт.
  • break внутри select выходит только из select, а не из цикла вокруг него.

Подпишись, чтобы пройти курс

Курс бесплатный. Войди через Telegram и подпишись на канал — откроются все уроки.

Войти другим способом