英文:
context.WithValue: how to add several key-value pairs
问题
使用Go的context
包,可以通过以下方式将请求特定的数据传递给请求处理函数的堆栈:
func WithValue(parent Context, key, val interface{}) Context
这将创建一个新的Context
,它是父Context
的副本,并包含可以通过键访问的值。
如果我想在Context
中存储多个键值对,应该如何操作?我需要多次调用WithValue()
,每次都将上一次调用WithValue()
返回的Context
作为参数传递吗?这看起来很麻烦。
还是我应该使用一个结构体,将所有的数据放在其中,这样我只需要传递一个值(即结构体),其他所有的值都可以通过该结构体访问?
或者是否有一种方法可以将多个键值对传递给WithValue()
函数?
英文:
With Go's context
package it is possible to pass request-specific data to the stack of request handling functions using
func WithValue(parent Context, key, val interface{}) Context
This creates a new Context
which is a copy of parent and contains the value val which can be accessed with key.
How do I proceed if I want to store several key-value pairs in a Context
? Shall I call WithValue()
several times, each time passing the Context
received from my last call to WithValue()
? This appears cumbersome.
Or shall I use a struct and put all my data there, s.t. I need to pass only one value (which is the struct), from which all others can be accessed?
Or is there a way of passing several key-value pairs to WithValue()
?
答案1
得分: 80
你列出了你的选项。你所寻找的答案取决于你想如何使用存储在上下文中的值。
context.Context
是一个不可变对象,通过在副本中添加新的键值对来“扩展”它只能通过复制它并将新的键值对添加到副本中来实现(这是由 context
包在幕后完成的)。
你是否希望后续处理程序能够以透明的方式通过键访问所有的值?那么可以使用循环将所有值添加到上下文中,始终使用最后一个操作的上下文。
需要注意的一点是,context.Context
在内部并不使用 map
来存储键值对,这一点可能会让人感到惊讶,但如果你考虑到它必须是不可变的并且对并发使用是安全的,就不会感到奇怪了。
使用 map
例如,如果你有很多键值对,并且需要通过键快速查找值,逐个添加每个键值对将导致 Context
的 Value()
方法变慢。在这种情况下,最好将所有的键值对作为单个 map
值添加,可以通过 Context.Value()
访问该值,并且可以通过相关键在 O(1)
时间内查询每个值。但要知道,这种方法在并发使用时不安全,因为 map
可能会从并发的 goroutine 中被修改。
使用 struct
如果你使用一个大的 struct
值,其中包含你想要添加的所有键值对的字段,那也是一个可行的选择。使用 Context.Value()
访问此结构将返回结构的副本,因此它对并发使用是安全的(每个 goroutine 只能获得一个不同的副本),但如果你有很多键值对,每当有人需要从中获取一个字段时,这将导致不必要的大结构的复制。
使用混合解决方案
混合解决方案可以是将所有的键值对放入一个 map
中,并为该 map
创建一个包装结构,隐藏 map
(未导出的字段),并仅为存储在 map
中的值提供一个 getter。只将此包装器添加到上下文中,你可以保持多个 goroutine 的安全并发访问(map
是未导出的),同时不需要复制大量的数据(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
如果性能不是关键(或者你只有相对较少的键值对),我建议逐个添加键值对。
英文:
You pretty much listed your options. The answer you're seeking for depends on how you want to use the values stored in the context.
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
package).
Do you want further handlers to be able to access all the values by key in a transparent way? Then add all in a loop, using always the context of the last operation.
One thing to note here is that the context.Context
does not use a map
under the hood to store the key-value pairs, which might sound surprising at first, but not if you think about it must be immutable and safe for concurrent use.
Using a map
So for example if you have a lot of key-value pairs and need to lookup values by keys fast, adding each separately will result in a Context
whose Value()
method will be slow. In this case it's better if you add all your key-value pairs as a single map
value, which can be accessed via Context.Value()
, and each value in it can be queried by the associated key in O(1)
time. Know that this will not be safe for concurrent use though, as a map may be modified from concurrent goroutines.
Using a struct
If you'd use a big struct
value having fields for all the key-value pairs you want to add, that may also be a viable option. Accessing this struct with Context.Value()
would return you a copy of the struct, so it'd be safe for concurrent use (each goroutine could only get a different copy), but if you have many key-value pairs, this would result in unnecessary copy of a big struct each time someone needs a single field from it.
Using a hybrid solution
A hybrid solution could be to put all your key-value pairs in a map
, and create a wrapper struct for this map, hiding the map
(unexported field), and provide only a getter for the values stored in the map. Adding only this wrapper to the context, you keep the safe concurrent access for multiple goroutines (map
is unexported), yet no big data needs to be copied (map
values are small descriptors without the key-value data), and still it will be fast (as ultimately you'll index a map).
This is how it could look like:
type Values struct {
m map[string]string
}
func (v Values) Get(key string) string {
return v.m[key]
}
Using it:
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"))
Output (try it on the Go Playground):
two
If performance is not critical (or you have relatively few key-value pairs), I'd go with adding each separately.
答案2
得分: 28
是的,你是正确的,每次都需要调用WithValue()
并传入相应的结果。要理解为什么要这样操作,值得思考一下上下文背后的理论。
上下文实际上是上下文树中的一个节点(因此有各种上下文构造函数需要传入“父”上下文)。当你从上下文中请求一个值时,实际上是在从该上下文开始,沿着树向上搜索,寻找与你的键匹配的第一个值。这意味着如果你的树有多个分支,或者你从一个分支的较高点开始,你可能会找到不同的值。这是上下文的一部分功能。而取消信号则向下传播到被取消的元素的所有子元素,因此你可以取消单个分支,或者取消整个树。
举个例子,这是一个包含各种可能存储在上下文中的内容的上下文树:
黑色的边表示数据查找,灰色的边表示取消信号。请注意,它们传播的方向相反。
如果你使用映射或其他结构来存储你的键,那么上下文的意义就被破坏了。你将无法只取消请求的一部分,或者根据请求的不同部分更改日志记录的位置等。
简而言之,是的,需要多次调用 WithValue。
英文:
Yes, you're correct, you'll need to call WithValue()
passing in the results each time. To understand why it works this way, it's worth thinking a bit about the theory behind context's.
A context is actually a node in a tree of contexts (hence the various context constructors taking a "parent" context). When you request a value from a context, you're actually requesting the first value found that matches your key when searching up the tree, starting from the context in question. This means that if your tree has multiple branches, or you start from a higher point in a branch, you could find a different value. This is part of the power of contexts. Cancellation signals, on the other hand, propagate down the tree to all child elements of the one that's canceled, so you can cancel a single branch, or cancel the entire tree.
For an example, here's a context tree that contains various things you might store in contexts:
The black edges represent data lookups, and the grey edges represent cancelation signals. Note that they propogate in opposite directions.
If you were to use a map or some other structure to store your keys, it would rather break the point of contexts. You would no longer be able to cancel only a part of a request, or eg. change where things were logged depending on what part of the request you were in, etc.
TL;DR — Yes, call WithValue several times.
答案3
得分: 13
创建一个具有多个键值对的Golang context
,你可以多次调用WithValue
方法。context.WithValue(basecontext, key, value)
ctx := context.WithValue(context.Background(), "1", "one") // 基础上下文
ctx = context.WithValue(ctx, "2", "two") // 派生上下文
fmt.Println(ctx.Value("1"))
fmt.Println(ctx.Value("2"))
在playground上查看实际效果。
英文:
To create a golang context
with multiple key-values you can call WithValue
method multiple times. 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"))
See it in action on the playground
答案4
得分: 7
根据"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
英文:
As "icza" said you can group the values in one struct:
type vars struct {
lock sync.Mutex
db *sql.DB
}
Then you can add this struct in context:
ctx := context.WithValue(context.Background(), "values", vars{lock: mylock, db: mydb})
And you can retrieve it:
ctxVars, ok := r.Context().Value("values").(vars)
if !ok {
log.Println(err)
return err
}
db := ctxVars.db
lock := ctxVars.lock
答案5
得分: 1
我创建了一个辅助包,可以一次性添加多个键值对。
package econtext
import (
"context"
)
func WithValues(ctx context.Context, kv ...interface{}) context.Context {
if len(kv)%2 != 0 {
panic("键值对数量必须为偶数")
}
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",
)
英文:
I created a helper pkg to add multiple key-value pairs at once
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
}
usage -
ctx = econtext.WithValues(ctx,
"k1", "v1",
"k2", "v2",
"k3", "v3",
)
答案6
得分: 0
一种(功能性)的方法是使用柯里化和闭包。
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
以上是要翻译的内容。
英文:
One (functional) way to do it is using currying and closures
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
答案7
得分: -3
import (
"google.golang.org/grpc/metadata"
"context"
)
func main(){
scheme := "bearer"
token := getToken() // 获取字符串形式的令牌
md := metadata.Pairs("authorization", fmt.Sprintf("%s %v", scheme, token))
nCtx := metautils.NiceMD(md).ToOutgoing(context.Background())
}
请注意,这是一段Go语言代码,它使用了google.golang.org/grpc/metadata
和context
包。在main
函数中,它定义了一个scheme
变量和一个token
变量,然后使用这些变量创建了一个metadata.Pairs
对象md
,其中包含了authorization
键和对应的值。最后,使用metautils.NiceMD(md).ToOutgoing(context.Background())
方法将md
转换为出站context.Context
对象nCtx
。
英文:
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())
}
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论