今天遇到个很有意思的一段代码,这段程序会打印出什么结果:

  package main
 
  import (
    "context"
    "fmt"
  )
 
  func f(ctx context.Context) {
    context.WithValue(ctx, "foo", -6)
  }
 
  func main() {
    ctx := context.TODO()
    f(ctx)
    fmt.Println(ctx.Value("foo"))
    // -6
    // 0
    // <nil>
    // panic
  }

先让我们看看 context.TODO() 返回的结果是什么:

  var (
    background = new(emptyCtx)
    todo       = new(emptyCtx)
  )
 
  type emptyCtx int
 
  func TODO() Context {
    return todo
  }

context.TODO() 返回的实例返回的正是一个 emptyCtx 对象,也就是 int ,它不能被 cancel,也不包含任何值,并且也没有 deadline。同时也不是一个空的结构体 struct{} ,因为它需要一个目标地址。

那么 context.WithValue 做了些什么事情呢:

type WithValue(parent Context, key, val interface{}) Context {
    if parent == nil {
        panic("cannot create context from nil parent")
    }
    if key == nil {
        panic("nil key")
    }
    if !reflectlite.TypeOf(key).Comparable() {
        panic("key is not comparable")
    }
    return &valueCtx{parent, key, val}
}
 
func valueCtx struct {
    Context,
    key, val interface{}
}

看到这里其实我们一开始的程序的结果已经很明显了,WithValue 每次都会返回一个新的带有 key-value 值的上下文对象 valueCtx ,如果没有重新赋值,那么我们的 key-value 就会被丢失,并不会携带下去。

那么 context.Value 是怎么查找值的呢:

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

在查找指定 key 时,会先从当前的 context 对象中查看是否存在对应的 key,没有的话则回溯到 parent context 进行查找,那么什么时候是查找的尽头呢:

func (*emptyCtx) Value(key interface{}) interface{} {
	return nil
}

查找的尽头正是当 context 是一开始的 emptyCtx 空实现上下文对象时。

也正是因为 valueCtx 的实现如上面这样,是一种嵌套的结构,并且每次都是生成一个新的对象,官方的建议在使用时应该只传递必要的参数,来减少它的层级和数据的大小:

WithValue returns a copy of parent in which the value associated with key is val.
Use context Values only for request-scoped data that transits processes and APIs, not for passing optional parameters to functions.