Урок по треку Go 6 мин

context в Go

Отмена и дедлайны, дерево контекстов, WithCancelCause, WithoutCancel, AfterFunc, значения в контексте.

context.Context переносит через цепочку вызовов три вещи: сигнал отмены, дедлайн и значения уровня запроса. Почти любая функция, которая ходит в сеть или базу, принимает ctx первым аргументом.

Интерфейс

type Context interface {
    Deadline() (deadline time.Time, ok bool)
    Done() <-chan struct{}   // закрывается при отмене
    Err() error              // Canceled или DeadlineExceeded после отмены
    Value(key any) any
}

Дерево контекстов

Корень: context.Background() (или context.TODO(), когда ещё не решили). От него строятся потомки:

ctx, cancel := context.WithTimeout(parent, 2*time.Second)
defer cancel() // обязательно, иначе ресурсы таймера живут до дедлайна
  • WithCancel: отмена вручную.
  • WithTimeout, WithDeadline: отмена по времени. Итоговый дедлайн не позже родительского.
  • WithValue: добавляет значение.
  • Go 1.20 WithCancelCause и context.Cause: отмена с причиной.
  • Go 1.21 WithoutCancel (потомок не отменяется вместе с родителем, например для фоновой записи аудита после ответа), AfterFunc (вызвать функцию после отмены), WithDeadlineCause, WithTimeoutCause.

Отмена идёт сверху вниз: отменили родителя, отменены все потомки. Отмена потомка не трогает родителя.

Как слушать отмену

func worker(ctx context.Context, jobs <-chan Job) error {
    for {
        select {
        case <-ctx.Done():
            return ctx.Err()
        case j, ok := <-jobs:
            if !ok { return nil }
            process(j)
        }
    }
}

Контекст ничего не прерывает принудительно. Он только закрывает канал Done, а код должен сам его проверять. Библиотеки (net/http, database/sql, драйверы) делают это за вас, если передать им ctx.

В HTTP

r.Context() отменяется, когда клиент разорвал соединение или сервер завершает обработку. Передавайте его в запросы к базе: если пользователь ушёл, запрос к базе прервётся. Исходящий запрос: http.NewRequestWithContext(ctx, ...).

Значения

type ctxKey struct{}
ctx = context.WithValue(ctx, ctxKey{}, requestID)
id, _ := ctx.Value(ctxKey{}).(string)
  • Ключ своего неэкспортируемого типа, чтобы не было коллизий между пакетами (не строка).
  • Только данные уровня запроса: request id, данные авторизации, трассировка. Не передавайте через контекст обязательные параметры функции, логгер по умолчанию или соединение с базой.
  • Поиск значения идёт вверх по цепочке, это O(глубины).

Правила

  • ctx первый аргумент, имя ctx, не храните его в полях структуры (исключения редки).
  • Не передавайте nil, используйте context.TODO().
  • Всегда вызывайте cancel, даже если операция завершилась успешно (go vet предупреждает о потерянной cancel).
  • Контекст безопасен для одновременного использования из многих горутин.

Частые ошибки

  • Забыть defer cancel().
  • Запустить фоновую работу с r.Context() и удивляться отмене после ответа: нужен WithoutCancel или свой контекст.
  • Не проверять ctx.Done() в долгом цикле.
  • Класть в контекст всё подряд.

Что спросят на собесе

  • Зачем нужен context и что в нём лежит.
  • Как распространяется отмена в дереве.
  • Почему обязательно вызывать cancel.
  • Что можно и нельзя класть в WithValue, какого типа делать ключ.
  • Что появилось в 1.20-1.21: Cause, WithoutCancel, AfterFunc.

Закрепите за 5 минут

После урока в тренажёре 3-5 проверочных вопросов, дальше модуль уходит в повторения.