前几天在工作中使用 grpc 和 context 的时候,对于 grpc 的 metadata 和 context 的 context.Value 有一些疑惑,顺便探索了一下 go 的 context,在这里分享一下。
现在是 2018-04-29 19:49,正在北京开往家的动车上,窗外已经是黑黑的一片了,11 点到家,写一会文章吧
context 包
这里说的 context 包指的是 golang(1.7+)中的标准库 context,文档在这里:https://pkg.go.dev/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 存储与 grpc medatada
在 grpc 中,client 与 server 之间通过 context 传递上下文数据的时候,不能使用 context.WithValue。
因为 context.Context 是没有办法跨网络传输的, grpc 需要定义自己的 Context 实现,并为这个 Context 实现基于网络的数据传输、序列化和反序列化方法。
那么如何在 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