А вы часто читаете реализацию стандартной библиотеки своего любимого языка? Меня зовут Константин Соколов, и мы с Сергеем Мачульскисом, моим коллегой из бэкенд-разработки в Positive Technologies хотим с вами поделиться вдохновением. Давайте вместе посмотрим на пакет context с последними обновлениями. На наш взгляд, он идеально выражает философию языка Go! Образцовый интерфейс, постоянное развитие пакета и использование самых распространенных приемов Go — все это говорит о том, что наш материал будет полезен не только новичкам, но и знатокам. Пакет context появился как x/net/context в 2014 году и быстро обрел популярность. В 2016 году его добавили в стандартную библиотеку Go 1.7. С тех пор практически ни одно приложение на Go не обходится без его использования, потому что пакет ощутимо упрощает многие задачи. Контекстом называют интерфейс Context из пакета context. Код почти дословно иллюстрирует, для чего используется контекст: чтобы устанавливать дедлайн исполнения блока кода; оповещать об окончании исполнения блока кода; узнавать причину отмены контекста; получать значения по ключу. Интерфейса достаточно для использования в любых местах, где код может «зависнуть». Это любое сетевое взаимодействие, а также долгие задачи, не выходящие за рамки процесса ОС. Кроме того, контекст можно использовать для неявной передачи параметров в функции. Все методы context.Context могут вызываться одновременно из нескольких горутин. В Go принято передавать контекст в виде первого аргумента функции. Обычно его называют ctx. Вот пример функции из пакета net/http: Корневой контекст невозможно отменить, он не имеет дедлайна и не содержит значений. Обычно корневой контекст создается в main() приложения и оборачивается в производный контекст с отменой, который затем передается в функции. Когда контекст отменяется в main(), отменяется и код в глубине стека вызовов. Это помогает реализовать корректное завершение работы процесса. Есть две функции получения корневого контекста: context.Background()   context.TODO()   Реализованы они очень просто:   Оба контекста по сути ничем не отличаются, но context.TODO() используется только на время проектирования или эксперимента. context.TODO() пригождается, когда нужно вызвать функцию, требующую context.Context в качестве аргумента, но в данный момент контекст не проброшен сверху по стеку вызовов. Тогда нормально временно использовать context.TODO(), а позже пробросить полноценный контекст, который можно отменять. Все контексты, за исключением корневых, создаются на основе родительских. Новый контекст расширяет родительский новой информацией. Например, в новый контекст можно положить ключ со значением, при этом не модифицируя родительский контекст. Для создания новых контекстов в пакете предусмотрены функции WithCancel, WithDeadline, WithTimeout, WithValue и WithoutCancel. WithCancel возвращает новый контекст и функцию отмены. Новый контекст хранит ссылку на родительский контекст. Функцию отмены нужно вызывать на том же уровне, где она была создана. Это называется отменой вручную. Вызывать функцию отмены в другом месте в Go считается антипаттерном, поскольку это может приводить к утечке незакрытых контекстов. Вызов cancel закрывает канал, возвращаемый методом контекста Done(). Также канал Done() дочернего контекста закрывается, если закрывается канал Done() родительского контекста. Автоматическую отмену контекста по времени можно сделать при помощи функций WithDeadline и WithTimeout. Они позволяют ограничить максимальное время выполнения блока кода. Пример:   Здесь функция context.WithTimeout принимает два аргумента: существующий контекст и time.Duration. Контекст будет отменен по истечении таймаута автоматически, но также обязательно отменять контекст вручную при помощи defer, чтобы не допустить утечки ресурсов (памяти и горутин). Допустимо делать отмену контекста несколько раз. После первого вызова все последующие игнорируются. Функция context.WithDeadline отличается тем, что вторым аргументом принимает time.Time. Если указать уже прошедший момент времени, то будет создан сразу отмененный контекст. Метод Deadline экземпляра типа context.Context помогает узнать, в какой момент произойдет отмена. Метод возвращает time.Time и ok. Deadline возвращает ok=false, когда дедлайн не установлен. В тех случаях, когда невозможно передать данные явным образом через несколько промежуточных функций, можно использовать WithValue. WithValue добавляет пару «ключ — значение» в контекст. Функция принимает три аргумента: родительский контекст, ключ и значение. Во избежание коллизий ключ не должен быть одним из встроенных типов и должен поддерживать операторы сравнения == и !=.  Получить значение можно методом Value. Он принимает ключ и возвращает его значение. Если ключ не был найден, возвращается nil. Пример:   Узнавать об отмене контекста помогают методы Done и Err экземпляра context.Context. Done представляет канал типа struct{}, который закрывается при отмене контекста и возвращает нулевое значение (nil) при последующей попытке чтения из него. Метод Err может возвращать три варианта значений: nil — если контекст активен; context.Canceled типа error в случае явной отмены контекста; context.DeadlineExceeded в случае отмены по истечении времени. В Go 1.21 добавлена функция WithoutCancel. WithoutCancel возвращает копию родительского контекста, которая не будет отменена при отмене родительского контекста. WithoutCancel можно использовать в следующих довольно частых сценариях: при обработке операций rollback/cleanup в контексте какого-либо события (например, обработки HTTP-запроса). Операции должны продолжиться, несмотря на отмену самого события (например, клиент API ушел или наступил таймаут операции) при обработке длительных операций, запущенных каким-либо событием (например, HTTP-запросом). Событие можно пометить как обработанное, не дожидаясь завершения длительной операции. Полученный контекст не возвращает Deadline или Err. Значение канала Done равно nil. Поэтому чтение из него приведет к блокировке программы навсегда. В Go 1.21 добавлена новая функция AfterFunc. Иногда нужно отменить блокирующую функцию, которая поддерживает прерывание своей работы, но не через механизм отмены контекста. До Go 1.21 это делалось сложно и неэффективно. Например, когда нужно было прервать чтение или запись в net.Conn или ожидание на sync.Cond, это делалось через запуск отдельной горутины. Горутина ждала, когда отменится контекст, а затем прерывала блокирующую функцию. Запуск новых горутин достаточно эффективен, но это большие накладные расходы, которые не имеют смысла, если операция очень легкая и отрабатывает за миллисекунды. AfterFunc позволяет зарегистрировать функцию, которая вызывается при отмене контекста. AfterFunc принимает функцию f, которая будет выполнена после того, как контекст будет отменен (в том числе после истечения таймаута). Если контекст уже завершен, функция будет запущена сразу же. Выполнение f происходит в отдельной горутине. Множественные вызовы AfterFunc выполняются независимо друг от друга. AfterFunc возвращает стоп-функцию. Вызов стоп-функции разрывает ассоциацию f с контекстом. Стоп-функция возвращает false, если контекст уже в состоянии Done и f уже была запущена, или если f уже была остановлена. Стоп-функция возвращает true, если f была прервана. Стоп-функция не дожидается, пока f будет завершена, поэтому если нужно контролировать состояние, то лучше явно коммуницировать с f. Рантайм Go с каждой версией становится лучше. Иногда для оптимизации рантайма требуются изменения в стандартной библиотеке. Пакет context эти оптимизации не обошли стороной. До Go 1.21 функции Background и TODO возвращали заранее заготовленные экземпляры контекста. В версии 1.21 контексты Background и TODO стали разными типами, встраивающими пустой контекст. То есть теперь нет глобальных переменных и инициализация рантайма проходит чуть-чуть быстрее. Do not store Contexts inside a struct type.   Документация пакета context настоятельно рекомендует не хранить контекст внутри структур. Вместо этого нужно явно передавать контекст в функции отдельным аргументом. Хранение контекста в структуре ведет к путанице и непредсказуемому поведению. Например, если в структуре несколько методов используют контекст, то невозможно выставить дедлайн или выполнить отмену только для одного из методов. Код, использующий такую структуру, сложнее отлаживать и тестировать в первую очередь потому, что для аккуратного использования методов структуры разработчику нужно будет прочитать все реализации методов структуры. А то, что нужно погружаться в реализацию при использовании каких-то методов может означать только одно: при проектировании нарушены базовые принципы абстракции и инкапсуляции. При явной передаче контекста в функции разработчики могут в полной мере воспользоваться его расширяемостью, устанавливать для каждого вызова дедлайн, отмену и значения ключей. Use context Values only for request-scoped data that transits processes and APIs, not for passing optional parameters to functions.    Одно из преимуществ использования WithValue — это возможность неявной передачи данных через несколько промежуточных функций. Однако эту функциональность стоит использовать взвешенно. И вот почему.  Если передавать в контексте опциональные аргументы функций, это усложняет их документирование. В комментарии к функции будет некорректно писать, что функция ожидает в контексте определенный ключ. Такие вещи должны проверяться компилятором, а не программистом при написании кода. Для опциональных аргументов лучше использовать обычные аргументы функций с нулевыми значениями. В контексте правильно передавать только значения, которые пересекают границы процессов и «живут» от начала и до конца обработки задачи. Например, идентификатор пользователя или запроса хорошо ложится в контекст. В любой функции, использующейся по ходу обработки запроса, идентификатор можно залогировать или передать во внешнюю систему. Логгер тоже хорошо ложится в контекст. Это особенно актуально для структурированного логирования. В пакете Context в версии 1.21 есть следующие типы, реализующие интерфейс Context: emptyCtx backgroundCtx todoCtx cancelCtx timerCtx afterFuncCtx valueCtx withoutCancelCtx stopCtx Большинство типов ассоциированы с функциями, создающими контекст. Пакет context активно использует механизм встраивания (embedding) в качестве альтернативы традиционному механизму наследования. Например, backgroundCtx реализован как type backgroundCtx struct{ emptyCtx } Тип emptyCtx встроен в тип backgroundCtx. У emptyCtx реализованы методы Deadline(), Done(), Err(), Value(), а backgroundCtx реализует лишь свой метод String. При этом все публичные методы emptyCtx автоматически доступны при обращении к backgroundCtx. Рассмотрим реализацию AfterFunc: AfterFuncCtx хранит объект sync.Once. a.once.Do() вызывает func() { stopped = true } только в том случае, если она вызывается в первый раз. Do() потокобезопасна, то есть Do() можно одновременно вызвать из нескольких горутин. Только одна из горутин выполнит f. Так сделано для того, чтобы при отмене контекста из разных горутин AfterFunc выполнилась только один раз. В пакете context есть интересная глобальная переменная closedchan. Этот код создает глобальный закрытый канал на старте приложения, импортирующего пакет context. Далее закрытый канал используется для изменения и проверки состояния Done при отмене контекста. Для начала ознакомимся с реализацией cancelCtx (такую структуру создает WithCancel): Вот что происходит при вызове cancelCtx.Done() (схематично, в реализации еще есть мьютекс): При вызове cancel() используем closedchan: Также используем closedchan для проверки того, что родительский контекст закрыт: Такая реализация позволяет не создавать лишние каналы и немного оптимизирует отмену контекстов. Каждый контекст связан со своим родительским контекстом, поэтому при отмене родительского контекста все дочерние будут также отменены. Функция propagateCancel отменяет дочерние контексты при отмене родительского. Она вызывается в функции отмены контекста вручную (withCancel), отмены по дедлайну (WithDeadline) и AfterFunc. Если родительский контекст уже отменен, то она запускает отмену всех дочерних контекстов при помощи функции cancel. Функция cancel закрывает Done-канал, отменяет дочерние контексты и удаляет связь между родительским и дочерним контекстами. Также при первой отмене она устанавливает причину отмены. Важно отметить, что при отмене дочернего контекста его родительский контекст не будет отменен, и родительскому контексту никак не сообщается об отмене дочерних контекстов. Получить значение контекста можно при помощи метода Value. Он принимает ключ и возвращает его значение. Если ключ не существует в текущем контексте, то поиск будет рекурсивно подниматься по дереву контекстов, пока не будет найден корневой контекст. Если значение не найдено, то вернется nil. Особенность реализации здесь в том, что поиск значения происходит во всех родительских контекстах, — это значительно упрощает его получение. При этом неважно, насколько глубоко вложенным является контекст с искомым значением. Следует избегать ситуаций с созданием нескольких контекстов с одинаковыми ключами с помощью WithValue(). В таких ситуациях ctx.Value() будет возвращать ближайшее к ctx значение, так как значения в родительских контекстах будут не перезаписаны, а скрыты, и, следовательно, это может стать причиной ошибки. Кроме того, стоит отметить, что при большой вложенности линейный перебор связного списка контекстов может быть довольно затратным. Поэтому существует рекомендация не использовать контекст как хранилище данных. В том числе есть известная проблема, что, когда цепочка вложенных контекстов достигает определенного размера, это провоцирует довольно дорогостоящую операцию по изменению размера стека горутин, что может отражаться на производительности.   Для менее затратного получения значений из большой цепочки вложенных контекстов может быть использована библиотека zerosnake0/goctx.   Итак, мы разобрали пакет context и теперь предлагаем вам оценить свои знания.  Определяем функцию:   Определяем значение тайм-аута и меняем вызов в main: Создаем контекст с отменой по тайм-ауту:   Запускаем функцию в горутине и по выполнении сообщаем в канал done: Ожидаем выполнение executeTask или отмены контекста по таймауту И вот как выглядит итоговое решение:  Допустим, ты хочешь научиться мастерски владеть инструментом. Стратегий достижения мастерства множество, но одна из самых простых — это учиться у действующих мастеров. Отлично, если это по совместительству мастера, которые создали инструмент. Еще лучше, если это мастера, которые создали его под себя. Да, скорее всего, в последнем случае инструмент не будет выглядеть модно, но зато он будет качественно решать поставленные перед ним задачи. Go — инструмент, который для себя создали профессионалы. Поэтому реализацию языка и стандартной библиотеки можно и нужно читать. Не потому, что можно использовать какие-нибудь особенности реализации, а потому, что она хорошо написана и там действительно есть чему поучиться.