context
本身是线程安全的,所以context
携带value
也是线程安全的.
func main() {
ctx := context.WithValue(context.Background(), "key", "value01")
go func() {
for {
_ = context.WithValue(ctx, "key", "value02")
}
}()
go func() {
for {
_ = context.WithValue(ctx, "key", "value03")
}
}()
go func() {
for {
fmt.Println(ctx.Value("key"))
}
}()
go func() {
for {
fmt.Println(ctx.Value("key"))
}
}()
time.Sleep(3 * time.Second)
}
程序是没有问题的,接下来就来看一下为什么context
是线程安全的.
为什么线程安全?
context
包提供两种创建根context
的方式:
-
context.Backgroud()
-
context.TODO()
又提供了四个函数(Value、Deadline、Done、Err)基于父Context
衍生,其中使用WithValue
函数来衍生context
并携带数据,每次调用WithValue
函数都会基于当前context
衍生一个新的子context
,WithValue
内部主要就是调用valueCtx
类:
func WithValue(parent Context, key, val interface{}) Context {
if key == nil {
panic("nil key")
}
if !reflectlite.TypeOf(key).Comparable() {
panic("key is not comparable")
}
return &valueCtx{parent, key, val}
}
说明:这里的parent是当前valueContext的父节点。
valueCtx
结构如下:
type valueCtx struct {
Context
key, val interface{}
}
valueContext继承父Context,这种是采用匿名接口的继承实现方式,key、val用来存储携带的键值对。
通过上面的代码分析,可以看到添加键值对不是在原Context结构体上直接添加,而是以此context作为父节点,重新创建一个新的valueContext子节点,将键值对添加到子节点上,由此形成一条context链。
获取键值对的过程也是层层向上调用直到首次设置key的父节点,如果没有找到首次设置key的父节点,会向上遍历直到根节点,如果根节点找到了key就会返回,否则就会找到最终的emptyCtx返回nil。如下图所示:
总结:context添加的键值对是一个链式的,会不断衍生新的context,所以context本身是不可变的,因此是线程安全的。