Перейти к основному содержимому

Управление временем жизни (context)

Представьте, что вы делаете заказ в ресторане. Официант (основной процесс) идет к повару (горутина 1), повар просит мясника (горутина 2) отрезать мясо. Если вы вдруг передумали и ушли из ресторана, официант должен сказать всем: "Стоп! Заказ отменен, не тратьте время".

context.Context — это как раз этот "сигнал отмены", который передается по цепочке вызовов.

1. Зачем нужен context?

У него две основные задачи:

  1. Отмена (Cancellation): Остановить выполнение горутин, если результат работы больше не нужен (например, таймаут или отмена пользователем).
  2. Передача данных (Request Scoping): Передать через цепочку функций какую-то информацию, актуальную для одного конкретного запроса (например, ID пользователя, токен авторизации или ID трассировки).

2. Как работает context?

Контекст — это иммутабельный объект (неизменяемый). Когда вы хотите "добавить" к контексту что-то новое (например, таймаут), вы не меняете старый, а создаете новый, который "держится" за старый.

Типы контекстов:

  • context.Background(): Базовый контекст. Используется в функции main или как корневой контекст для тестов.
  • context.WithCancel(parent): Возвращает новый контекст и функцию cancel(). Вызов cancel() пошлет сигнал всем горутинам, использующим этот контекст.
  • context.WithTimeout(parent, duration): То же самое, что WithCancel, но отмена произойдет автоматически через указанное время.

3. Пример: Таймаут для долгой задачи

Представьте, что вы делаете запрос к базе данных, который не должен длиться дольше 2 секунд.

func processData(ctx context.Context) {
select {
case <-time.After(5 * time.Second): // Имитация долгой работы
fmt.Println("Задача выполнена!")
case <-ctx.Done(): // Сигнал, если время вышло или мы сами вызвали cancel
fmt.Println("Задача отменена:", ctx.Err())
}
}

func main() {
// Создаем контекст с таймаутом в 2 секунды
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel() // Всегда вызывайте cancel, чтобы освободить ресурсы!

processData(ctx)
}

4. Как правильно использовать context

Существует несколько «золотых правил» использования контекста в Go:

  1. Контекст первым аргументом: Если ваша функция принимает контекст, он всегда должен быть первым аргументом. func DoSomething(ctx context.Context, arg1 int) error { ... }
  2. Не храните контекст в структурах: Контекст — это про запрос, а не про объект. Он должен "течь" сквозь функции.
  3. Используйте context.TODO(): Если вы еще не знаете, какой контекст передать, используйте TODO() — это сигнал другим разработчикам, что здесь должен быть реальный контекст.
  4. defer cancel(): При использовании WithCancel или WithTimeout всегда вызывайте cancel сразу после создания (через defer). Это освобождает ресурсы памяти, связанные с контекстом.

5. Передача значений (Request-Scoped Values)

Иногда нужно передать ID пользователя через 10 слоев функций, не пробрасывая его в каждом аргументе.

// Установка значения
ctx := context.WithValue(context.Background(), "userID", 123)

// Чтение значения
if val, ok := ctx.Value("userID").(int); ok {
fmt.Println("ID пользователя:", val)
}

Важно: Не используйте WithValue для передачи обычных аргументов функций! Это "плохой тон", так как это делает код запутанным и лишает его статической типизации. Используйте это только для технических данных (логирование, авторизация, трейсинг).


Итого: когда применять?

  • Если ваша функция выполняет сетевой запрос или длительные вычисления — она обязана принимать context.Context.
  • Если вам нужно ограничить время выполнения задачи — используйте context.WithTimeout.
  • Если вы пишете web-серверnet/http уже предоставляет контекст запроса, который нужно передавать дальше по цепочке функций.