Deep Engineering
Средний·Опубликовано·30 МИН

Context в Go: отмена только вниз, cancel, который не про отмену, и Value, который дорожает с глубиной

Собеседование идёт лестницей: зачем контекст — почему первым аргументом — что делает cancel и почему его вызывают всегда — чем Canceled отличается от DeadlineExceeded — что класть в Value. Замерено: забытый cancel оставляет 123 байта на потомка навсегда, а Value на двадцати слоях дороже в 18 раз.

Полное техническое изложение

TL;DR

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

Отсюда то, что удивляет. Связь в дереве односторонняя: отмена родителя доходит до всех потомков, отмена потомка не трогает ни родителя, ни соседа — проверено прогоном. cancel — не про отмену результата, а про освобождение: его вызывают всегда, даже при успехе, иначе потомок остаётся в списке родителя. Замерено: десять тысяч потомков без cancel оставляют 123 байта на каждого, с cancel0. И Canceled с DeadlineExceededразные ошибки: после таймаута errors.Is(err, context.Canceled) даёт false, а код, проверяющий только Canceled, не отличит ушедшего клиента от собственного просроченного дедлайна.

Дальше — числа, устройство и границы. Контекст передают первым аргументом, а не хранят в структуре: у каждого вызова свой срок. В Value кладут данные уровня запроса, проходящие через границы пакетов, — трассировку, дедлайн, данные аутентификации, но не параметры функции. Класть дёшево (одно выделение), а доставать дорого: поиск идёт вверх по цепочке родителей, и на двадцати слоях это в 18 раз дороже, чем на нуле, при 2,21 нс у поля структуры. Кратность верна для этого прогона на go1.24.7; правило из неё — доставать один раз на входе, а не помнить число. И context.Background() никогда не отменяется, его Done() равен nil.

Порог входа
Перед уроком достаточно понимать
  • сервер получает запрос, что-то по нему делает и отвечает;
  • работа по одному запросу может идти сразу в нескольких горутинах и уходить в другие сервисы;
  • клиент не ждёт вечно: он может уйти, а у нас может истечь свой срок ожидания.
Заранее знать не нужно
  • WithCancel, WithTimeout, WithValue, Done(), Err() — все разбираются по дороге;
  • чем Canceled отличается от DeadlineExceeded, что такое WithoutCancel и AfterFunc, и наносекунды из замеров.

Что здесь на самом деле спрашивают

Лестница почти всегда такая:

  1. «Зачем контекст?» — разминка: отмена, дедлайн, значения уровня запроса.
  2. «Почему первым аргументом, а не полем структуры?» — здесь начинается содержание.
  3. «Что делает cancel и почему defer cancel() пишут даже при успехе?»
  4. «Чем Canceled отличается от DeadlineExceeded — вопрос, на котором ломается обработка таймаутов.
  5. «Что класть в Value — и почему «всё, что удобно» неверно.
  6. «Как отменить операцию, которая не принимает контекст?» — вопрос на понимание, что контекст ничего не прерывает сам.

Дальше урок идёт по этой лестнице. Спина у неё одна: контекст — это дерево сигналов, а не хранилище и не средство прерывания.

База: зачем работе знать, что запрос уже не нужен

Начнём с задачи, а не с пакета.

Клиент отправил запрос. Сервер принял его и пошёл работать: сходил в базу, полез в соседний сервис, что-то посчитал. А клиент за это время закрыл вкладку и ушёл. Ответ теперь никому не нужен — но работа по нему продолжается: запрос к базе выполняется, соединение занято, память держится. То же самое, если ответа ждать уже поздно: клиент отвалился по своему сроку ожидания, а мы всё ещё считаем.

Работа, которую никто не ждёт, — это не просто лишняя работа. Под нагрузкой она вытесняет нужную: каждая брошенная задача занимает место, которое досталось бы живому запросу.

Значит, до самого низа цепочки вызовов должна как-то доходить новость: это больше не нужно. Сама по себе ни одна функция внизу её не узнает — она не видит ни клиента, ни часов. Новость надо принести, и приносят её тем же путём, каким шёл запрос: сверху вниз по вызовам. Это и есть контекст.

Переносит он три вещи:

  1. отмену — новость, что работа больше не нужна;
  2. срок — момент, после которого она не нужна в любом случае;
  3. значения, привязанные к запросу, — то, что относится к нему целиком и должно быть видно на всём пути.

Первые две — одна и та же новость, только приходит она по разным причинам: кто-то отменил или вышло время. Третья вещь устроена иначе и разбирается отдельно.

И главное: новость сама ничего не останавливает. Она доступна для чтения — а смотреть на неё обязана сама работа. Минимальный образец выглядит так: работа ждёт двух событий сразу — «контекст закончился» и «результат готов», — и что случится раньше, то и определит исход.

GO
select {
case res := <-ch:
    return res, nil
case <-ctx.Done():
    return nil, ctx.Err()
}

Функция, которая так написана, отменяемая. Функция, которая просто считает в цикле и никуда не смотрит, — нет, сколько контекстов ей ни передай.

Этого уже достаточно, чтобы ответить на базовый вопрос собеседования. Дальше — про то, почему новость идёт только вниз и никогда вверх, почему функцию отмены вызывают даже после успеха, чем «отменили» отличается от «вышло время» и во что обходится третья вещь — значения.

Механизм 1: четыре назначения, которые путают

context делает четыре разных дела, и половина путаницы в теме — оттого, что их считают одним:

назначениечем задаётсякто им пользуется
отмена — сообщить, что работа больше не нужнаWithCancelтот, кто читает Done()
срок — ограничить время сверхуWithTimeout, WithDeadlineон же
распространение — донести и то и другое через все вызовыпервый аргумент ctxкаждая функция по пути
значения уровня запросаWithValueтот, кому нужна трассировка

Первые три работают вместе и разбираются механизмами ниже. Четвёртое устроено иначе и стоит отдельно — в разделе «Глубже»; смешивать их не стоит, потому что Value не имеет отношения ни к отмене, ни к сроку.

контракт языкаГарантии пакета: не зависят ни от версии Go, ни от машины. Числа появятся в разделе «Глубже».

И общая рамка, из которой выводится всё остальное: контекст — это дерево сигналов. Не хранилище и не средство прерывания.

Дерево, и связь односторонняя

Три контекста: корень и два потомка. Нажмите «отменить A» и посмотрите, что не изменится:

Прогон bench/gocontext/internals.go печатает то же самое:

отменили A:      A=context canceled  B=nil               корень=nil
отменили корень: A=context canceled  B=context canceled  корень=context canceled

Первая строка — вся суть. Отмена потомка не трогает ни родителя, ни соседа. Вторая — обратное направление: отмена родителя доходит до всех.

Отсюда сразу два практических вывода:

  • Обработчик может отменить свою подзадачу, не роняя весь запрос: сделал ctx, cancel := context.WithCancel(parent), отменил — родитель не заметил.
  • Отменить чужую работу можно, только держа её контекст. Поэтому его и передают явно.

Do not store Contexts inside a struct type; instead, pass a Context explicitly to each function that needs it. The Context should be the first parameter, typically named ctx.

перевод

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

Пакет context

Причина не в стиле. Контекст у каждого вызова свой: у одного запроса — пятисекундный дедлайн, у другого — секундный, третий уже отменён. Поле структуры хранит один контекст на все вызовы, и первый же параллельный запрос получит чужой дедлайн.

Механизм 2: cancel — про освобождение, а не про отмену

Самый частый вопрос на понимание: зачем defer cancel(), если операция и так завершилась успешно?

Документация отвечает прямо:

Failing to call the CancelFunc leaks the child and its children until the parent is canceled or the timer fires.

перевод

Невызов CancelFunc приводит к утечке потомка и его потомков до тех пор, пока не будет отменён родитель или не сработает таймер.

Пакет context

Слово «утечка» стоит понимать буквально, и замер показывает, чего именно. Десять тысяч потомков долгоживущего родителя:

осталось в куче на потомка
без cancel123 байта
с cancel0
наблюдение замераbench/gocontext/internals.go, go1.24.7 linux/amd64. Сто двадцать три байта — то, что осталось на потомка в этом прогоне; на другой версии и на другом типе контекста размер будет свой. Правило отсюда не «сто двадцать три», а «без cancel потомок не собирается, пока жив родитель».

И это не про горутины. Первая редакция замера считала runtime.NumGoroutine() и получила ноль прибавившихся горутин в обоих случаях: WithTimeout заводит таймер рантайма, а не горутину. Утверждение «забытый cancel течёт горутинами» замер не подтвердил.

деталь реализации · Go 1.24Список потомков у родителя и таймер вместо горутины — устройство нынешней реализации пакета. Пакет обещает другое и меньшее: что невызов CancelFunc приводит к утечке потомка. Чем именно течёт — деталь, которая уже менялась; обязанность вызвать cancel от неё не зависит.

Течёт другое: пока cancel не вызван, потомок остаётся в списке потомков родителя, и ни он сам, ни то, на что он ссылается, собрано быть не может. Родитель в реальном коде живёт долго — это контекст сервиса или соединения, — поэтому список растёт вместе с числом забытых cancel.

Отсюда формулировка, которую и хотят услышать: cancel не «отменяет результат», а вычёркивает потомка из списка. Поэтому он безопасен после успеха и поэтому его пишут через defer сразу после создания контекста.

go vet это ловит проверкой lostcancel: она требует, чтобы функция отмены, возвращённая из context.WithCancel, WithTimeout и WithDeadline, была вызвана, — иначе новый контекст остаётся живым, пока не отменят родителя.

Механизм 3: срок — это тоже отмена, но по другой причине

WithTimeout(ctx, d) — это ровно WithDeadline(ctx, time.Now().Add(d)), а дедлайн — это отмена, которую назначили заранее. На шкале видно, что общего и что разного:

Слева и справа происходит одно и то же — закрывается Done(). Отличается только Err(), и вот на этом отличии ломается обработка таймаутов.

Ещё одно свойство шкалы, которое спрашивают: дедлайн потомка не может быть позже родительского. Рантайм возьмёт меньший из двух. Поэтому цепочка WithTimeout на каждом уровне не продлевает срок, а только сокращает его, — и это ровно то поведение, которого от распространения и ждут.

Canceled и DeadlineExceeded — разные ошибки

Вопрос выглядит формальным, а ломается на нём обработка таймаутов.

If Done is closed, Err returns a non-nil error explaining why: DeadlineExceeded if the context's deadline passed, or Canceled if the context was canceled for some other reason.

перевод

Если Done закрыт, Err возвращает ненулевую ошибку с объяснением причины: DeadlineExceeded, если истёк дедлайн контекста, или Canceled, если контекст был отменён по какой-то другой причине.

Пакет context — Err

Прогон подтверждает: после таймаута errors.Is(err, context.DeadlineExceeded) даёт true, а errors.Is(err, context.Canceled)false.

Почему это важно на практике. Две ситуации требуют разного поведения:

  • Клиент ушёл (Canceled) — работать дальше незачем, отвечать некому, логировать как ошибку не надо.
  • Не уложились в срок (DeadlineExceeded) — это уже симптом: либо зависимость медленная, либо дедлайн занижен. Такое пишут в метрики и в алерты.

Код, проверяющий только Canceled, сваливает оба случая в один — и в графиках пропадает именно то, ради чего дедлайны и ставят.

Отдельно про Background и TODO. Оба — пустые контексты, и Done() у них равен nil (чтение из nil-канала блокируется навсегда, поэтому такой контекст никогда не «сработает»). Разница между ними чисто сигнальная: Background — корень в main и в тестах, TODO — пометка «здесь потом будет настоящий контекст». Инструменты статического анализа умеют искать TODO.

Механизм 4: контекст ничего не прерывает

Самое частое заблуждение темы, и его стоит уметь сформулировать.

Отмена контекста — это сигнал, а не прерывание. ctx.Done() закрывается, и всё. Горутина, которая его не читает, продолжит работать как ни в чём не бывало. Никакого механизма «убить операцию» в языке нет — как нет и способа убить горутину.

Отсюда правило: контекст работает только там, где его читают. Три места, где это происходит:

GO
// 1. Явная проверка в цикле.
for _, item := range items {
    select {
    case <-ctx.Done():
        return ctx.Err()
    default:
    }
    process(item)
}
 
// 2. Ожидание с отменой.
select {
case res := <-ch:
    return res, nil
case <-ctx.Done():
    return nil, ctx.Err()
}
 
// 3. Библиотека, которая принимает контекст сама.
rows, err := db.QueryContext(ctx, query)
resp, err := client.Do(req.WithContext(ctx))

Третий пункт — самый важный на практике: если библиотека не принимает контекст, отменить её вызов нечем. Обёртка вида «запустить в горутине и вернуться по таймауту» отменяет только ожидание, а сама операция продолжает работать и держать ресурсы. Это законный приём, но называть его отменой нельзя, и на собеседовании эту разницу проверяют.

Механизм 5: как это выглядит на сквозном пути запроса

Всё, что разобрано выше, стоит один раз увидеть целиком — на пути, который проходит настоящий запрос:

Три сценария на этом пути, и каждый спрашивают.

Клиент отвалился. http.Server закрывает Done() контекста запроса; сигнал доходит вниз до QueryContext, и тот прерывает запрос в базе. Ошибка у обработчика — Canceled. Отвечать некому, логировать как сбой не надо.

Не уложились в две секунды. Сработал таймер обработчика, Done() закрыт, Err()DeadlineExceeded. Это симптом: либо база медленная, либо срок занижен. Такое пишут в метрики.

Обработчик закончил раньше срока. defer cancel() вычёркивает потомка из списка родителя. Ничего не «отменяется» — освобождается место, и именно поэтому вызов обязателен и при успехе.

И главное, что видно только на целой картинке: контекст никто не «применяет» — его передают. Работу прерывает не он, а QueryContext, который его читает. Уберите из этой цепочки одно звено, передающее ctx дальше, — и всё, что ниже, перестанет отменяться, хотя код будет выглядеть правильным.

Глубже: Value — дёшево класть, дорого доставать

Правило из документации звучит строже, чем его обычно пересказывают:

Use context Values only for request-scoped data that transits processes and APIs, not for passing optional parameters to functions.

перевод

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

Пакет context

Два условия сразу: данные уровня запроса и проходящие через границы. Идентификатор трассировки, данные аутентификации, идентификатор арендатора — да. Настройки функции, зависимости, конфигурация — нет: это параметры, и они должны быть видны в сигнатуре.

И вторая половина, про цену. Класть дёшево — одно выделение. Доставать дороже, и цена растёт с глубиной, потому что Value идёт вверх по цепочке родителей:

слоёв над значениемValue
06,13 нс×1,0
530,30×4,9
1051,80×8,4
20110,58×18,0
поле структуры2,21для сравнения
наблюдение замераbench/gocontext/internals.go, go1.24.7 linux/amd64. Кратность верна для этой лестницы глубин на этой машине: у вас будут свои наносекунды и своя ×N. Что от машины не зависит — знак: цена растёт с глубиной, потому что поиск идёт по цепочке, а не по карте.

Последняя строка — мера того, насколько это не замена полю. Практический вывод: значения из контекста достают один раз на входе в обработчик, а дальше передают явно. Читать ctx.Value в горячем цикле — это платить восемнадцать раз там, где хватило бы одного.

И тут стоит удержаться от обратного перегиба. Восемнадцать — не свойство Value, а свойство двадцати слоёв: столько же слоёв над значением надо ещё суметь набрать. Правило пишется не от кратности, а от её причины: раз поиск идёт вверх по цепочке, время чтения зависит от глубины — значит, читают на входе, где глубина известна и мала, а не в цикле, где она какая получится.

И ещё одно, про типобезопасность: ключ должен быть своим неэкспортируемым типом, а не строкой.

GO
type ctxKey string
const userKey ctxKey = "user"

Со строковым ключом два независимых пакета могут случайно взять одно и то же имя и молча перезаписать друг друга. Со своим типом это невозможно: типы разные, даже если строки совпадают.

Как отвечать на собеседовании

Короткий ответ: контекст переносит по дереву вызовов отмену, срок и значения, привязанные к запросу — чтобы работу, которая больше никому не нужна, можно было остановить, а не доводить до конца. Отмена при этом сигнал, а не прерывание: она действует только там, где Done() читают. И связь в дереве односторонняя — вниз отмена идёт, вверх и вбок нет.

Этого достаточно, чтобы ответить верно. Дальше — то, что добавляют, если собеседник копает.

Если интервьюер копает глубже

На «зачем контекст» отвечайте тремя словами и сразу четвёртым. «Отмена, дедлайн, значения уровня запроса — и всё это дерево с односторонней связью».

Про первый аргумент называйте причину, а не правило. «У каждого вызова свой дедлайн; поле структуры хранит один контекст на все вызовы, и параллельный запрос получит чужой».

Про cancel скажите «освобождает, а не отменяет». «Он вычёркивает потомка из списка потомков родителя; без него потомок висит там, пока жив родитель — у меня выходило 123 байта на потомка».

Про Canceled и DeadlineExceeded назовите разное поведение. «Клиент ушёл — работать незачем; не уложились в срок — это симптом, его пишут в метрики. Код, проверяющий только Canceled, сваливает их в одно».

Про отмену скажите, что это сигнал. «Контекст ничего не прерывает: горутина, которая не читает Done(), продолжит работать. Если библиотека не принимает контекст, отменить её вызов нечем».

Про Value назовите оба условия и цену. «Данные уровня запроса, проходящие через границы; ключ — свой неэкспортируемый тип. И доставать дорого: на двадцати слоях в восемнадцать раз дороже, чем на одном, поэтому достают один раз на входе».

Дальше спросят

Спросят дальше

Как дописать данные, когда запрос уже отменён?

Короткий ответ

Для этого в Go 1.21 появился context.WithoutCancel: он возвращает копию контекста, которая не отменяется вместе с исходным, но сохраняет его значения. Типичный случай — записать метрику или аудит-лог после того, как клиент отвалился.

Важно не перепутать это с «игнорировать отмену». Значения (трассировка, идентификатор арендатора) сохраняются, а дедлайн — нет, поэтому такой операции обычно ставят свой, короткий: context.WithTimeout(context.WithoutCancel(ctx), time.Second).

Спросят дальше

Чем WithTimeout отличается от WithDeadline?

Короткий ответ

Ничем, кроме формы записи: WithTimeout(ctx, d) — это ровно WithDeadline(ctx, time.Now().Add(d)). Оба возвращают контекст, который отменится сам, и оба требуют cancel.

Разница в том, что удобнее выражать. WithDeadline естественнее, когда срок задан снаружи и его надо пронести через несколько вызовов: дедлайн у потомка не может быть позже родительского — рантайм возьмёт меньший. Поэтому цепочка WithTimeout на каждом уровне не «продлевает» срок, а только сокращает его.

Спросят дальше

Можно ли передавать nil вместо контекста?

Короткий ответ

Нельзя: документация говорит про это прямо, а на практике nil-контекст даст панику при первом же обращении к Done() или Value(). Если контекста ещё нет — это context.TODO().

Разница между TODO и Background не техническая: оба пустые. TODO — это пометка для человека и для линтера: «здесь должен быть настоящий контекст, но пока непонятно, откуда его взять». В готовом коде TODO быть не должно.

Спросят дальше

Безопасен ли контекст для нескольких горутин?

Короткий ответ

Да, и это прямо сказано в блоге Go: контексты безопасны для одновременного использования несколькими горутинами. Причина в том, что контекст неизменяемWithValue и WithCancel не меняют существующий, а создают новый узел дерева.

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

Спросят дальше

Что делает context.AfterFunc?

Короткий ответ

Регистрирует функцию, которая выполнится при отмене контекста, — появился в Go 1.21. До него ту же задачу решали горутиной с select { case <-ctx.Done(): ... }, и это стоило горутины на каждое ожидание.

Практический случай: закрыть соединение или освободить ресурс, когда запрос отменён, не заводя ради этого отдельную горутину. Возвращает функцию отмены регистрации — её тоже надо вызывать, иначе получается та же утечка, что и с забытым cancel.

Спросят дальше

Контекст в структуре — совсем нельзя?

Короткий ответ

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

Признак, по которому отличают допустимый случай от недопустимого: если у структуры несколько методов и они вызываются в разное время разными вызывающими, контекст в поле — ошибка. Если структура живёт ровно столько, сколько операция, — это ещё один способ записать тот же самый «первый аргумент».

Частые заблуждения

Утверждение

отмена контекста прерывает выполнение

На самом деле

Отмена — это сигнал, а не прерывание: Done() закрывается, и всё. Горутина, которая его не читает, продолжит работать как ни в чём не бывало. Если библиотека не принимает контекст, отменить её вызов нечем — обёртка «запустить в горутине и уйти по таймауту» отменяет только ожидание.

Утверждение

cancel нужен только чтобы отменить работу

На самом деле

Он вычёркивает потомка из списка потомков родителя, поэтому вызывается всегда — и при успехе тоже. Замерено: десять тысяч потомков без cancel оставляют 123 байта на каждого, с cancel — 0. Отсюда идиома defer cancel() сразу после создания.

Утверждение

забытый cancel течёт горутинами

На самом деле

Замер этого не подтверждает: число горутин не изменилось ни в одном случае — WithTimeout заводит таймер рантайма, а не горутину. Течёт память: потомок остаётся в списке родителя, и всё, на что он ссылается, удерживается вместе с ним.

Утверждение

context.Canceled покрывает и таймаут

На самом деле

Нет: после таймаута errors.Is(err, context.Canceled) даёт false, а DeadlineExceeded — true. Это разные случаи и разное поведение: клиент ушёл — работать незачем; не уложились в срок — это симптом для метрик.

Утверждение

отмена потомка отменит и родителя

На самом деле

Связь односторонняя: прогон печатает, что после отмены потомка у родителя и у соседа Err() остался nil. Вниз отмена идёт, вверх и вбок — нет. Именно поэтому подзадачу можно отменить, не роняя весь запрос.

Утверждение

контекст можно хранить в структуре, так удобнее

На самом деле

У каждого вызова свой дедлайн и своя отмена, а поле структуры хранит один контекст на все вызовы — первый же параллельный запрос получит чужой. Исключение одно: структура, которая сама представляет одну операцию со своим сроком жизни.

Утверждение

в context.Value удобно класть что угодно

На самом деле

Документация требует двух условий сразу: данные уровня запроса и проходящие через границы процессов и интерфейсов. Зависимости и настройки — это параметры, и они должны быть видны в сигнатуре. А ключ обязан быть своим неэкспортируемым типом: со строковым два пакета молча перезапишут друг друга.

Утверждение

Value дешёвый, можно читать в цикле

На самом деле

Дёшево класть — одно выделение. Доставать дорого и тем дороже, чем глубже: 6,13 нс на нулевой глубине против 110,58 на двадцати слоях, при 2,21 нс у обычного поля структуры. Значения достают один раз на входе в обработчик и дальше передают явно.

Практика

Две задачи. Сначала ответьте, потом сверьтесь с настоящим выводом: в обеих правильный ответ взят из прогона скрипта, а не назначен.

Практика · что напечатает

Дойдёт ли отмена родителя до потомка, затронет ли отмена потомка родителя, какую ошибку даст таймаут — и видно ли значение потомка родителю?
<-child.Done()
fmt.Println(errors.Is(child.Err(), context.Canceled))

_, cancelC2 := context.WithCancel(p2)
cancelC2()
fmt.Println(p2.Err() == nil)

<-tctx.Done()
fmt.Println(errors.Is(tctx.Err(), context.DeadlineExceeded))
fmt.Println(errors.Is(tctx.Err(), context.Canceled))

fmt.Println(withUser.Value(userKey))
fmt.Println(base.Value(userKey))

Практика · оцените

Достать одно и то же значение из контекста. Во сколько раз это дороже, когда над ним десять слоёв WithValue, чем когда один?
раз

Проверка знаний

Вопрос 1 из 6

Горутина выполняет длинный цикл и не читает ctx.Done(). Контекст отменили. Что произойдёт?

Источники и что читать дальше

4 ИСТОЧНИКА

  1. Пакет contextОфициальная документация. Правила, которые чаще всего цитируют неточно. Про место контекста: «Do not store Contexts inside a struct type; instead, pass a Context explicitly to each function that needs it. The Context should be the first parameter, typically named ctx» (Не храните контексты внутри структурного типа; вместо этого передавайте контекст явно в каждую функцию, которой он нужен. Контекст должен быть первым параметром, обычно называемым ctx). Про cancel: «Failing to call the CancelFunc leaks the child and its children until the parent is canceled or the timer fires» (Невызов CancelFunc приводит к утечке потомка и его потомков до тех пор, пока не будет отменён родитель или не сработает таймер). Про Value: «Use context Values only for request-scoped data that transits processes and APIs, not for passing optional parameters to functions» (Используйте значения контекста только для данных уровня запроса, проходящих через процессы и интерфейсы, а не для передачи необязательных параметров функциям).https://pkg.go.dev/context
  2. Пакет context — Err, Canceled, DeadlineExceededОфициальная документация. Разница, из-за которой ломается обработка таймаутов: «If Done is not yet closed, Err returns nil. If Done is closed, Err returns a non-nil error explaining why: DeadlineExceeded if the context's deadline passed, or Canceled if the context was canceled for some other reason» (Если Done ещё не закрыт, Err возвращает nil. Если Done закрыт, Err возвращает ненулевую ошибку с объяснением причины: DeadlineExceeded, если истёк дедлайн контекста, или Canceled, если контекст был отменён по какой-то другой причине). И про Background: «Background returns a non-nil, empty Context. It is never canceled, has no values, and has no deadline» (Background возвращает ненулевой пустой контекст. Он никогда не отменяется, не содержит значений и не имеет дедлайна).https://pkg.go.dev/context#Context
  3. Go Concurrency Patterns: Context — блог GoОфициальная документация. Исходное обоснование модели: «At Google, we require that Go programmers pass a Context parameter as the first argument to every function on the call path between incoming and outgoing requests» (В Google мы требуем, чтобы программисты на Go передавали параметр Context первым аргументом каждой функции на пути вызовов между входящим и исходящим запросами). Там же про смысл отмены: «Contexts are safe for simultaneous use by multiple goroutines» (Контексты безопасны для одновременного использования несколькими горутинами).https://go.dev/blog/context
  4. Заметки к выпуску Go 1.21 — context.WithoutCancel и AfterFuncОфициальная документация. Дополнения, о которых спрашивают на позициях повыше: «WithoutCancel returns a copy of a context that is not canceled when the original is canceled» (WithoutCancel возвращает копию контекста, которая не отменяется при отмене исходного) и «AfterFunc registers a function to run when a context is canceled» (AfterFunc регистрирует функцию, которая будет выполнена при отмене контекста). Первое закрывает частый практический вопрос: как дописать данные в базу, когда запрос уже отменён.https://go.dev/doc/go1.21