Go的Context是一个设计非常精巧的接口,我们可以使用它非常方便进行上下文的值传递,同时也控制
goroutine
的生命周期。
Context提供了一个
WithValue
函数,可将一对 key/value 的值存放到Context中
func TestContextWithValue(t *testing.T) {
ctx := context.WithValue(context.Background(), "name", "派大星")
fmt.Println(ctx.Value("name"))
}
很多第三方类库都会有一个超时时间,当执行某个操作超过我们设置时间时,就不再继续进行操作了,而是返回一个超时错误,他们实现的方式基本也都是使用Context的超时取消功能。
func TestContextWithTimeout(t *testing.T) {
ctx := context.Background()
ctx, _ = context.WithTimeout(ctx, time.Second*3)
select {
case <-ctx.Done():
fmt.Println("已超时,结束执行")
}
}
当我们做某些对时间有限制的逻辑时,比如到某时刻停止某个协程时,就可以使用该函数。
func TestContextWithDeadLine(t *testing.T) {
ctx := context.Background()
deadTime := time.Now().Add(time.Second*3)
ctx, _ = context.WithDeadline(ctx, deadTime)
select {
case <-ctx.Done():
fmt.Println("到达deadLine,结束执行")
return
}
}
Context 提供了一个WithCancel的函数可以帮我们手动取消该Context,该函数会返回一个Cancel方法,我们调用此方法即可。
func TestContextWithCancel(t *testing.T) {
ctx := context.Background()
ctx, cancel := context.WithCancel(ctx)
cancel()
select {
case <-ctx.Done():
fmt.Println("手动取消")
}
}
其实无论是超时取消,定时取消还是手动取消,它本质都是往Context的Done通道里面放一个值罢了,我们通过监听Done通道来达到实现这些目的。
通过源码我们可以看到Context的Value方法是通过递归去获取key的值得,当前的Context如果获取不到时,它会去获取该Context的父Context,直到获取到或父Context为
nil
时为止,所以一旦子Context的key与父Context的key相同时,子Context会覆盖掉父Context中key的值,我们通过 子Context.Value(“key”) 方法只能拿到子Context的,而拿不到父Context的
测试一下
func TestContext(t *testing.T) {
ctx := context.Background()
ctx = context.WithValue(ctx, "name", "我是父Context")
ctx = context.WithValue(ctx, "name", "我是子Context")
fmt.Println(ctx.Value("name"))
}
当我们调用GRPC接口来传递Context里面的key/value时,我们会发现下游的GRPC接口是获取不到我们的key/value的,这是因为GRPC内部对Context做了转换导致的,解决方式是我们可以将信息放到metadata里面,然后GRPC下游接口可以从metadata里面获取信息。
import (
"context"
"fmt"
"google.golang.org/grpc/metadata"
"testing"
)
func TestContextWithMetadata(t *testing.T) {
ctx := context.Background()
md := metadata.New(map[string]string{
"user": "metadata-user-info",
})
// 存放metadata里
newCtx := metadata.NewOutgoingContext(ctx, md)
var user string
// 从metadata取
if md, ok := metadata.FromOutgoingContext(newCtx); ok {
if md["user"] != nil && len(md["user"]) > 0 {
user = md["user"][0]
}
}
fmt.Println(user)
}
这种情况下,被调用的 rpc 是收不到请求的。 PingGo 先return 掉了,整个 context 已经处于 cancel 的状态了。
解决方案
重新创建一个 context,将该context作为调用rpc 传递的参数