context.Context: отмена и таймаут по цепочке вызовов
Содержание курса
ctx.Done() и ctx.Err(): остановка doWork через select при отмене и таймауте
ctx.Done() возвращает канал типа <-chan struct{}. Здесь важен один нюанс: для неотменяемого контекста — например, context.Background() — этот метод возвращает nil, и такой канал никогда не закрывается. Для контекстов, созданных через WithCancel или WithTimeout, канал ненулевой: когда контекст отменяется, он закрывается, и операция receive по нему становится постоянно готовой.
Типичная рабочая функция использует select для одновременного ожидания двух событий:
func doWork(ctx context.Context, workCh <-chan string) error {
for {
select {
case <-ctx.Done():
return ctx.Err()
case item, ok := <-workCh:
if !ok {
return nil
}
fmt.Println("processing:", item)
}
}
}
Горутина заблокирована до тех пор, пока хотя бы один из каналов не станет готов — это не busy loop. После закрытия ctx.Done() ветка отмены становится постоянно готовой и сигнал уже не теряется. Однако приоритета у неё нет: если одновременно готов и workCh, select выбирает одну из веток псевдослучайно. Обработка отмены гарантированно произойдёт при одном из последующих проходов цикла — но не обязательно немедленно. Именно поэтому длительную работу внутри рабочей ветки стоит ограничивать или также делать отменяемой.
Посмотрим на оба сценария:
func main() {
workCh := make(chan string)
// Сценарий 1: явная отмена
ctx1, cancel1 := context.WithCancel(context.Background())
defer cancel1()
go func() {
time.Sleep(100 * time.Millisecond)
cancel1()
}()
err := doWork(ctx1, workCh)
fmt.Println(err) // context canceled
// Сценарий 2: таймаут
ctx2, cancel2 := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancel2()
err = doWork(ctx2, workCh)
fmt.Println(err) // context deadline exceeded
}
В первом случае горутина явно вызывает cancel1() — doWork в итоге получает context.Canceled. Во втором никто cancel2 явно не зовёт, но через 100 мс таймер срабатывает сам — doWork получает context.DeadlineExceeded. Значение ctx.Err() фиксируется в момент отмены и уже не меняется: функция, которая проверит его чуть позже, увидит ровно ту же причину остановки.
