Пакет context в Go
English version
Если заглянуть в src директорию SDK, то из всего списка пакетов, context - самое неоднозначное название. Пакет context перекочевал в Go 1.7 из golang.org/x/net/context (обратите внимание на путь: net здесь не случаен, этот пакет использовался для прекращения работы HTTP-запросов) Бред Фитцпатрик - разработчик Go с 2010 до 2020 - в своем proposal указывал на возможное использование контекста не только для отмены HTTP-запросов, но и для остановки работы TCP-диалера (установка TCP-соединения), остановки дочернего процесса ОС, запущенного через os/exec, отмены выполнения SQL-запроса и т.д. Когда вы видите фукнцию/метод (далее просто функция), принимающую context.Context, вы должны это воспринимать как предложение управлять отменой её выполнения.
Как можно решить задачу отмены процедуры без пакета context? Канал done в качестве первого аргумента функции - это один из самых очевидных способов реализации отмены. Отличные примеры можно найти также в статье https://go.dev/blog/pipelines. Запустить Go Playground
А вот реализация отмены по истечению таймаута: Запустить Go Playground
Для решения задачи отмены процедуры в Go, SDK предлагает пакет context. Пакет context - это микрофреймворк, предназначенный для коммуникации с процедурой, чаще через сигнал отмены её выполнения.
Мне не удалось найти упоминаний выбора именно такого названия для пакетов. Если изучить первую публикацию о Context Object паттерне (Context Object, A Design Pattern for Efficient Information Sharing across Multiple System Layers, 2005 год), то становятся ясны предпосылки для такого названия пакета. Цитата из публикации:
This pattern provides an efficient, and application transparent way of sharing information between different layers in a software system.Пакет context в Go также служит для передачи информации между слоями/уровнями приложения, но в Go акцент больше смещен на информацию об отмене выполнения процедуры. Задачу коммуникации с процедурой можно решить также иным способом, через менеджер (Peter Sommerlad, “The Manager Design Pattern", Pattern Languages of Program Design 3, Addison-Wesley, 1998.), но это решение обладает серьезным недостатком: для конкурентной работы с менеджером нужна синхронизация между рутинами, например, через Mutex'ы.
Немного о том как устроен контекст в Go. Пакет context предоставляет Context интерфейс, плюс несколько его имплементаций, которые можно объединять в цепочку. Это удобно, если у вас есть несколько причин для отмены функции (вызов cancel, таймаут, дедлайн) и отмена должна случится при наступлении любой из них. Любой пользовательский контекст является дочерним к другому контексту, который называется родительским. Только пустой контекст (context.Background() или context.TODO()) не имеет родителя. Эта иерархичность важна, т.к. влияет на то, какие из контекстов цепочки будут отменены при отмене родителя. Запустить Go Playground
Отмена родительского контекста влечет отмену всех дочерних контекстов. Запустить Go Playground
Отмена дочернего контекста не отменяет родительский. Запустить Go Playground
Важно понимать, что сам по себе контекст не отменяет выполнение функции. Он лишь предоставляет механизм для отмены, который нужно реализовать самостоятельно. Поэтому важно заглядывать в исходный код сторонных пакетов и проверять как на самом деле используется контекст. Запустить Go Playground
"go vet" ругается, если не вызвать функцию отмены контекста. Запустить Go Playground
А что случится, если не вызвать функцию отмены контекста? В случае timerCtx запускается таймер и по истечению времени запускается Go-рутина, в которой вызывается cancel-функция. Только после этого происходит сборка мусора (т.е. освобождение ресурсов). В случае cancelCtx вызов cancel-функции очищает map дочерних контекстов. Если вы используете кастомные контексты, то можете получить утечку go-рутин, т.к. только в случае кастомного контекста запускается go-рутина, ждущая отмены родительского или дочернего контекста. Запустить Go Playground
Если вы планируете использовать контекст только для передачи данных, то лучше сделать специальную структуру для этого. В противном случае вы теряете все преимущества статической типизации. Значения в контексте хранятся в виде any. Может быть полезна следующая рекомендация: контекст только для отмены, значения в контексте только в случае обоснованной необходимости.
Как организовано хранение значений в cancelCtx? Каждый следующий контекст хранит ссылку на предыдущий контекст. Контекст отмены (context.cancelCtx), используемый под капотом также контекстом отмены по дедлайну/таймауту (timerCtx), хранит ссылки на все дочерние контексты, чтобы иметь возможность их отмены при отмене родительского контекста. Запустить Go Playground
Рассмотрим использование контекста в HTTP-сервере из пакета net.http. Контекст здесь служит для передачи самого HTTP-сервера и TCP-адреса, на котором он запущен. С помощью своих middleware вы можете также обогатить контекст дополнительными данными. Т.к. базовый контекст серавера (Server.BaseContext) передаваемый в каждый запрос можно переопределить (по умолчанию используется context.Background()), то можно контекст использовать, например, для передачи сигнала о скорой перезагрузке сервера (см. готовую имплементацию в SDK signal.NotifyContext). Запустить Go Playground
Помимо middleware, дополнить контекст данными новосозданного TCP-соединения можно через ConnContext функцию сервера. Запустить Go Playground
Некоторые пакеты предоставляют готовые решения по отмене по таймауту. Например, можно использовать http.TimeoutHandler, если нужно ограничить время ответа хендлера. Сам хендлер продолжит работу до окончания работы программы, если в нем специально не проверять завершен ли контекст запроса. Запустить Go Playground
Несколько напутственных рекомендаций:
- Убедитесь, что не можете решить задачу без использования Go-рутин. Если можете обойтись без Go-рутин, то можете обойтись без context-пакета.
- Если вы не планируете отменять процедуру, то передавайте пустой контекст (context.Background() или context.TODO()).
- Если сторонняя функция принимает context.Context, проверьте как он используется, заглянув в её исходный код. Пакет context предоставляет механизмы для отмены, но не саму отмену процедуры.