Перейти к содержимому

Для чего используется контекст context в go

  • автор:

Контекст

Перевод статьи от Sameer Ajmani Go Concurrency Patterns: Context из официального блога Go.

Введение

В Go сервере каждый входящий запрос обрабатывается в отдельной go-рутине. Обработчики запросов часто стартуют свои рутины для доступа к базам данным, сторонним АПИ и т.д. Всем этим рутинам, как правило, нужен доступ к спецефичным для этого запроса параметрам, таким как идентификатор пользователя, токены авторизации и время завершения запроса(request’s deadline). Кроме того, эти go-рутины, работающие в рамках этого запроса, должны быстро завершаться и освобождать ресурсы.

В Google мы разработали пакет context , который упрощает передачу запросозависимых значений, сигналов отмены, и время завершения для всех go-рутин запущенных в рамках этого запроса. Сам пакет доступен тут code.google.com/p/go.net/context. В этой статье описано как использовать этот пакет и показаны несколько примеров работы с ним.

Контекст(Context)

Самая важная часть пакета context это тип Context :

// Контекст предоставляет механизм дедлайнов, сигнал отмены, и доступ к запросозависимым значениям. // Эти методы безопасны для одновременного использования в разных go-рутинах. type Context interface < // Done возвращает канал, который закрывается когда Context отменяется // или по таймауту. Done() // Err объясняет почему контекст был отменен, после того как закрылся канал Done. Err() error // Deadline возвращает время когда этот Context будет отменен. Deadline() (deadline time.Time, ok bool) // Value возвращает значение ассоциированное с ключем или nil. Value(key interface<>) interface<> > 

(Это выжимка из godoc. Там можно найти больше)

Метод Done возвращает канал который действует как сигнал отмены для функций запущенных от имени Context . Когда канал закрывается, функции должны завершить работу. Метод Err возвращает ошибку, которая объясняет, почему Context был отменен. В статье «Pipelines and Cancelation» идиома Done каналов объясняется более подробно.

У контекста нет метода Cancel по той же причине, по которой канал Done работает только на прием. Функция, которая принимает сигнал отмены, как правило, совсем не та что его отправляет. В частности, когда родительская операция стартует go-рутину для подоперации эта подоперация не должна иметь возможность отменить работу родителя. Вместо этого функция WithCancel (которая описана ниже) предоставляет возможность отменить новое значение Context .

Context безопасен для использования в множестве go-рутин. Мы можем передать один Context любому количеству go-рутин и отметить этот Context по сигналу любой из них.

Метод Deadline позволяет функциям определить должны ли они начать работу. Если осталось слишком мало времени, это уже может быть не целесообразным. Так же, можно использовать дедлайны для установки таймаута для операций ввода/вывода.

Value позволяет в Context пользоваться запросозависимыми данными. Данные должны быть безопасны для одновременного использованием множеством go-рутин.

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

Пакет context предоставляет возможность производить новые значения Context из существующего. Все эти значения образуют дерево и в случае отмены родительского Context все производные тоже будут отменены.

Background это корень для всего дерева `Context и он никогда не отменяется:

// Background возвращает пустой Context. Он никогда не будет отменен и не имеет deadline // и значений. Background обычно используется в main, init, тестах и как верхний уровень // Context для входящих запросов. func Background() Context 

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

// WithCancel возвращает копию родителя, в котором Done закрывается как только // parent.Done будет закрыт или контекст будет отменен. func WithCancel(parent Context) (ctx Context, cancel CancelFunc) // CancelFunc отменяет контекст. type CancelFunc func() // WithTimeout возвразщает копию родителя, в котором Done закрывается как только // parent.Done будет закрыт, контекст будет отменен или закончится таймаут. Новый дедлайн // контекста состоит из текущее время + таймаут и родительский дедлайн если такой имеется. // Если таймер все еще работает, функция отмены релизит свои ресурсы. func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) 

WithValue предоставляет возможность привязать запросозависимые значения к контексту.

// WithValue возвращает копию родителя, в которой метод Value возвращает значение по ключу. func WithValue(parent Context, key interface<>, val interface<>) Context 

Самый лучший способ разобраться как работает пакет context это посмотреть на него в действии.

Пример: Google Web Search

В качестве примера у нас HTTP сервер, который обрабатывает URLы в таком формате /search?q=golang&timeout=1s передает запрос из параметра «golang» в Google Web Search API и отображает результат. Параметр «timeout» говорит нашему серверу через какое время прекратить запрос.

Весь код разделен на три пакета:

  • server в этом пакете определена функция main и обработчики для /search .
  • userip предоставляет функции для получения клиентского IP из запроса и привязка его к Context .
  • google тут определена функция Search для отправки запроса в сервис Google.

Пакет server обрабатывает запросы вида /search?q=golang для получения первых нескольких результатов из Google в golang. В это пакете регистрируется handleSearch для обработки всех запросов к /search . Обработчик создает начальный Context с названием ctx и подготавливает его к закрытию после завершения работы обработчика. Если запрос включает параметр timeout , тогда Context будет автоматически отменен по завершению таймаута:

func handleSearch(w http.ResponseWriter, req *http.Request) < // ctx это Context для этого обработчика. Отмена контекста (вызов cancel) // закрывает канал ctx.Done что является сигналом отмены // для запросов запущенных в этом обработчике. var ( ctx context.Context cancel context.CancelFunc ) timeout, err := time.ParseDuration(req.FormValue("timeout")) if err == nil < // В запросе есть параметр timeout, в таком случае создаем // контекст который будет отменен автоматически по // окончанию таймаута. ctx, cancel = context.WithTimeout(context.Background(), timeout) >else < ctx, cancel = context.WithCancel(context.Background()) >defer cancel() // Отменяем ctx как только handleSearch закончит работу. 

Обработчик извлекает поисковый запрос и клиентский IP адрес с помощью пакета userip . Клиентский IP нужен для выполнения запросов к АПИ Google. В результате handleSearch аттачит все это к ctx

 // Получаем посковый запрос. query := req.FormValue("q") if query == "" < http.Error(w, "no query", http.StatusBadRequest) return >// Записываем пользовательский IP в ctx для использования в других пакетах. userIP, err := userip.FromRequest(req) if err != nil < http.Error(w, err.Error(), http.StatusBadRequest) return >ctx = userip.NewContext(ctx, userIP) 

Обработчик вызывает google.Search и передает ctx и query :

 // Запускаем Google поиск и выводим результаты. start := time.Now() results, err := google.Search(ctx, query) elapsed := time.Since(start) 

Если поиск отработал нормально, тогда обработчик выводит результаты:

 if err := resultsTemplate.Execute(w, struct < Results google.Results Timeout, Elapsed time.Duration >< Results: results, Timeout: timeout, Elapsed: elapsed, >); err != nil

Пакет userip предоставляет функции для извлчения IP адреса из запроса и привязки его к Context . Сам Context дает возможность мапинга ключ/значение, в котором ключи и значения имеют тип interface<> . Все типы ключей должны поддерживать сравнение и все типы значений должны быть безопасными для использования в нескольких go-рутинах. Такие пакеты как userip должны скрывать подробности реализации этого мапинга и предоставлять строго типизированный доступ к значениям в Context .

Для избежания коллизий с ключами, в пакете userip определена константа key которая используется как ключ для получения значения из контекста:

// Тип key не экспортируемый для предотвращения коллизий к другими ключами определенными // в других пакетах. type key int // userIPkey это ключ контекста для клиентского IP. Его значение равно нулю. // Если в этом пакете определить другие ключи, они могут иметь различные // целочисленные значения. const userIPKey key = 0 

FromRequest извлекает значение userIP из http.Request :

func FromRequest(req *http.Request) (net.IP, error)

NewContext возвращает новый Context который содержит значение userIP :

func NewContext(ctx context.Context, userIP net.IP) context.Context

FromContext извлекает userIP из Context :

func FromContext(ctx context.Context) (net.IP, bool) < // ctx.Value возвращает nil если ctx не имеет значение для этого ключа; // Приведение к типу net.IP возвращает ok=false для значения nil. userIP, ok := ctx.Value(userIPKey).(net.IP) return userIP, ok >

Функция google.Search отправляет HTTP запрос к Google Web Search API и парсит результат в формате JSON. Он принимает Context параметр ctx и возвращает результат немедленно если ctx.Done будет закрыт пока запрос выполняется.

Запрос Google Web Search API содержит поисковый запрос и пользовательский IP:

func Search(ctx context.Context, query string) (Results, error) < // Подготовка запроса к Google Search API. req, err := http.NewRequest("GET", "https://ajax.googleapis.com/ajax/services/search/web?v=1.0", nil) if err != nil < return nil, err >q := req.URL.Query() q.Set("q", query) // Если ctx содержит пользовательский IP, передаем его серверу. // Google API использует пользовательский IP чтобы отличить запрос с сервера // от пользовательского запроса. if userIP, ok := userip.FromContext(ctx); ok < q.Set("userip", userIP.String()) >req.URL.RawQuery = q.Encode() 

Search использует вспомогательную функцию httpDo для создания HTTP запроса и его отмены, если ctx.Done закроется во время обработки запроса или ответа. Search передает замыкание в httpDo , которое в качестве параметра принимает HTTP запрос:

 var results Results err = httpDo(ctx, req, func(resp *http.Response, err error) error < if err != nil < return err >defer resp.Body.Close() // Разбираем результат в формате JSON. // https://developers.google.com/web-search/docs/#fonje var data struct < ResponseData struct < Results []struct < TitleNoFormatting string URL string >> > if err := json.NewDecoder(resp.Body).Decode(&data); err != nil < return err >for _, res := range data.ResponseData.Results < results = append(results, Result) > return nil >) // httpDo waits for the closure we provided to return, so it's safe to // read results here. return results, err 

Функция httpDo запускает HTTP запрос и обрабатывает ответ в новой go-рутине. Запрос будет отменен если ctx.Done будет отменен до выхода из go-рутины:

func httpDo(ctx context.Context, req *http.Request, f func(*http.Response, error) error) error < // Запускаем HTTP запрос в go-рутине и передаем запрос в f. tr := &http.Transport<>client := &http.Client c := make(chan error, 1) go func() < c () select < case > 

Адаптация кода для использования контекстов

Множество разный фреймворков предоставляют пакеты для работы с запросозависимыми значениями. Мы можем определить новую реализацию для интерфейса Context для связи между используемым фреймворком и кодом, который ожидает получить параметр типа Context .

Для примера, Gorilla пакета github.com/gorilla/context позволяет обработчику ассоциировать данные с входящим запросом предоставляя ключ/значение мапинг для HTTP запроса. В gorilla.go представлена реализация интерфейса Context в котором метод Value возвращает значения из HTTP запроса с помощью Gorilla’ского пакета.

Некоторые другие пакеты реализуют отмену по аналогии с нашим Context . Например пакет Tomb предоставляет функцию Kill , которая сигнализирует об отмене закрытием канала Dying . Tomb так же предоставляет функцию для ожидания всех go-рутин аналогичную sync.WaitGroup . В tomb.go мы представили реализацию Context который может быть отменен при отмене родительского Context или при условии, что Tomb будет «убит».

Заключение

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

Фреймворки, которые хотят работать с Context должны предоставить свои реализации для связи между функциями, которые принимают Context в параметрах. Функции в их клиентских библиотеках должны уметь работать с Context . Устанавливая интерфейс для работы с запросозависимыми данными и возможностью отмены, Context упрощает разработку более универсальных, масштабируемых и всем понятных пакетов.

Golang

Во многих API Go, особенно современных, первым аргументом функций и методов часто является context.Context. Контекст предоставляет средства передачи крайних сроков, отмены вызывающего абонента и других значений области запроса через границы API и между процессами. Он часто используется, когда библиотека напрямую или транзитивно взаимодействует с удаленными серверами, такими как базы данных, API-интерфейсы и т. д.

В документации для контекста указано:

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

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

Предпочитать контексты, передаваемые в качестве аргументов

Чтобы понять совет не хранить контекст в структурах, давайте рассмотрим предпочтительный подход контекста как аргумента:

type Worker struct < /* … */ >type Work struct < /* … */ >func New() *Worker < return &Worker<>> func (w *Worker) Fetch(ctx context.Context) (*Work, error) < _ = ctx // ctx для каждого вызова используется для отмены, // крайних сроков и метаданных. >func (w *Worker) Process(ctx context.Context, work *Work) error < _ = ctx // ctx для каждого вызова используется для отмены, // крайних сроков и метаданных. >

Здесь методы (*Worker).Fetch и (*Worker).Process принимают контекст напрямую. Благодаря этой конструкции передачи в качестве аргумента пользователи могут устанавливать для каждого вызова крайние сроки, отмену и метаданные. И ясно, как будет использоваться context.Context, переданный каждому методу: нет никаких ожиданий, что context.Context, переданный одному методу, будет использоваться любым другим методом. Это связано с тем, что контекст ограничен настолько малой операцией, насколько это необходимо, что значительно увеличивает полезность и ясность контекста в этом пакете.

Хранение контекста в структурах приводит к путанице

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

type Worker struct < ctx context.Context >func New(ctx context.Context) *Worker < return &Worker> func (w *Worker) Fetch() (*Work, error) < _ = w.ctx // Общий w.ctx используется для отмены, // крайних сроков и метаданных. >func (w *Worker) Process(work *Work) error < _ = w.ctx // Общий w.ctx используется для отмены, // крайних сроков и метаданных. >

Оба метода (*Worker).Fetch и (*Worker).Process используют контекст, хранящийся в Worker. Это не позволяет вызывающим объектам Fetch и Process (которые сами могут иметь разные контексты) указывать крайний срок, запрашивать отмену и прикреплять метаданные для каждого вызова. Например: пользователь не может указать крайний срок только для (*Worker).Fetch или отменить только вызов (*Worker).Process. Время жизни вызывающей стороны смешано с общим контекстом, а контекст ограничен временем жизни, в котором создается Worker.

API также намного больше сбивает пользователей с толку по сравнению с подходом с передачей в качестве аргумента. Пользователи могут спросить себя:

  • Поскольку New принимает context.Context, выполняет ли конструктор работу, требующую отмены или крайних сроков?
  • Применяется ли context.Context, переданный в New, для работы в (*Worker).Fetch и (*Worker).Process? Ни один? Один, а не другой?

API потребуется много документации, чтобы точно сообщить пользователю, для чего используется context.Context. Пользователю, возможно, также придется читать код вместо того, чтобы полагаться на структуру передаваемого API.

И, наконец, может быть довольно опасно проектировать сервер производственного уровня, запросы которого не имеют контекста и, следовательно, не могут должным образом учитывать отмену. Без возможности устанавливать крайние сроки для каждого вызова ваш процесс может исчерпать свои ресурсы (например, память)!

Исключение из правила: сохранение обратной совместимости

Когда был выпущен Go 1.7, который представил context.Context, большому количеству API пришлось добавить поддержку контекста обратно совместимыми способами. Например, методы Client net/http, такие как Get и Do, были отличными кандидатами в контекст. Каждый внешний запрос, отправленный с помощью этих методов, выиграет от наличия крайнего срока, отмены и поддержки метаданных, которые поставляются с context.Context.

Существует два подхода к добавлению поддержки context.Context обратно совместимыми способами: включение контекста в структуру, как мы вскоре увидим, и дублирование функций с дублированием, принимающим context.Context и имеющим Context в качестве суффикса имени функции. Подход с дублированием должен быть предпочтительнее контекста-в-структуре и более подробно обсуждается в посте Обеспечение совместимости ваших модулей. Однако в некоторых случаях это непрактично: например, если ваш API предоставляет большое количество функций, то дублирование их всех может оказаться невозможным.

Пакет net/http выбрал подход контекст-в-структуре, который представляет собой полезный пример. Давайте посмотрим на Do. До введения context.Context Do определялся следующим образом:

func (c *Client) Do(req *Request) (*Response, error)

После Go 1.7 Do мог бы выглядеть следующим образом, если бы не тот факт, что это нарушило бы обратную совместимость:

func (c *Client) Do(ctx context.Context, req *Request) (*Response, error)

Но сохранение обратной совместимости и соблюдение обещания совместимости Go 1 имеет решающее значение для стандартной библиотеки. Поэтому вместо этого разработчики решили добавить context.Context в структуру http.Request, чтобы разрешить поддержку context.Context без нарушения обратной совместимости:

type Request struct < ctx context.Context // . >func NewRequestWithContext(ctx context.Context, method, url string, body io.Reader) (*Request, error) < // Упрощено для краткости. return &Request< ctx: ctx, // . >> func (c *Client) Do(req *Request) (*Response, error)

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

func (c *Client) Call() error < return c.CallContext(context.Background()) >func (c *Client) CallContext(ctx context.Context) error < // . >
Вывод

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

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

При разработке API с контекстом помните совет: передайте context.Context в качестве аргумента; не храните его в структурах.

  • Совместимость ваших модулей
  • Тип Client пакета net/http в Golang
  • Паттерны конкурентности в Golang: Context

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

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

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

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

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

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

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

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

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

type Context interface  
Deadline() (deadline time.Time, ok bool) 
Done() chan struct<> 
Err() error 
Value(key interface<>) interface<> 

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

Метод Deadline()

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

Метод Done()

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

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

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

// Stream generates values with DoSomething and sends them
// to out until DoSomething returns an error or ctx.Done is
// closed.
func Stream(ctx context.Context, out chan
v, err := DoSomething(ctx)
if err != nil 
return err
select 
case 
return ctx.Err()
case out 

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

Метод Err()

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

// Canceled is the error returned by Context.Err when the
// context is canceled.
var Canceled = errors.New("context canceled")
// DeadlineExceeded is the error returned by Context.Err
// when the context's deadline passes.
var DeadlineExceeded error = deadlineExceededError<>

Метод 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() с именем и контекстом. Она работает в бесконечном цикле, выводя на консоль свое имя и крайний срок контекста, если таковой имеется. Затем он просто секунду спит.

Как использовать пакет context.Context с языком Go

В этом посте я расскажу о пакете context, официально выпущенном для версии Go 1.7, и о том, как его правильно использовать. Не обязательно, но было бы неплохо, если бы вы были хотя бы знакомы с ним, поэтому, если вы еще этого не сделали, вам следует прочитать этот пост в блоге из блога Golang.org, в котором рассказывается о пакете и о том, как он используется. В любом случае, я начну с того, что это такое.

Что такое пакет context.Context?

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

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

Что в нем?

Ядром пакета контекста является тип контекста, который является интерфейсом.

// A Context carries a deadline, cancelation signal, and request-scoped values across API boundaries. Its methods are safe for simultaneous use by multiple goroutines. type Context interface < // Done returns a channel that is closed when this Context is canceled // or times out. Done() // Err indicates why this context was canceled, after the Done channel // is closed. Err() error // Deadline returns the time when this Context will be canceled, if any. Deadline() (deadline time.Time, ok bool) // Value returns the value associated with key or nil if none. Value(key interface<>) interface<> >

Метод Done возвращает канал, который отправляет сигнал отмены функциям, выполняющимся от имени Context . Когда канал закрыт, функции должны отказаться от своей работы и вернуться.

Метод Err возвращает ошибку, указывающую, почему Context был отменен.

Метод Deadline позволяет функциям определять, должны ли они вообще начинать работу; если осталось слишком мало времени, это может не иметь смысла. Код также может использовать крайний срок для установки тайм-аутов для операций ввода-вывода.

Value позволяет Context переносить данные в рамках запроса. Эти данные должны быть безопасными для одновременного использования несколькими горутинами.

Добавление значения в контекст, где foo является ключом, а bar - значением

ctx = context.WithValue(ctx, “foo”, “bar”)

Получение значения из контекста:

ctx.Value(“foo”)

Как интегрировать контекстный пакет в ваш API?

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

Вот два способа интегрировать объекты контекста в ваш API:

  • Первый параметр вызова функции
  • Необязательный конфиг в структуре запроса

Пример первого см. В net package, Dialer.DialContext. Эта функция выполняет обычную операцию набора номера, но отменяет ее в соответствии с объектом Context.

func (d *Dialer) DialContext(ctx context.Context, network, address string) (Conn, error)

Пример второго способа интеграции контекста см. В package net / http Request.WithContext

func (r *Request) WithContext(ctx context.Context) *Request

Это создает новый объект запроса, который заканчивается в соответствии с данным контекстом.

Контекст должен проходить через ваше приложение

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

Единственное исключение, когда контекст не сохраняется, - это когда вам нужно поместить его в структуру, которая используется исключительно как сообщение, которое передается по каналу. Это показано в примере ниже.

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

  • Тайм-аут q в случае, если процессор переполнен
  • Чтобы сообщить q, должен ли он вообще обрабатывать сообщение
  • По истечении времени q отправка сообщения обратно в newRequest ()
  • Чтобы истекло время ожидания newRequest () ответа от ProcessMessage

Что должно и что не должно входить в значения Context.Value и области действия запроса?

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

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

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

Регистратор не имеет области действия запроса, если он находится на объекте сервера или является одиночным элементом пакета. Однако, если он содержит метаданные о том, кто отправил запрос и, возможно, если для запроса включено ведение журнала отладки, тогда он становится ограниченным по области действия.

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

Context.Value скрывает информационный поток

Одна из основных причин, по которой на правильное использование Context.Value накладывается так много ограничений, заключается в том, что он скрывает ожидаемый ввод и вывод функции или библиотеки. Вот почему его так ненавидят многие люди . Параметры функции ясны, вы можете это видеть, это самодостаточная документация о том, что требуется для того, чтобы функция работала. Это позволяет функции легко писать тесты и интуитивно обдумывать их, а также проводить рефакторинг позже. Например, рассмотрим следующую функцию, которая выполняет аутентификацию из контекста.

func IsUserRootAndAdmin(ctx context.Context) bool

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

Давайте наглядно представим этот поток с помощью функции, но более наглядно.

func IsUserRootAndAdmin(token string, authService AuthService) int

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

Контекст.Ценность в реальных проектах

Я полностью понимаю необходимость добавления элементов в Context.Value. Чем сложнее система, тем больше у вас будет слоев промежуточного программного обеспечения и нескольких абстракций в стеке вызовов. Значения, вычисленные в верхней части стека вызовов, утомительны, сложны и просто уродливы для ваших вызывающих, если вам нужно добавлять их к каждому вызову функции между верхним и нижним, чтобы просто распространить что-то простое, например идентификатор пользователя, идентификатор корреляции или токен аутентификации. Представьте, что вам нужно добавить еще один параметр, называемый «ID корреляции», к дюжине функций между двумя вызовами в двух разных пакетах, чтобы сообщить пакету ASD о том, какой пакет обнаружил ZXC? API выглядел бы некрасиво и, мягко говоря, от этого кода пахло бы.

Context.Value должно информировать, а не контролировать

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

Другими словами, если ваша функция не может работать правильно из-за значения, которое может находиться или не находиться внутри context.Value, значит, ваш API слишком сильно скрывает необходимые входные данные. Даже если вы забудете о документации, ваше приложение также будет ожидаемым поведением. Если функция, например, ведет себя так, как задокументировано, но способ ее использования приложением имеет практическое поведение, требующее чего-то в контексте для правильного поведения, тогда она приближается к влиянию на элемент управления вашей программы, и это пахнет.

Одним из примеров inform является идентификатор корреляции. Что обычно они используются в журналах или других системах агрегирования для группировки запросов вместе. Фактическое содержимое идентификатора корреляции никогда не изменяет результат оператора if, и если идентификатор корреляции отсутствует, он ничего не делает для изменения результата функции, возможно, вам будет трудно найти некоторую информацию в ваших журналах, но это об этом.

Например, соединение с базой данных - это наихудший пример объекта, который нужно поместить в context.Value, потому что он, очевидно, управляет программой и является обязательным вводом для ваших функций.

Как не использовать Context.Value

Здесь я покажу, как оставаться внутри такой абстракции, при этом не нужно злоупотреблять Context.Value. Давайте покажем пример кода, который использует промежуточное ПО HTTP и Context.Value для распространения идентификатора пользователя, найденного в начале промежуточного ПО.

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

Теперь давайте покажем, как можно использовать ту же абстракцию, но не злоупотребляя Context.Value.

Здесь мы по-прежнему можем использовать те же абстракции промежуточного программного обеспечения и по-прежнему иметь информацию о цепочке промежуточного программного обеспечения только основной функции, но использовать UserID типобезопасным способом. Переменная chainPartOne - это цепочка промежуточного программного обеспечения до момента, когда мы извлекаем UserID. Затем эта часть цепочки может создать следующую часть цепочки, chainWithAuth, напрямую используя UserID.

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

Если вы не можете использовать context.Value, не делайте этого!

Я понимаю, как легко просто добавить что-то в context.Value и получить это позже в какой-то далекой галактике, но теперь простота использования платная, вызывающая много боли при последующем рефакторинге, не говоря уже о когда пытаешься написать для него тесты. Пакет Context может быть очень полезен, но при неправильном использовании он может принести больше вреда, чем пользы. Так что будьте осторожны.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *