👨💻 Контекст неизменяемый, а память нет — 24 июля 2026 г. в 10:27:01.104
👨💻 Контекст неизменяемый, а память нет Правило знают все. context.WithValue ничего не меняет, а создаёт новую обёртку, поэтому контекст безопасно шарить между горутинами. Учебники забывают сказать главное. Иммутабельны обёртки, а объект в корне цепочки может переиспользоваться фреймворком через sync.Pool. Тогда горутина, пережившая запрос, держит указатель на память, которая вот‑вот достанется другому запросу. Хендлер запускает фоновую задачу и сразу возвращает ответ. Линтер ругается на отмену контекста, и со времён Go 1.21 его успокаивают одной строкой: go s.processAsync(context.WithoutCancel(ctx), id) Для net/http это правильно. Там контекст это обычная аллокация в куче. Но линтер слеп к архитектуре и не знает, что под context.Context может стоять пулящаяся структура. ❓ Где можно споткнуться В Fiber и всём на fasthttp c.Context() возвращает *fasthttp.RequestCtx. Пулящийся объект и есть ваш контекст, и после отправки ответа fasthttp сбрасывает его и кладёт обратно в пул. Gin пулит *gin.Context, поэтому передавать его в горутину нельзя (для снимка есть c.Copy()), а c.Request.Context() безопасен. Echo свой контекст пулит, но context.Context он не реализует, так что задеть гонку сложнее. Дальше воркер просыпается после I/O, зовёт ctx.Value("user_id") и читает память, которую перезаписывают данными другого пользователя. И вот что коварно. WithoutCancel снимает только отмену, а Value() по‑прежнему делегирует пулящемуся родителю. То есть фикс, который позеленил линтер, ровно и удерживает протухший указатель. ➡️ Как правильно Если задаче ничего не нужно из запроса, постройте свежий корень: ctx := context.Background() Если нужны trace‑id или user‑id, вытащите их как простые значения, пока горутина запроса ещё жива, и положите в свежий контекст: userID := c.Locals("user_id").(string) traceID := c.Locals("trace_id").(string) go func(userID, traceID string) { ctx := context.WithValue(context.Background(), userCtxKey, userID) ctx = context.WithValue(ctx, traceCtxKey, traceID) h.svc.ProcessAsync(ctx, userID) }(userID, traceID) Вся безопасность держится на том, что значения сняты до возврата из хендлера. Логику стоит вынести в хелпер вроде ctxutil.Detach(ctx). Из штатных лазеек помогают c.Copy() у Gin и c.UserContext() у Fiber. Линтер видит синтаксис, но не жизненный цикл памяти. Если горутина переживает запрос, не давайте ей контекст запроса ни в какой обёртке. Снимайте значения как простые данные, стройте свежий корень и держите -race в CI.

