Golang中的context之传值(一)

2018/04/29

前几天在工作中使用context的时候,对于metadata和context.Value有一些疑惑,顺便探索了一下go的context,在这里分享一下

现在是2018-04-29 19:49,正在北京开往家的动车上,窗外已经是黑黑的一片了,11点到家,写一会文章吧

context包

这里说的context包指的是golang(1.7+)中的标准库context,文档在这里:https://godoc.org/context

context包定义了Context类型,他可以在携带deadlines和cancelatin信号,也可以携带其他的请求值。

发送到server的请求应该创建一个context,服务器处理请求的时候也应该接受一个context。在程序的函数调用链中,必须传递context,也可以基于一个context,使用WithCancel, WithDeadline, WithTimeout, or WithValue等函数创建一个新的context传递下去。当一个context被canceled的时候,那么所有通过改context派生的context都应该被cancel

WithCancel, WithDeadline, and WithTimeout这三个函数使用一个context(the parent) 作为参数,然后创建一个新的context(the child)和CancelFunc函数。调用CancelFunc函数将会取消该child和他的children,移除parent到child的应用,然后停止任何相关的timeers。吐过调用这个函数失败的话,会导致child和他的children知道parent cancel的时候或者timer fired的时候才会取消。go vet工具会检查CancelFuncs对否在所有的控制分支上使用了。

context的使用规则 * context应该作为函数参数传递,而不是struct的一个field * context应该是函数的第一个参数 * 不要传递nil context,即使是不使用,如果不知道用啥,用 context.TODO 吧 * 只使用context传递请求上下文,而不是为了传递可选参数 * context可能会被同一个函数在不同的goroutine中使用,他是并发安全的

context接口

以下是context的接口定义

type Context interface {
    // 返回当前context应该被cancel的时间
    // 如果 ok==false 的话,那么当前context没有deadline
    Deadline() (deadline time.Time, ok bool)

    // 返回了一个chan,当 当前context被cancel的时候,这个chan就会被close
    // 如果这个context永远也不会被cancel的时候,会返回一个nil
    //
    // WithCancel会在cancel的遍历Done
    // WithDeadline
    // WithTimeout
    //
    // Done是为了在select中使用提供的
    //
    //  // Stream generates values with DoSomething and sends them to out
    //  // until DoSomething returns an error or ctx.Done is closed.
    //  func Stream(ctx context.Context, out chan<- Value) error {
    //          for {
    //                  v, err := DoSomething(ctx)
    //                  if err != nil {
    //                          return err
    //                  }
    //                  select {
    //                  case <-ctx.Done():
    //                          return ctx.Err()
    //                  case out <- v:
    //                  }
    //          }
    //  }
    //
    // See https://blog.golang.org/pipelines for more examples of how to use
    // a Done channel for cancelation.
    Done() <-chan struct{}

    // 如果Done还没有被close,返回nil
    // 如果Done已经被clode了,返回一个non-nil的err,有几个err:
    // Canceled: context是被cancel的
    // DeadlineExceeded: context的deadline过了
    // 如果Err返回了一个non-nil,那么以后返回的结果也都是一样的
    Err() error

    // 返回指定的key的值,如果没有,返回nil
    //
    // 一个key指定了context中唯一的value。
    // key可以是任何可以比较的类型,(golint不允许使用string)
    // 各个包应该定义自己的非导出的类型所谓key,以避免重叠
    Value(key interface{}) interface{}
}

context是如何实现k-v键值对存储的

什么是contextk-v存储

通过下面这样的代码可以在goroutine之间安全的传递数据

type key = struct{}
ctx := context.Background()
ctx = context.WithValue(ctx, key{}, "this is value")
fmt.Printf("value: %v\n", ctx.Value(key{}))

简析

context值的实现和cancel等的实现是基于不同的struct的,这些struct都和接口Context组合了,所以都实现了接口Context(参见这篇我的博文

这种思想是可以借鉴的,即先定义一个interface,然后不同的struct组合这个interface,然后实现不同的方法

context的k-v对存储是一个树状的结构,每个节点都存储一对k-v,并指向父context

代码解析

下面结合k-v实现的代码看一下

// 定义存储k-v的struct,是一个树结构
type valueCtx struct {
    Context
    key, val interface{}
}

// String接口
// 其中的c.Context是通过`%v`格式化的,所以这个函数实际上是一个递归函数
// 这里使用%v作为递归的方法而不是c.Context.String(),是因为Context接口没有定义String方法,而valueCtx没有定义,这一点值得学习
func (c *valueCtx) String() string {
    return fmt.Sprintf("%v.WithValue(%#v, %#v)\n", c.Context, c.key, c.val)
}

// 取值方法
// 先判断当前context中是否有该存储的值
// 没有就使用父context取,也是一个递归函数
func (c *valueCtx) Value(key interface{}) interface{} {
    if c.key == key {
        return c.val
    }
    return c.Context.Value(key)
}

// 赋值方法
// 实际上是返回一个struct,这个struct里面有key和val的field,作为k-v的存储载体
// 然后为了保留意见设置的数据,需要把老context传下去
// 所以大概是这么个格式:{{{context.TODO(), key1, val1}, key2, val2}, key3, val3}
// 所以如果在同一个key上设置了两个值,那么旧的值永远不会取出来,因为会先取到后设置的值;所以也可以直接理解为一个map(实际上复杂度不是O(1)的)
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}
}

context中的value存储与medatada

在grpc中,client与server之间通过context传递上下文数据的时候,不能使用context.WithValue。

因为server端无法获取到client自定义的key(是这个key本身的原因还是grpc做了限制?)

那么如何在client与server之间传递数据呢?可以使用grpc提供的medadata接口:

package main

import (
    "context"
    "fmt"
    "google.golang.org/grpc/metadata"
)

func main() {
    // client
    md := metadata.Pairs(
        "k1", "v1",
        "k2", "v2",
    )
    ctx := metadata.NewOutgoingContext(context.Background(), md)

    // server
    md2, ok := metadata.FromIncomingContext(ctx)
    if !ok {
        panic("no metadata")
    }

    fmt.Printf("k1 %v\n", md2["k1"])
    fmt.Printf("k2 %v\n", md2["k2"])
}

这里使用了metadata.NewOutgoingContext传递数据,metadata.FromIncomingContext获取数据。

这里其实就是通过context.WithValue实现的,只不过key分别是指定的mdOutgoingKey{}mdIncomingKey{}

mdOutgoingKey{}mdIncomingKey{}之间的区别就是,在client端发送数据的时候选用metadata.NewOutgoingContext,在server接受数据的时候,选用metadata.FromIncomingContext

参考文章