Глобально уникальный ключ для значения контекста в Golang

Предположим, у вас есть контекст, который передается через множество пакетов. Любой из этих пакетов может помещать туда информацию (например, что-то, связанное с обрабатываемым запросом). Вы хотите убедиться, что ключи не конфликтуют. Если пакет A помещает что-то под ключ K, тогда пакет B не может перезаписать значение под этим ключом.

Соберем несколько фактов:

Два значения интерфейса равны, если они имеют одинаковые динамические типы и одинаковые динамические значения или оба имеют значение nil.

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

package main
import (
    "context"
    "fmt"
    "github.com/mlowicki/b"
)
func main() {
    ctx, _ := context.WithCancel(context.Background())
    ctx = b.Set(ctx)
    fmt.Println(b.Get(ctx))
    fmt.Println(ctx.Value(1))
}
package b
import (
    "context"
)
type key int
var id = key(1)
func Set(ctx context.Context) context.Context {
    return context.WithValue(ctx, id, "secret")
}
func Get(ctx context.Context) (string, bool) {
    val, ok := ctx.Value(id).(string)
    return val, ok
}

выход:

secret true
<nil>

Значение, хранящееся в key(1), безопасно, поскольку только код в пакете b может использовать тип b.key.

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

Редактировать

Обсуждение на Reddit принесло несколько идей:

  • const можно использовать вместо var, чтобы запретить изменение значения ключа
  • type key struct{} можно использовать, и приведенные выше правила по-прежнему остаются в силе. Тесты с testing.AllocsPerRun показывают такое же количество выделений в Go 1.9, что и при использовании int. Вероятно, этот подход более эффективен с точки зрения памяти в некоторых старых версиях Go.

Кредиты для TheMerovius и epiris.

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

Ресурсы