Модуль
1Горутины: основы2Каналы3select и таймауты← вы здесь4sync: Mutex, Once, atomic
Урок 3~12 минут

select и таймауты

select: switch для каналов

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

go
select {
case v := <-ch1:
    fmt.Println("получили из ch1:", v)
case v := <-ch2:
    fmt.Println("получили из ch2:", v)
case ch3 <- 99:
    fmt.Println("отправили в ch3")
}

Правила:

  • Если готов один кейс — выполняется он
  • Если готовы несколько — выбирается случайный (защита от starvation)
  • Если ни один не готов — select блокируется до первого готового

Таймауты через time.After

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

go
func fetchWithTimeout(url string) (string, error) {
    result := make(chan string, 1)
 
    go func() {
        // симулируем медленный запрос
        time.Sleep(2 * time.Second)
        result <- "данные с " + url
    }()
 
    select {
    case data := <-result:
        return data, nil
    case <-time.After(1 * time.Second):
        return "", fmt.Errorf("таймаут: %s не ответил за 1s", url)
    }
}

Важно: time.After создаёт таймер, который не отменяется при досрочном выходе из select. В горячих путях лучше time.NewTimer с явным Stop():

go
timer := time.NewTimer(1 * time.Second)
defer timer.Stop()  // предотвращаем утечку таймера
 
select {
case data := <-result:
    return data, nil
case <-timer.C:
    return "", errors.New("таймаут")
}

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

default в select выполняется если ни один кейс не готов — делает операцию неблокирующей:

go
// Неблокирующее чтение
select {
case v := <-ch:
    fmt.Println("прочитали:", v)
default:
    fmt.Println("канал пуст")
}
 
// Неблокирующая отправка
select {
case ch <- value:
    fmt.Println("отправили")
default:
    fmt.Println("канал заполнен, пропускаем")
}

Паттерн «проверить без блокировки»:

go
func tryReceive(ch <-chan int) (int, bool) {
    select {
    case v := <-ch:
        return v, true
    default:
        return 0, false
    }
}

Done channel: сигнал отмены

Классический паттерн для остановки горутин — канал-сигнал done:

go
func worker(done <-chan struct{}) {
    for {
        select {
        case <-done:
            fmt.Println("получили сигнал остановки")
            return
        default:
            // выполняем работу
            fmt.Println("работаем...")
            time.Sleep(500 * time.Millisecond)
        }
    }
}
 
func main() {
    done := make(chan struct{})
 
    go worker(done)
 
    time.Sleep(2 * time.Second)
    close(done)  // сигнализируем всем горутинам
 
    time.Sleep(100 * time.Millisecond)
    fmt.Println("завершено")
}

chan struct{} — пустой канал, не несёт данных, только сигнал. close(done) разблокирует все горутины, читающие из него — в отличие от отправки одного значения.


context: стандартный способ отмены

Пакет context — современная замена done channel для отмены операций с дедлайнами:

go
import "context"
 
func worker(ctx context.Context) error {
    for {
        select {
        case <-ctx.Done():
            return ctx.Err()  // context.Canceled или context.DeadlineExceeded
        default:
            // работа
            time.Sleep(100 * time.Millisecond)
        }
    }
}
 
func main() {
    // Отмена по таймауту
    ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
    defer cancel()  // всегда вызывать! освобождает ресурсы
 
    if err := worker(ctx); err != nil {
        fmt.Println("worker остановлен:", err)
    }
}

context.WithCancel — ручная отмена:

go
ctx, cancel := context.WithCancel(context.Background())
 
go worker(ctx)
go worker(ctx)
go worker(ctx)
 
time.Sleep(2 * time.Second)
cancel()  // останавливает все 3 горутины одновременно

context передаётся первым аргументом по всей цепочке вызовов — это конвенция Go.


Паттерн: periodic ticker

time.Tick для периодических задач:

go
func monitor(ctx context.Context) {
    ticker := time.NewTicker(1 * time.Second)
    defer ticker.Stop()
 
    for {
        select {
        case t := <-ticker.C:
            fmt.Println("тик в", t.Format("15:04:05"))
            // делаем периодическую работу
        case <-ctx.Done():
            fmt.Println("монитор остановлен")
            return
        }
    }
}

Разница между time.Tick и time.NewTicker:

  • time.Tick — удобно, но таймер нельзя остановить (утечка в горутинах)
  • time.NewTicker — явный Stop(), всегда используй в горутинах

Паттерн: или-канал (or-channel)

Объединение нескольких done-каналов в один:

go
func or(channels ...<-chan struct{}) <-chan struct{} {
    switch len(channels) {
    case 0:
        return nil
    case 1:
        return channels[0]
    }
 
    orDone := make(chan struct{})
    go func() {
        defer close(orDone)
        switch len(channels) {
        case 2:
            select {
            case <-channels[0]:
            case <-channels[1]:
            }
        default:
            select {
            case <-channels[0]:
            case <-channels[1]:
            case <-channels[2]:
            case <-or(append(channels[3:], orDone)...):
            }
        }
    }()
    return orDone
}

Сигнализирует когда любой из каналов закрыт — полезно для «первый ответивший побеждает».

select — это switch для каналов. Если готовы несколько кейсов — один выбирается случайно. default делает select неблокирующим. time.After создаёт канал, который получает значение через указанное время.
ch1
--
EMPTY
ch2
--
EMPTY
ch3
--
EMPTY
// Go: select statement select { case v := <-ch1: case v := <-ch2: case v := <-ch3: fmt.Println("ch1:", v) fmt.Println("ch2:", v) fmt.Println("ch3:", v) }
🎯
Миссия 1 из 4
Что делает default в select?