使用Go的context
包,可以使用
func WithValue(parent Context, key, val interface{}) Context
这会创建一个新的Context
,它是父项的副本,并包含可以使用键访问的值val。
如果我想在Context
中存储多个键值对,该如何操作?我应该多次拨打WithValue()
,每次从我上次拨打Context
时收到的WithValue()
?这看起来很麻烦
或者我应该使用结构并将所有数据放在那里,s.t。我只需要传递一个值(即结构),从中可以访问所有其他值吗?
或者有没有办法将几个键值对传递给WithValue()
?
答案 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()
传递结果。要理解为什么它以这种方式工作,值得思考一下背景背后的理论。
上下文实际上是上下文树中的节点(因此各种上下文构造函数采用"父级"上下文)。当您从上下文请求值时,从相关上下文开始,您实际上在搜索树时实际请求找到与您的键匹配的第一个值。这意味着如果您的树有多个分支,或者您从分支中的较高点开始,则可以找到不同的值。这是语境力量的一部分。另一方面,取消信号将树向下传播到已取消的子元素,因此您可以取消单个分支,或取消整个树。
例如,这里有一个上下文树,其中包含您可能存储在上下文中的各种内容:
黑色边缘表示数据查找,灰色边缘表示取消信号。请注意,它们以相反的方向传播。
如果您使用地图或其他结构来存储密钥,则宁可打破上下文的范围。您将无法仅取消请求的一部分,或者例如。根据您所处的请求的哪个部分等更改记录事项的位置
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"))
上查看其运行情况
答案 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",
)