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

时间:2016-11-02 12:28:01

标签: go concurrency goroutine

使用Go的context包,可以使用

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

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

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

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

6 个答案:

答案 0 :(得分:21)

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

context.Context是一个不可变对象,"扩展"只有通过复制它并将新的键值添加到副本(通过context包在幕后完成),才能使用键值对。

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

这里需要注意的一点是,context.Context并没有使用map来存储键值对,这一开始可能听起来令人惊讶,但如果你想一想就不行必须是不可变的,并且可以安全地同时使用。

使用map

例如,如果您有很多键值对并需要按键快速查找值,则单独添加每个值将导致Context Value()方法会很慢。在这种情况下,如果您将所有键值对添加为单个map值,可以通过Context.Value()访问它,并且相关联的查询中的每个值都可以查询键入O(1)时间。知道这对于并发使用是不安全的,因为可以从并发的goroutine修改地图。

使用struct

如果您使用包含要添加的所有键值对的字段的大struct值,那么这也可能是一个可行的选项。使用Context.Value()访问此结构将返回结构的副本,因此它对于并发使用是安全的(每个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

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

答案 1 :(得分:12)

是的,您是正确的,您每次都需要致电WithValue()传递结果。要理解为什么它以这种方式工作,值得思考一下背景背后的理论。

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

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

Tree representation of context

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

如果您使用地图或其他结构来存储密钥,则宁可打破上下文的范围。您将无法仅取消请求的一部分,或者例如。根据您所处的请求的哪个部分等更改记录事项的位置

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

答案 2 :(得分:0)

正如“ 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

答案 3 :(得分:0)

一种(实用的)方法是使用curring和closes

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

答案 4 :(得分:0)

要创建具有多个键值的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

上查看其运行情况

答案 5 :(得分:0)

我创建了一个 helper 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",
)