С акцентом на параллелизм

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

  • известно, что операция отменена
  • отменить, если операция не завершена к определенному времени
  • тайм-аут, если операция выполняется слишком долго
  • передавать общие данные в виде хранилища ключей/значений

Мы рассмотрим каждый из них и их общие варианты использования ниже.

Пустой контекст

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

Фоновый контекст

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

ctx := context.Background()

Это создаст пустой контекст, который можно передавать и расширять функциями отмены, тайм-аутами/сроками или значениями (см. следующие разделы). Этот контекст никогда не отменяется и не имеет крайнего срока или связанных значений. Обычно это используется для контекста или тестов верхнего уровня.

Контекст TODO

Точно так же мы можем получить контекст TODO со следующим:

ctx := context.TODO()

Это возвращает пустой контекст, который никогда не отменяется и не имеет крайнего срока или связанных значений. Этот контекст используется, когда неясно, какой контекст использовать, или функция ожидает передачи контекста, который еще не доступен. По сути, он предоставляет средства для компиляции и передачи информации, но помечает этот контекст как неполный (аналогично комментарию // TODO).

Вы можете увидеть, как оба могут быть сгенерированы и использованы в (несколько бесполезном) примере ниже:

Отменяемый контекст

Отмена — это самый простой способ завершить путь вычислений, который зависит от контекста — он позволяет вызывающему коду сигнализировать всему нижестоящему потоку, что вычисления больше не нужны. Настало время очистить ресурсы и выйти. Это может быть полезно при обработке длительных запросов (например, остановить обработку, если клиент отключается от сервера), и это распространенный способ корректного завершения работы приложения — когда приложение получает сигнал остановки (например, SIGTERM) он может отменить контекст верхнего уровня, и все, что получено из этого контекста, будет знать, что нужно очистить и прекратить действия.

Мы генерируем контексты с отменой следующим образом:

ctx, cancelFunc := WithCancel(parent Context)

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

В следующем примере мы генерируем отменяемый контекст и проверяем, был ли он отменен, проверяя готовый канал через Done():

Когда мы запустим приведенный выше код, мы увидим вывод:

background long running task launched
background long running task still going
going to cancel background task
long running task bailed because context cancelled
some time has elapsed after cancelling
Program exited.

Если вы хотите поэкспериментировать с этим самостоятельно, вы можете скопировать/вставить этот пример (или все, что следует за ним) в Игровую площадку.

Контекст с крайним сроком

Контексты с крайними сроками часто используются при выполнении запросов к внешнему ресурсу, состояние которого вызывающая сторона может не знать или контролировать, например к базе данных или API. Они говорят: «Я хочу, чтобы что-то произошло в определенное время или к определенному времени, или же залог». Канал Done контекста закроется, когда истечет крайний срок, означая для любого нижестоящего кода, что они должны прекратить делать то, что они делают.

Они принимают форму

ctx, cancelFunc := WithDeadline(parent Context, deadline time.Time)

где parent — это контекст, на котором это основано, а deadline — это время, когда вы хотите, чтобы это произошло. Обратите внимание, что если parent имеет крайний срок раньше, чем deadline. Этот контекст будет иметь тот же крайний срок, что и parent.

В следующем примере показано, как его можно использовать для завершения длительной задачи:

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

bailed because context deadline passed
completed before context deadline passed
Program exited.

longRunningTask() также демонстрирует простой пример того, как код, который принимает контекст, может обрабатывать отмененный контекст, используя select на разных каналах.

Контекст с тайм-аутом

Контексты с таймаутами — удобная функция, похожая на контексты с дедлайнами. Они называются так:

ctx, cancelFunc := WithTimeout(parent Context, dur time.Duration)

Под капотом звонят:

WithDeadline(parent, time.Now().Add(timeout))

Но эй, мы примем любое упрощение, которое мы можем получить!

По сути, когда мы передаем контекст с тайм-аутом, мы говорим, что готовы подождать, пока не пройдет timeout времени, и если вызов не завершен, он должен завершиться и выйти из строя. Это снижает вероятность того, что наш код может бесконечно (для некоторого значения «infinite») зависать в ожидании завершения какого-либо вызова, а не выдавать ошибку и продолжать выполнять свою работу.

В следующем примере показано, как это можно использовать для завершения длительной задачи:

Мы получаем следующий вывод:

bailed because context timed out
completed before context timed out
Program exited.

Мы видим, насколько это похоже на приведенный выше пример крайнего срока. Выбирая между WithDeadline и WithTimeout, просто выберите тот, который лучше соответствует коду вызова. Если вы хотите, чтобы это произошло к определенному времени, выберите крайний срок! Если вы хотите, чтобы это произошло в течение определенного времени, выберите тайм-аут!

Контекст со значениями

Контексты также позволяют передавать значения в области запроса. Чтобы присоединить пару ключ/значение к контексту, вызовите:

ctx := WithValue(parent Context, key, val any)

Это возвращает копию parent, где значение, связанное с key, равно val.

Предоставленный key должен быть сопоставимым и не должен быть встроенным типом (например, строкой или целым числом), чтобы избежать конфликтов между различными пакетами, использующими контекст. Если несколько пакетов используют один и тот же тип key с одним и тем же значением, они могут столкнуться при попытке установить/получить val. Вместо этого key следует определить как собственный тип с правильной областью действия для установки и получения val. Таким образом, даже если несколько областей определяют ключ с одним и тем же базовым значением, они не будут конфликтовать в контексте. Следующий пример иллюстрирует это:

Здесь мы определяем тип ключа (keyType1) с областью действия main(). Он построен со значением "foo". Теперь мы назначаем пару ключ/значение "foo"="bar" в контексте. Если мы попытаемся получить ключ типа keyType1 (со значением "foo"), мы получим "bar", как и ожидалось.

Теперь мы можем определить новую функцию с собственным типом ключа. Если мы назначим то же значение ключа этому новому типу ключа ("foo"), мы можем попытаться получить значение из контекста:

found a value for key type 1: bar
no value for key type 2
Program exited.

Как мы видим, поскольку у нас нет доступа к правильному ключу type, мы не можем получить доступ к значению, даже если базовое значение для ключа такое же. Обратите внимание, что мы могли бы также присвоить новое значение в tryAnotherKeyType(), и оно было бы недоступно в main() из-за того же несоответствия типа ключа, которое мы обсуждали до сих пор. Так мы избегаем коллизий ключ/значение между пакетами (или любой единицей области видимости).

Этот инструмент следует использовать для передачи данных в области запроса между процессами и API, а не только для необязательных параметров функций, поскольку это может запутать код. Также обратите внимание, что значения в контексте должны быть потокобезопасными, то есть они могут использоваться одновременно несколькими горутинами.

Производные контексты

Мы упоминали об этом ранее в этой статье, но производные контексты могут быть очень полезными. Подводя итог, можно сказать, что дочерние контексты регулируются их родителями — они не могут иметь более длительный тайм-аут/крайний срок, чем их родители, и они не могут отменить своих родителей. Они также наследуют любые значения, хранящиеся у их родителей. Однако родители могут отказаться от своих детей.

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

Таким образом, мы можем завершить все приложение (путем отмены основного контекста), обработку запроса (путем отмены первого производного контекста) или вызов хранилища (путем отмены второго производного контекста).

Подведение итогов

Обратите внимание, что со всем вышеперечисленным лучше всего вызывать CancelFunc контекста, когда он больше не нужен для освобождения ресурсов (если это контекст, который включает функцию отмены). В приведенных выше примерах я обычно игнорировал функцию отмены для простоты, но важно эффективно использовать ресурсы для долго работающих приложений. Контексты обычно передаются в качестве первого параметра функции, поэтому имейте это в виду при разработке собственного кода, использующего контекст!

Надеюсь, это послужило трамплином для изучения контекстов в Go и улучшения ваших навыков разработки приложений. Это должно начать обретать смысл сейчас, когда вы видите функции, которые принимают контексты (такие как обращение к базе данных или API), и вы можете начать понимать, как вы, как клиент, можете использовать контексты для создания собственного кода. более предсказуем и эффективен.

Мы рассмотрели основную механику различных контекстных утилит в Go и обсудили, когда и как их можно использовать. Вы можете использовать этот инструмент по-разному, поэтому относитесь к нему скорее как к стартовому звонку, чем к финишу.

Спасибо за чтение! Следите за новостями.