Конкурентность: горутины, каналы и контекст

context.Context: отмена и таймаут по цепочке вызовов

Содержание курса

В прошлом уроке stopCh — закрытый канал — служил сигналом остановки горутин. Он прекрасно справлялся с одной функцией, но как только вызовов становится несколько, нужно вручную прокидывать stopCh по всей цепочке и синхронизировать закрытие. context.Context решает именно эту задачу: отмена распространяется по дереву вызовов автоматически, а тип и соглашения стандартизированы.

context.Background() как корень и соглашение о передаче ctx первым аргументом

context.Background() — это отправная точка. Он не имеет дедлайна, никогда не отменяется и не несёт никаких значений. Его единственная роль — быть корнем, от которого вы потом получите дочерние контексты через WithCancel или WithTimeout.

В main это выглядит так:

package main

import (
    "context"
    "fmt"
)

func doWork(ctx context.Context, name string) {
    fmt.Println("working:", name)
}

func main() {
    ctx := context.Background()
    doWork(ctx, "task-1")
}

Обратите внимание на сигнатуру doWork: ctx context.Context идёт первым параметром, тип — интерфейс context.Context, а не какой-либо конкретный тип. Это устоявшееся соглашение Go: если функция принимает контекст, он обычно передаётся первым параметром и называется ctx. Стандартная библиотека и большинство популярных пакетов придерживаются этого правила, поэтому функцию можно вызвать с любым контекстом — фоновым, с таймаутом или тестовым.

Почему ctx не хранят в поле структуры? Контекст описывает время жизни конкретного вызова или запроса: у каждого HTTP-запроса свой ctx, который отменяется, когда клиент отключился. Если поместить контекст в поле, он теряет эту привязку: непонятно, когда его отменять, кто владеет его жизненным циклом и откуда пришёл сигнал отмены. Это делает код непрозрачным и трудным для тестирования. Передача через аргумент явно показывает: «этот контекст действует только во время данного вызова».