Golang context.WithValue: как добавить несколько пар ключ-значение

С пакетом Go context можно передать данные, специфичные для запроса, в стек функций обработки запросов, используя

func WithValue(parent Context, key, val interface{}) Context

Это создает новый context, который является копией родителя и содержит значение val, к которому можно получить доступ с помощью ключа.

Как мне продолжить, если я хочу сохранить несколько пар ключ-значение в context? Должен ли я называть WithValue() несколько раз, каждый раз передавая context, полученный от моего последнего вызова, на WithValue()? Это кажется громоздким. Или я буду использовать структуру и поместить все мои данные там, s.t. Мне нужно передать только одно значение (которое является структурой), из которого можно получить доступ ко всем остальным?

Или существует способ передачи нескольких пар ключ-значение в WithValue()?

Ответ 1

Вы в значительной степени указали свои варианты. Ответ, который вы ищете, зависит от того, как вы хотите использовать значения, хранящиеся в контексте.

context.Context - неизменный объект, "расширение" его с помощью пары ключ-значение возможно только путем создания его копии и добавление нового ключевого значения в копию (что делается под капотом, с помощью context).

Хотите, чтобы дополнительные обработчики имели доступ ко всем значениям ключа прозрачным способом? Затем добавьте все в цикл, используя всегда контекст последней операции.

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

Использование map

Так, например, если у вас много пар ключ-значение и вам нужно быстро найти значения по клавишам, добавление каждого из них приведет к созданию context, метод Value() будет медленным. В этом случае лучше, если вы добавите все свои пары ключ-значение в виде единственного значения map, к которому можно получить доступ через Context.Value(), и каждое значение в нем может быть запрошено связанным ключом в O(1) времени. Знайте, что это не будет безопасно для одновременного использования, хотя, поскольку карта может быть изменена из параллельных goroutines.

Использование struct

Если вы используете большое значение struct, имеющее поля для всех пар ключ-значение, которые вы хотите добавить, это также может быть жизнеспособным вариантом. Доступ к этой структуре с помощью Context.Value() вернет вам копию структуры, поэтому было бы безопасно для одновременного использования (каждый goroutine мог получить только другую копию), но если у вас много пар ключ-значение, это приведет к ненужная копия большой структуры каждый раз, когда кому-то требуется одно поле.

Использование гибридного решения

Гибридное решение может состоять в том, чтобы поместить все ваши пары ключ-значение в map и создать структуру-оболочку для этой карты, скрыв map (нераспределенное поле) и предоставить только получатель для сохраненных значений на карте. Добавляя только эту оболочку в контекст, вы сохраняете безопасный параллельный доступ для нескольких goroutines (map не экспортируется), но не нужно копировать большие данные (map значения - это небольшие дескрипторы без данных ключа), и тем не менее он будет быстрым (так как в конечном счете вы индексируете карту).

Вот как это могло бы выглядеть:

type Values struct {
    m map[string]string
}

func (v Values) Get(key string) string {
    return v.m[key]
}

Используя его:

v := Values{map[string]string{
    "1": "one",
    "2": "two",
}}

c := context.Background()
c2 := context.WithValue(c, "myvalues", v)

fmt.Println(c2.Value("myvalues").(Values).Get("2"))

Выход (попробуйте на Go Playground):

two

Если производительность не является критичной (или у вас относительно мало пар ключ-значение), я бы добавил их отдельно.

Ответ 2

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

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

Например, вот контекстное дерево, которое содержит различные вещи, которые вы можете хранить в контекстах:

Tree representation of context

Черные края представляют поиск данных, а серые края представляют сигналы отмены. Обратите внимание, что они распространяются в противоположных направлениях.

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

TL; DR - Да, вызывать WithValue несколько раз.

Ответ 3

package main

import (
    "context"
    "fmt"
)


func main() {
    type favContextKey string

    f := func(ctx context.Context, k favContextKey) {
        if v := ctx.Value(k); v != nil {
            fmt.Println("found value:", v)
            return
        }
        fmt.Println("key not found:", k)
    }

ctx := context.Background()

    k := favContextKey("zap-graylog.Logger")
    ka := favContextKey("zap-graylog.Token")
    kb := favContextKey("zap-graylog.RequestId")
    ctx = context.WithValue(ctx, k, "Go1")
    ctx = context.WithValue(ctx, ka, "Go2")
    ctx = context.WithValue(ctx, kb, "Go3")
    f(ctx, k)
    f(ctx, ka)
    f(ctx, kb)
}