Context.Value 实现和查找原理

context.Context 的 WithValue 方法是创建了一个 valueCtx 对象,用于在 goroutin 之间传递数据。

valueCtx 结构的定义如下:

type valueCtx struct {
    Context
    key, val interface{}
}

valueCtx 实现了两个方法:

func (c *valueCtx) String() string {
    return fmt.Sprintf("%v.WithValue(%#v, %#v)", c.Context, c.key, c.val)
}
func (c *valueCtx) Value(key interface{}) interface{} {
    if c.key == key {
        return c.val
    }
    return c.Context.Value(key)
}

由于它直接将 Context 作为匿名字段,因此它继承了父 context,还增加了 2 个方法。valueCtx 仍然是一个 Context 类型。

创建 valueCtx 的函数:

func WithValue(parent Context, key, val interface{}) Context {
    if key == nil {
        panic("nil key")
    }
    if !reflect.TypeOf(key).Comparable() {
        panic("key is not comparable")
    }
    return &valueCtx{parent, key, val}
}

key 要求必须是可比较类型,因为需要通过 key 比较来获取 context 中的值,所以可比较是必须的。

通过层层传递 context,最终形成这样一棵树:

context.Value 的查找过程

和链表有点类似,只是方向相反:Context 指向它的父节点,链表则指向下一个节点。通过 WithValue 函数,可以创建多层 valueCtx,存储 goroutine 间可以共享的变量。

取值的过程,实际上是一个递归查找的过程:

func (c *valueCtx) Value(key interface{}) interface{} {
    if c.key == key {
        return c.val
    }
    return c.Context.Value(key)
}

它会顺着链路一直往上找,比较当前节点的 key 是否是要找的 key,如果是,则直接返回 value。否则,一直顺着 context 往前,最终找到根节点(一般是 emptyCtx),直接返回一个 nil。所以用 Value 方法的时候要判断结果是否为 nil。

因为查找方向是往上查找,所以,父节点没法获取子节点存储的值,子节点却可以获取父节点的值。

WithValue 创建 context 节点的过程实际上就是创建链表节点的过程。两个节点的 key 值是可以相等的,但它们是两个不同的 context 节点。查找的时候,会向上查找到最后一个挂载的 context 节点,也就是离得比较近的一个父节点 context。所以,整体上而言,用 WithValue 构造的其实是一个低效率的链表。

如果你接手过项目,肯定经历过这样的窘境:在一个处理过程中,有若干子函数、子协程。各种不同的地方会向 context 里塞入各种不同的 k-v 对,最后在某个地方使用。 你根本就不知道什么时候什么地方传了什么值?这些值会不会被“覆盖”?这也是 context.Value 最受争议的地方。所以,很多人建议尽量不要通过 context 传值。

下一章:Go defer 原理和源码剖析

Go 语言中有一个非常有用的保留字 defer,defer 语句可以调用一个函数,该函数的执行被推迟到包裹它的函数返回时执行。defer 语句调用的函数,要么是因为包裹它的函数执行了 return 语句,到 ...