context.WithValue:如何添加多个键值对

context.WithValue: how to add several key-value pairs

使用 Go 的 context 包,可以使用

将特定于请求的数据传递到请求处理函数的堆栈
func WithValue(parent Context, key, val interface{}) Context

这将创建一个新的 Context,它是 parent 的副本并包含可以使用键访问的值 val。

如果我想在 Context 中存储多个键值对,我该如何进行?我应该多次调用 WithValue(),每次都将我上次调用收到的 Context 传递给 WithValue() 吗?这看起来很麻烦。
或者我应该使用一个结构并将我所有的数据放在那里,s.t。我只需要传递一个值(即结构),从中可以访问所有其他值?

或者有没有办法将几个键值对传递给 WithValue()

您几乎列出了您的选择。您正在寻找的答案取决于您希望如何使用存储在上下文中的值。

context.Context is an immutable object, "extending" it with a key-value pair is only possible by making a copy of it and adding the new key-value to the copy (which is done under the hood, by the context包)。

您是否希望进一步的处理程序能够以透明的方式按键访问所有值?然后在循环中添加所有内容,始终使用最后一个操作的上下文。

这里要注意的一件事是 context.Context 不使用 map 底层存储键值对,这乍一听可能令人惊讶,但如果你认为关于它必须是不可变的并且可以安全地同时使用。

使用 map

因此,例如,如果您有很多键值对并且需要通过键 fast 查找值,分别添加每个将导致 ContextValue() 方法会很慢。在这种情况下,最好将所有键值对添加为单个 map 值,该值可以通过 Context.Value() 访问,并且其中的每个值都可以通过 [=] 中的关联键查询22=] 时间。知道这对于并发使用来说并不安全,因为地图可能会从并发 goroutines 中修改。

使用 struct

如果您使用一个大 struct 值,其中包含要添加的所有键值对的字段,这也是一个可行的选择。使用 Context.Value() 访问此结构将 return 您是该结构的副本,因此并发使用是安全的(每个 goroutine 只能获得不同的副本),但如果您有很多键值对,这将导致每次有人需要它的单个字段时都不必要地复制一个大结构。

使用混合解决方案

混合 解决方案可能是将所有键值对放在 map 中,并为此映射创建一个包装器结构,隐藏 map(未导出的字段),并且只为映射中存储的值提供 getter。仅将此包装器添加到上下文中,您可以为多个 goroutine 保持 安全并发访问 map 未导出),但 无需复制大数据map 值是没有键值数据的小描述符),它仍然是 fast(因为最终您将索引地图)。

它可能是这样的:

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

如果性能不是很重要(或者您的键值对相对较少),我会单独添加每个键值对。

是的,你是对的,你需要调用 WithValue() 每次传递结果。要理解它为何以这种方式工作,值得思考一下上下文背后的理论。

上下文实际上是上下文树中的一个节点(因此各种上下文构造函数采用“父”上下文)。当您从上下文中请求一个值时,您实际上是在从相关上下文开始搜索树时请求找到的与您的键匹配的第一个值。这意味着如果您的树有多个分支,或者您从分支中较高的点开始,您可能会找到不同的值。这是语境的一部分力量。另一方面,取消信号会沿着树向下传播到被取消元素的所有子元素,因此您可以取消单个分支,或取消整个树。

例如,这里有一个上下文树,其中包含您可能存储在上下文中的各种内容:

黑色边缘代表数据查找,灰色边缘代表取消信号。请注意,它们的传播方向相反。

如果您要使用映射或其他一些结构来存储您的密钥,它宁愿打破上下文的要点。您将不再能够仅取消请求的一部分,或者例如。根据您所在的请求部分等更改记录内容的位置。

TL;DR — 是的,多次调用 WithValue。

正如"icza"所说,您可以将值分组在一个结构中:

type vars struct {
    lock    sync.Mutex
    db      *sql.DB
}

然后你可以在上下文中添加这个结构:

ctx := context.WithValue(context.Background(), "values", vars{lock: mylock, db: mydb})

你可以检索它:

ctxVars, ok := r.Context().Value("values").(vars)
if !ok {
    log.Println(err)
    return err
}
db := ctxVars.db
lock := ctxVars.lock

一种(功能性)方法是使用柯里化和闭包

r.WithContext(BuildContext(
   r.Context(),
   SchemaId(mySchemaId),
   RequestId(myRequestId),
   Logger(myLogger)
))

func RequestId(id string) partialContextFn {
   return func(ctx context.Context) context.Context {
      return context.WithValue(ctx, requestIdCtxKey, requestId)
   }
}

func BuildContext(ctx context.Context, ctxFns ...partialContextFn) context.Context {
   for f := range ctxFns {
      ctx = f(ctx)
   }

   return ctx
} 

type partialContextFn func(context.Context) context.Context

要创建具有多个 key-values 的 golang context,您可以多次调用 WithValue 方法。 context.WithValue(basecontext, key, value)

    ctx := context.WithValue(context.Background(), "1", "one") // base context
    ctx = context.WithValue(ctx, "2", "two") //derived context

    fmt.Println(ctx.Value("1"))
    fmt.Println(ctx.Value("2"))

playground

上查看实际效果
import (
    "google.golang.org/grpc/metadata"
    "context"
)

func main(){
    scheme := "bearer"
    token := getToken() // get token in string
    md := metadata.Pairs("authorization", fmt.Sprintf("%s %v", scheme, token))
    nCtx := metautils.NiceMD(md).ToOutgoing(context.Background())
}

我创建了一个帮助程序 pkg 来一次添加多个键值对

package econtext

import (
    "context"
)

func WithValues(ctx context.Context, kv ...interface{}) context.Context {
    if len(kv)%2 != 0 {
        panic("odd numbers of key-value pairs")
    }
    for i := 0; i < len(kv); i = i + 2 {
        ctx = context.WithValue(ctx, kv[i], kv[i+1])
    }
    return ctx
}

用法 -

ctx = econtext.WithValues(ctx,
    "k1", "v1",
    "k2", "v2",
    "k3", "v3",
)