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

ale*_*lex 24 concurrency go goroutine

使用Go的context包,可以使用特定于请求的数据传递给请求处理函数堆栈

func WithValue(parent Context, key, val interface{}) Context
Run Code Online (Sandbox Code Playgroud)

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

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

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

icz*_*cza 29

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

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

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

这里需要注意的一点是,context.Context不会使用map底层来存储键值对,这一开始可能听起来令人惊讶,但如果您认为它必须是不可变的并且对于并发使用是安全的则不会.

用一个 map

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

用一个 struct

如果您使用包含struct要添加的所有键值对的字段的大值,那么这也可能是一个可行的选项.访问此结构Context.Value()将返回结构的副本,因此它对于并发使用是安全的(每个goroutine只能获得不同的副本),但是如果你有许多键值对,这将导致不必要的副本每当有人需要一个字段时,一个大结构.

使用混合解决方案

一个混合的解决办法是把在所有键值对map,并创建一个包装结构为这个地图,隐藏map(不导出字段),并提供了对存储在地图的值的吸气剂.仅将此包装器添加到上下文中,您可以保持多个goroutine 的安全并发访问(map未导出),但不需要复制大数据(map值是没有键值数据的小描述符),并且它仍然会很快(最终你会为地图编制索引).

这是它的样子:

type Values struct {
    m map[string]string
}

func (v Values) Get(key string) string {
    return v.m[key]
}
Run Code Online (Sandbox Code Playgroud)

使用它:

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"))
Run Code Online (Sandbox Code Playgroud)

输出(在Go Playground上试试):

two
Run Code Online (Sandbox Code Playgroud)

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

  • 正如 Go 上下文文档所说,“提供的键必须是可比较的,并且不应该是字符串类型或任何其他内置类型,以避免使用上下文的包之间发生冲突。WithValue 的用户应该定义自己的键类型” (3认同)
  • @sprutex 是的,但这对于并发使用来说并不安全。如果您使用非指针,“您可以获得的所有”都是一个副本,并且获得它的每个人都有自己的副本,因此本质上并发使用是安全的。 (3认同)

小智 17

context要创建具有多个键值的golang,您可以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"))
Run Code Online (Sandbox Code Playgroud)

在操场上观看它的实际表现

  • 以这种方式执行操作将故意忽略文档中的此语句:“提供的密钥必须是可比较的,并且**不应该是字符串类型或任何其他内置类型**...”。[这应该有帮助](https://medium.com/@matryer/context-keys-in-go-5312346a868d) (4认同)
  • 这很简单,简洁,而且效果很好! (2认同)

Sam*_*ted 14

是的,你是对的,你每次都需要打电话WithValue()给结果.要理解为什么它以这种方式工作,值得思考一下背景理论背后的理论.

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

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

上下文的树表示

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

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

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

  • 是的,这是公平的,它确实取决于具体情况.一般来说,我会说"如果它们是分开的,不相关的值,将它们分组是没有意义的".例如.如果您不将它们一起存储在结构或地图中,请不要在上下文中进行.当然,这只是我个人的偏好以及我认为最佳实践. (4认同)
  • _“如果你要使用映射或其他结构来存储你的密钥,它宁愿破坏上下文的点。”_ – 虽然这是真的,但提问者无意取消从最后 3 个开始的子树他想要添加的对。提问者想要添加他所有的键值对,他甚至没有公开“中间方式”上下文(并且它们无法通过“Context”接口访问)。因此,在我看来,在这种情况下,将所有键值对添加到单个节点没有任何缺点。 (2认同)
  • @SamWhited +1 以获得更深入的洞察力! (2认同)

omo*_*tto 6

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

type vars struct {
    lock    sync.Mutex
    db      *sql.DB
}
Run Code Online (Sandbox Code Playgroud)

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

ctx := context.WithValue(context.Background(), "values", vars{lock: mylock, db: mydb})
Run Code Online (Sandbox Code Playgroud)

你可以检索它:

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

  • 提供的键必须是可比较的,并且不应是字符串类型或任何其他内置类型,以避免使用上下文的包之间发生冲突。https://golang.org/pkg/context/#WithValue (2认同)