Перейти к содержимому
ГоМастер

Поиск по урокам

Найдите урок, курс, термин или раздел

Урок 10 из 25 ~18 мин +50 XP

context.Context: прокидываем данные в хендлеры

chi context — механизм переноса данных вниз по цепочке: middleware проверил токен и обязан донести пользователя до хендлера, не заводя глобальных переменных. Разберём r.Context(), context.WithValue и r.WithContext, непросвечиваемый тип ключа вместо строкового "user" из туториалов, helper-функции с ok-формой и цену паники на отсутствующем значении.

Содержание урока · 10 разделов

В цепочке middleware из пятого урока осталась незакрытая задача: auth-слой проверил заголовок Authorization — а хендлер listNotes всё ещё не знает, кто отправил запрос. Нельзя ни положить пользователя в глобальную переменную — запросы конкурентны, и данные перепутаются между ними, — ни тащить его пятым аргументом через каждый слой: сигнатуры middleware фиксированы стандартной библиотекой. Решение живёт в самих *http.Request: у каждого запроса есть контекст, r.Context(), и chi, и любой net/http-middleware кладут в него то, что относится к этому конкретному запросу. Мостик отсюда в прошлое: chi.URLParam из четвёртого урока сам читает из контекста — роутер складывает разбор пути туда же, под свой ключ. В этом уроке — механика WithValue и WithContext, собственный тип ключа, helper-функции и две аварии, которые случаются, когда ключ оставляют строкой.

r.Context(): контекст внутри http.Request#

r.Context() возвращает context.Context — интерфейс с методами Value, Done, Err и парой служебных; для переноса данных важен Value. Контекст устроен как слоёный пирог: каждый context.WithValue(parent, key, val) возвращает новый контекст с одной парой поверх родителя, а Value ищет сверху вниз — самая свежая обёртка выигрывает. Отсюда ключевой ход middleware: положить значение мало, надо, чтобы его увидели следующие слои. Новый контекст не заменяет старый сам по себе — он доезжает дальше в новой обёртке запроса: r = r.WithContext(ctx), и только затем next.ServeHTTP(w, r). Вызов next со старым r теряет значение молча: компилятор не ругается, тесты с одним middleware зелёные, а хендлер читает пустоту. У контекста запроса есть и встроенная жизнь: сервер гасит его, когда соединение с клиентом закрылось или ServeHTTP вернулся, — но отмене и дедлайнам посвящён отдельный разговор, здесь речь про перенос данных.

Middleware кладёт, хендлер достаёт

GO
func userToCtx(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
                ctx := context.WithValue(r.Context(), "user", "ann")

                next.ServeHTTP(w, r.WithContext(ctx))
        })
}

func profile(w http.ResponseWriter, r *http.Request) {
        user := r.Context().Value("user").(string)

        fmt.Fprintf(w, "привет, %s", user)
}

Схема ровно из README chi: middleware строит новый контекст поверх r.Context(), оборачивает запрос через r.WithContext и передаёт эстафету уже с новым r. Хендлер достаёт значение двумя ходами: r.Context().Value("user") возвращает any, а .(string) превращает его в строку. Пока middleware один и ключ один, код работает — и именно так его пишут в туториалах. Оставаться таким в проде он не должен: строковый ключ и голый type assertion — две мины, на обеих наступаем ниже. Для запуска нужен локальный Go: песочница сайта TCP-сервер не поднимает.

Почему строковый ключ "user" приводит к подмене#

Документация пакета context прямо просит: ключ должен быть сравнимым типом, но не строкой и не другим встроенным типом — иначе коллизии между пакетами неизбежны. Механика подмены простая: Value ищет с самой свежей обёртки вниз, и кто позже положил значение под тот же ключ, того и значение. История из жизни: в сервис поставили middleware из туториала со строковым ключом "user", а полгода спустя — стороннюю библиотеку, чей middleware кладёт под "user" собственный объект сессии. Зарегистрировали её ниже — и хендлеры начали читать чужой объект: тип всё тот же any, данные уже не те. Тесты при этом зелёные: на стенде чужой middleware просто не установлен. Лекарство — непросвечиваемый тип ключа, и chi делает это сам дважды: в ядре ключ маршрутизации — непросвечиваемая структура contextKey, а в middleware/request_id.go — собственный тип ctxKeyRequestID на базе int. Два разных типа — два разных ключа: совпасть они не могут физически.

Свой тип ключа и helper-функции

GO
type ctxKey int

const userKey ctxKey = 0

type User struct {
        Name string
}

func UserToCtx(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
                u := &User{Name: "ann"}

                next.ServeHTTP(w, r.WithContext(
                        context.WithValue(r.Context(), userKey, u),
                ))
        })
}

func UserFromCtx(r *http.Request) *User {
        u, _ := r.Context().Value(userKey).(*User)

        return u
}

type ctxKey int — непросвечиваемый тип: пользовательский ключ больше не равен ни строке "user", ни чужому ctxKey из другого пакета, потому что типы разные. Константа userKey не экспортируется — ключ живёт внутри пакета, а наружу смотрят две функции. UserToCtx кладёт значение и передаёт эстафету с новой обёрткой запроса — ровно тот ход, пропуск которого теряет данные. UserFromCtx инкапсулирует вторую половину: ключ, Value и type assertion с ok-формой. Хендлер читает одну строку: user := UserFromCtx(r).

Типобезопасность: ok-форма обязательна#

Value возвращает any — интерфейс без типа, и без приведения значение бесполезно. Приведение бывает двух форм, и разница между ними — цена продовой аварии. Голая форма user := r.Context().Value(key).(User) паникует, если значения под ключом нет или оно другого типа: запрос мимо auth-ветки, опечатка в ключе, чужой middleware — любое из этого даёт пустоту, и каст nil в User роняет горутину; chi Recoverer из шестого урока превратит панику в 500, но осадок останется. ok-форма u, ok := ....Value(key).(*User) панику не поднимает: false говорит «значения нет», и хендлер решает сам — 401 без пользователя или дефолт для гостя. Helper из прошлого блока делает выбор за вас: он возвращает nil вместо паники, а проверка if user == nil остаётся единственной обязанностью хендлера. Третьей формы нет: любое Value — сначала ok-форма, потом логика.

Проверка: паника на пустом контексте

Значение под ключом в контексте не установлено. Что сделает строка user := r.Context().Value(userKey).(*User)?

Что кладём → кто кладёт → кто читает#

Что кладёмКто кладётКто читает
Пользовательauth-middleware после проверки токенахендлеры: имя в ответ, владельца в лог
RequestIDmiddleware.RequestID из chiлоги и хендлеры через middleware.GetReqID
Разбор путисам роутер chi под ключом RouteCtxKeyваши хендлеры через chi.URLParam
Язык из Accept-LanguageWithValue из chi/middlewareхендлеры локализации ответа
Время на обработкуобёртка с context.WithTimeoutсервисный слой: дедлайн в вызовах базы

Значение иммутабельно: только новая обёртка#

Значение, положенное в контекст, не редактируется: ни SetValue, ни Delete в API нет — пирог наращивают слоями, а не переписывают. Нужно «изменить» данные — строится новый контекст с новой парой поверх существующего: старый слой остаётся на месте и живёт для тех, кто успел его прочитать выше по цепочке. Точнее всего это видно на двух auth-слоях: внешний положил гостя, внутренний после проверки токена положил настоящего пользователя под тем же ключом — хендлер увидит пользователя, потому что Value начинает поиск с верхней обёртки, а слой лога, стоящий выше, всё ещё читает гостя. Это свойство не баг, а следствие иммутабельности: параллельные ветки цепочки не затирают друг друга, гонок на контексте не бывает. Практический вывод: один ключ — один смысл, а не один ключ на пакет; два разных «пользователя» под одним ключом превращают порядок middleware в скрытую бизнес-логику.

Какое имя прочитает хендлер?

Прочитайте код и выберите, что он выведет:

func mwA(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
                ctx := context.WithValue(r.Context(), "user", "ann")
                next.ServeHTTP(w, r.WithContext(ctx))
        })
}

func mwB(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
                ctx := context.WithValue(r.Context(), "user", "bob")
                next.ServeHTTP(w, r.WithContext(ctx))
        })
}

// регистрация: r.Use(mwA, mwB)
// хендлер: fmt.Fprint(w, r.Context().Value("user"))

Отмена и дедлайны — вторая половина контекста#

Перенос данных — половина работы контекста; вторая половина — отмена, и у входящего запроса она встроена: контекст из r.Context() гасится, когда клиент закрыл соединение или ServeHTTP вернулся. Отсюда бесплатная экономика: сервисный слой, который слушает ctx.Done(), перестаёт считать работу, никому не нужную. Второе применение — ограничить время хендлера: middleware оборачивает контекст в context.WithTimeout и передаёт эстафету с ним, и любое долгое обращение к базе получает дедлайн бесплатно. Здесь это одна фраза намеренно: отмена, дерево контекстов и errors.Is на причинах — тема урока про контекст продвинутого курса, где всё разобрано на горутинах. В chi же запомните связку: r.Context() — база, WithValue кладёт данные, WithTimeout ограничивает время, и всё это едет в хендлер через r.WithContext.

Авторизация как middleware с контекстом

GO
func auth(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
                token := r.Header.Get("Authorization")

                u, err := parseToken(token)
                if err != nil {
                        http.Error(w, "нет доступа", http.StatusUnauthorized)
                        return
                }

                next.ServeHTTP(w, r.WithContext(
                        context.WithValue(r.Context(), userKey, u),
                ))
        })
}

r := chi.NewRouter()
r.Use(auth)

r.Get("/profile", func(w http.ResponseWriter, r *http.Request) {
        user := UserFromCtx(r)

        fmt.Fprintf(w, "привет, %s", user.Name)
})

Полная цепочка из практики: auth проверяет токен и на ошибке отвечает 401 сам — ранний ответ из урока про middleware, эстафета не передаётся. Успешная ветка кладёт пользователя под непросвечиваемым ключом и передаёт дальше новую обёртку запроса. Хендлер достаёт пользователя одной строкой через helper: ключ, каст и ok-форма спрятаны внутри UserFromCtx. parseToken здесь заглушка — настоящую проверку токена соберём в уроке про собственный middleware.

Проверка: потерянная обёртка

Middleware построил ctx := context.WithValue(r.Context(), userKey, u), но вызвал next.ServeHTTP(w, r) — со старым r. Что прочитает UserFromCtx в хендлере?

Соберите перенос пользователя

Восстановите запись в контекст, передачу новой обёртки, чтение и проверку ok-формы.

func userToCtx(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ctx := context.(r.Context(), userKey, u) next.ServeHTTP(w, r.(ctx)) }) } func UserFromCtx(r *http.Request) *User { u, ok := r.Context().(userKey).(*User) if ! { return nil } return u }

Пять правил контекста запроса#

  • Ключ — собственный непросвечиваемый тип: type ctxKey int; строковые ключи подменяются чужими middleware.
  • Значение доезжает до хендлера только через r.WithContext(ctx) и next.ServeHTTP(w, r) с новым r.
  • Value возвращает any: приведение — в ok-форме, иначе паника на отсутствующем значении.
  • Значение иммутабельно: «изменение» — новая обёртка поверх; Value читает самую свежую.
  • Helper-функции UserToCtx/UserFromCtx прячут ключ, каст и дефолты — хендлер не знает про механику.

Соберите middleware с контекстом

Расставьте строки middleware: от сигнатуры до передачи эстафеты с новой обёрткой.

Строки

Ваша программа

Нажимайте на строки слева, чтобы собрать программу

Карточки: контекст запроса

Термин

r.Context()

нажмите, чтобы перевернуть
Определение

Контекст текущего запроса из *http.Request: сюда middleware и роутер кладут данные запроса. Гасится сервером при закрытии соединения клиентом.

1 / 6

Пайплайн данных собран: middleware кладёт под непросвечиваемым ключом, хендлер достаёт через helper с ok-формой, и вся механика спрятана в двух функциях на пакет. Следующий шаг — положить там не заглушку, а настоящего пользователя: урок про собственный middleware собирает авторизацию целиком, от проверки заголовка до ролей. А после него — JSON: хендлер научится принимать и отдавать тела запросов, и пользователь из контекста станет автором заметок в REST API на chi. До того проверьте связку руками на живом сервере: добавьте второй middleware со своим типом ключа и убедитесь, что подмена значений больше невозможна, даже если рядом живёт туториальный строковый "user".

Песочница урока

Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.

Вывод

Запустите код, чтобы увидеть вывод.

Коротко: что запомнить

  • r.Context() — контекст запроса: chi кладёт туда разбор пути, middleware — пользователя и id; хендлер достаёт Value.
  • WithValue строит новую обёртку поверх родителя; до хендлера она доезжает только через r.WithContext(ctx) в next.ServeHTTP.
  • Ключ — собственный непросвечиваемый тип (type ctxKey int): строковый "user" подменяется чужим middleware с тем же ключом.
  • Value возвращает any: приведение в ok-форме, иначе паника на nil; helper UserFromCtx прячет каст и возвращает nil.
  • Значение иммутабельно: «изменение» — новая обёртка, Value читает самую свежую — последний записавший выигрывает.
  • Контекст — не чемодан: в нём данные запроса, бизнес-объекты едут аргументами функций.
  • Отмена и дедлайны — вторая половина контекста: r.Context() гасится при закрытии клиента, WithTimeout ограничивает хендлер.

Домашнее задание

Соберите auth-middleware из урока: проверка заголовка Authorization, пользователь кладётся под типом ctxKey, хендлер /profile читает через UserFromCtx и отвечает 401, если там nil. Добавьте второй middleware со строковым ключом "user" и убедитесь, что непросвечиваемый тип защищает данные от коллизии.

Личные заметкиПусто
Хранятся только в вашем браузере (Chi · context.Context: прокидываем данные в хендлеры)

Вопросы и ответы по уроку

Почему нельзя использовать строковые ключи в context.WithValue?

Ключ типа string равен такой же строке из любого другого пакета: чужой middleware с ключом "user" затмит ваше значение — Value вернёт его первым, ведь свежая обёртка выигрывает. Документация context прямо просит определять собственные типы ключей; chi и его middleware так и делают.

Чем r.Context() отличается от chi.URLParam?

Уровнем: URLParam — готовая функция над контекстом, она сама достаёт разбор пути, который роутер положил в r.Context() под своим ключом RouteCtxKey. Свои данные прячутся так же: ключ и каст внутри пакета, наружу — helper с типизированным результатом.

Можно ли изменить значение, уже лежащее в контексте?

Нет: значения иммутабельны, API изменения не существует. Строится новый контекст через WithValue поверх старого — до хендлера доедет самая свежая обёртка, а слои выше по цепочке продолжат читать прежнее значение. Поэтому на порядок middleware ради разных версий данных полагаться нельзя.

Термины урока

Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.

Следующая ступень учебного пути

Redis25 уроков~12 чпрактика25 уроков уже доступны

Redis в Go: кэш, сессии и очереди на go-redis

Курс Redis для Go-разработчиков: кэширование запросов, сессии, счётчики, Pub/Sub, Streams и rate limiter. 25 уроков с примерами кода go-redis v9.

  • Подключаться к Redis из Go и настраивать пул соединений
  • Кэшировать результаты запросов и придумывать TTL
  • Работать с хэшами, списками, множествами и ZSET
  • Строить сессии, счётчики, лидерборды и rate limiter