Контекст в Go: запись и чтение значений

от автора

Данная статья — это вырезка из книги Джона Боднера под названием «Go идиомы и паттерны проектирования». На момент чтения 14-й главы, посвященной теме контекста, мне показался полезным её подраздел про работу со значениями посредством контекста. Полезным в том смысле, что этот подраздел вполне может служить справкой для новичков сам по себе, взятый автономно из содержащей его книги. Справкой по конкретному вопросу чтения и записи значений из контекста, разумеется, а не обозревающей тему контекста целиком. Помимо освещения API работы с контекстом для хранения значений, Боднер приводит и объяснение, в каких случаях это может быть уместно.

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


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

Однако, иногда невозможно передать данные явно. Типичный пример — обработчик HTTP-запросов и ассоциированный с ним промежуточный слой. Любой обработчик HTTP-запросов имеет два параметра: один для запроса и один для ответа. Чтобы сделать значение доступным для обработчика в промежуточном слое, его нужно сохранить в контексте. Примером такой ситуации может служить извлечение пользователя из веб-токена JSON (JWT, JSON Web Token) или создание для каждого запроса глобального уникального идентификатора, который передается в обработчик и бизнес-логику через несколько промежуточных слоев.

Наряду с фабричными методами для создания контекстов, отменяемых таймаутом или функцией отмены, в пакете context имеется и фабричный метод для записи значений в контекст, context.WithValue. Он принимает три параметра: обертываемый контекст, ключ для извлечения значения и само значение. Параметры для передачи ключа и значения объявлены с типом any. В качестве результата этот метод возвращает дочерний контекст с парой «ключ — значение» и обернутым родительским контекстом context.Context.

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

С помощью метода Value типа context.Context можно проверить наличие значения в контексте или в одном из его родителей. Этот метод принимает ключ и возвращает ассоциированное с ним значение. При этом параметр ключа и возвращаемое значение опять же объявлены с типом any. Если искомый ключ отсутствует, то метод возвращает nil. Чтобы привести возвращаемое значение к подходящему типу, используйте идиому «запятая-ok»:

ctx := context.Background() if myVal, ok := ctx.Value(myKey).(int); !ok {   fmt.Println("no value") } else {   fmt.Println("value:", myVal) }

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

В контексте можно сохранить значение любого типа, но для ключа нужно выбрать правильный тип. Как и ключ отображения, ключ сохраняемого в контекст значения должен иметь тип, поддерживаемый сравнение. Не используйте простые строки, такие как «id». Если в качестве типа ключа задействовать строку или другой экспортируемый тип, то в других пакетах можно будет создать идентичные ключи, что приведет к конфликтам. Это вызовет трудно поддающиеся отладке проблемы, как, например, в случае, когда один пакет записывает в контекст данные, маскирующие данные, записанные другим пакетом, или читает из контекста данные, записанные другим пакетом.

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

type userKey int

После объявления неэкспортируемого типа объявляется неэкспортируемая константа этого типа:

const (   _ userKey = iota   key )

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

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

func ContextWithUser(ctx context.Context, user string) context.Context {   return context.WithValue(ctx, key, user) }  func UserFromContext(ctx context.Context) (string, book) {   user, ok := ctx.Value(key).(string)   return user, ok }

Другой вариант — определить неэкспортируемый тип ключа, используя пустую структуру:

type userKey struct{}

И соответственно изменить функции доступа к значению контекста:

func ContextWithUser(ctx context.Context, user string) context.Context {   return context.WithValue(ctx, userKey{}, user) }  func UserFromContext(ctx context.Context) (string, bool) {   user, ok := ctx.Value(userKey{}).(string)   return user, ok }

Как правильно выбрать стиль ключа в каждом конкретном случае? Если требуется сохранить в контексте набор связанных ключей с различными значениями, используйте прием на основе int и iota. Если задействуется только один ключ, то подойдет любой из способов. Важно лишь обеспечить невозможность конфликтов ключей в контексте.

Теперь, располагая кодом для управления пользователями, посмотрим, как его можно применить. Напишем промежуточный слой, извлекающий ID пользователя из файла cookie:

// в реальной реализации следует использовать подпись, // чтобы исключить возможность подделки идентификатора пользователя func extractUser(req *http.Request) (string, error) {   userCookie, err := req.Cookie("identity")   if err != nil {     return "", err   }   return userCookie.Value, nil }  func Middleware(h http.Handler) http.Handler {   return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {     user, err := extractUser(req)     if err != nil {       rw.WriteHeader(http.StatusUnauthorized)       rw.Write([]byte("unauthorized"))       return     }     ctx := req.Context()     ctx = ContextWithUser(ctx, user)     req = req.WithContext(ctx)     h.ServeHTTP(rw, req)   }) }

В промежуточном слое мы сначала получаем значение ID пользователя. Затем извлекаем контекст из запроса с помощью метода Context и создаем новый контекст со значением ID пользователя вызовом функции ContextWithUser. После этого создаем новый запрос на основе старого запроса и нового контекста с помощью метода WithContext. Наконец, вызываем следующую функцию в цепочке обработчиков, передавая ей новый запрос и полученный в качестве параметра экземпляр типа http.ResponseWriter.

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

func (c Controller) DoLogic(rw http.ResponseWriter, req *http.Request) {   ctx := req.Context()   user, ok := identity.UserFromContext(ctx)   if !ok {     rw.WriteHeader(http.StatusInternalServerError)     return   }   data := req.URL.Query().Get("data")   result, err := c.Logic.BusinessLogic(ctx, user, data)   if err != nil {     rw.WriteHeader(http.StatusInternalServerError)     rw.Write([]byte(err.Error()))     return   }   rw.Write([]byte(result)) }

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

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

Вот как выглядит простая реализация глобального уникального идентификатора (GUID) с поддержкой контекста, позволяющая следить за передачей запроса от сервиса к сервису и создавать в журнале записи, содержащие GUID-идентификатор:

package tracker  import (   "context"   "fmt"   "net/http"    "github.com/google/uuid" )  type guidKey int  const key guidKey = 1  func contextWithGUID(ctx context.Context, guid string) context.Context {   return context.WithContext(ctx, key, guid) }  func guidFromContext(ctx context.Context) (string, bool) {   g, ok := ctx.Value(key).(string)   return g, ok }  func Middleware(h http.Handler) http.Handler {   return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {     ctx := req.Context()     if guid := req.Header.Get("X-GUID"); guid != "" {       ctx = contextWithGUID(ctx, guid)     } else {       ctx = contextWithGUID(ctx, uuid.New().String())     }     req = req.WithContext(ctx)     h.ServeHTTP(rw, req)   }) }  type Logger struct{}  func (Logger) Log(ctx context.Context, message string) {   if guid, ok := guidFromContext(ctx); ok {     message = fmt.Sprintf("GUID: %s - %s", guid, message)   }   // выполняем журналирование   fmt.Println(message) }  func Request(req *http.Request) *http.Request {   ctx := req.Context()   if guid, ok := guidFromContext(ctx); ok {     req.Header.Add("X-GUID", guid)   }   return req }

Функция Middleware либо извлекает GUID-идентификатор из входящего запроса, либо генерирует новый. В обоих случаях она записывает GUID-идентификатор в контекст, создает новый запрос с обновленным контекстом и выполняет следующий вызов в цепочке вызовов.

Далее мы видим, как используется этот GUID-идентификатор. Структура Logger предоставляет универсальный метод журналирования, который принимает в качестве параметров контекст и строку. Если в контексте содержится GUID-идентификатор, он добавляется в начало сообщения журнала, которое затем выводится на экран. Функция Request применяется в том случае, когда данный сервис вызывает другой сервис. Она принимает экземпляр типа http.Request, добавляет заголовок с GUID-идентификатором при его наличии в контексте и возвращает экземпляр типа *http.Request.

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

type Logger interface {   Log(context.Context, string) }  type RequestDecorator func(*http.Request) *http.Request  type LogicImpl struct {   RequestDecorator RequestDecorator   Logger           Logger   Remote           string }

Затем реализуем бизнес-логику:

func (l LogicImpl) Process(ctx context.Context, data string) (string, error) {   l.Logger.Log(ctx, "starting Process with " + data)   req, err := http.NewRequestWithContext(ctx, http.MethodGet, l.Remote+"/second?query="+data, nil)      if err != nil {     l.Logger.Log(ctx, "error building remote request:"+err.Error())     return "", err   }   req = l.RequestDecorator(req)   resp, err := http.DefaultClient.Do(req)   // продолжение обработки }

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

controller := Controller{   Logic: LogicImpl{     RequestDecorator: tracker.Request,     Logger:           tracker.Logger{},     Remote:           "http://localhost:4000"   } }

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


ссылка на оригинал статьи https://habr.com/ru/articles/932368/


Комментарии

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

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