Advertisement
  1. Code
  2. Go

Контекстное программирование в Go

by
Read Time:8 minsLanguages:

Russian (Pусский) translation by Masha Kolesnikova (you can also view the original English article)

Программы Go, которые выполняют несколько параллельных вычислений в goroutines, должны управлять их временем жизни. Беглые программы могут попасть в бесконечные циклы, заблокировать другие ожидающие программы или просто занять слишком много времени. В идеале вы должны быть в состоянии отменить гоу-рутины или сделать так, чтобы они вышли по окончании.

Введение в контент-программирование. Go 1.7 представил пакет контекста, который предоставляет именно эти возможности, а также возможность связывать произвольные значения с контекстом, который путешествует с выполнением запросов и позволяет осуществлять внешнюю связь и передачу информации.

В этом уроке вы узнаете все тонкости контекста в Go, когда и как их использовать, и как не злоупотреблять ими.

Кому нужен контекст?

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

  • Она отделяет основные параметры вычислений от рабочих параметров.
  • Она кодифицирует общие эксплуатационные аспекты и способы их передачи через границы.
  • Она обеспечивает стандартный механизм добавления внеполосной информации без изменения сигнатур.

Контекстный интерфейс

Вот весь интерфейс Context:

В следующих разделах объясняется назначение каждого метода.

Метод Deadline()

Deadline возвращает время, когда работа, выполненная от имени этого контекста, должна быть отменена. Крайний срок возвращает ok==false, если не установлен крайний срок. Последовательные звонки в Deadline возвращают те же результаты.

Метод Done()

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

  • Функция context.WithCancel() обеспечивает закрытие канала Done при вызове cancel.
  • Функция context.WithDeadline() организует закрытие канала Done по истечении крайнего срока.
  • Функция context.WithTimeout() обеспечивает закрытие канала Done по истечении времени ожидания.

Done может быть использовано в операторах выбора:

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

Метод Err()

Err() возвращает nil, пока открыт канал Done. Он возвращает Canceled, если контекст был отменен, или DeadlineExceeded, если истек крайний срок контекста или истекло время ожидания. После закрытия Done последующие вызовы Err() возвращают одно и то же значение. Вот определения:

Метод Value()

Value возвращает значение, связанное с этим контекстом для ключа, или ноль, если никакое значение не связано с ключом. Последовательные вызовы Value с одним и тем же ключом возвращают один и тот же результат.

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

Ключ идентифицирует конкретное значение в контексте. Функции, которые хотят хранить значения в Context, обычно выделяют ключ в глобальной переменной и используют этот ключ в качестве аргумента для context.WithValue() и Context.Value(). Ключ может быть любого типа, который поддерживает равенство.

Контекстная область

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

Контексты образуют иерархию. Вы начинаете с context.Background() или context.TODO(). Каждый раз, когда вы вызываете WithCancel(), WithDeadline() или WithTimeout(), вы создаете производный контекст и получаете функцию отмены. Важно то, что когда родительский контекст отменяется или истекает, все его производные контексты тоже.

Вы должны использовать context.Background() в функции main(), init() и тестах. Вы должны использовать context.TODO(), если вы не уверены, какой контекст использовать.

Обратите внимание, что Background и TODO не подлежат отмене.

Сроки, тайм-ауты и отмены

Как вы помните, WithDeadline() и WithTimeout() возвращают контексты, которые отменяются автоматически, в то время как WithCancel() возвращает контекст и должны быть явно отменены. Все они возвращают функцию отмены, поэтому даже если тайм-аут/крайний срок еще не истек, вы все равно можете отменить любой производный контекст.

Давайте рассмотрим пример. Во-первых, здесь есть функция contextDemo() с именем и контекстом. Она работает в бесконечном цикле, выводя на консоль свое имя и крайний срок контекста, если таковой имеется. Затем он просто секунду спит.

Функция main создает три контекста:

  • timeoutContext с трехсекундным таймаутом
  • не истекающий cancelContext
  • deadlineContext, который получен из cancelContext, с крайним сроком четыре часа

Затем он запускает функцию contextDemo в виде трех программ. Все запускаются одновременно и каждую секунду печатают свои сообщения.

Затем основная функция ожидает отмены программы с тайм-аутом timeoutCancel чтением из канала Done() (блокируется, пока не закроется). Когда время ожидания истекает через три секунды, main() вызывает метод cancelFunc(), которая отменяет выполнение процедуры с помощью cancelContext, а также последнюю процедуру с производным контекстом предельного срока, равным четырем часам.

Вот вывод:

Передача значений в контексте

Вы можете прикрепить значения к контексту, используя функцию WithValue(). Обратите внимание, что возвращается исходный контекст, а не производный контекст. Вы можете прочитать значения из контекста, используя метод Value(). Давайте изменим нашу демонстрационную функцию, чтобы получить ее имя из контекста вместо передачи ее в качестве параметра:

А давайте изменим функцию main, добавив имя через WithValue():

Вывод остается прежним. См. раздел «Лучшие практики», где приведены рекомендации по правильному использованию значений контекста.

Лучшие практики

Несколько лучших практик появились вокруг значений контекста:

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

Контекст HTTP-запроса

Одним из наиболее полезных вариантов использования для контекстов является передача информации вместе с HTTP-запросом. Эта информация может включать идентификатор запроса, учетные данные для аутентификации и многое другое. В Go 1.7 стандартный пакет net/http воспользовался тем, что пакет контекста стал «стандартизированным», и добавил поддержку контекста непосредственно в объект запроса:

Теперь можно прикрепить идентификатор запроса из заголовков к конечному обработчику стандартным способом. Функция обработчика WithRequestID() извлекает идентификатор запроса из заголовка «X-Request-ID» и генерирует новый контекст с идентификатором запроса из существующего контекста, который он использует. Затем он передает его следующему обработчику в цепочке. Открытая функция GetRequestID() предоставляет доступ к обработчикам, которые могут быть определены в других пакетах.

Заключение

Контекстное программирование обеспечивает стандартный и хорошо поддерживаемый способ решения двух распространенных проблем: управление временем жизни подпрограмм и передача внеполосной информации по цепочке функций.

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

Advertisement
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.