Go语言中Context使用技巧

Go的Context是一个设计非常精巧的接口,我们可以使用它非常方便进行上下文的值传递,同时也控制goroutine的生命周期。

1. 常用功能

1.1 值传递

Context提供了一个WithValue 函数,可将一对 key/value 的值存放到Context中

func TestContextWithValue(t *testing.T) {
	ctx := context.WithValue(context.Background(), "name", "派大星")
	fmt.Println(ctx.Value("name"))
}

1.2 超时取消

很多第三方类库都会有一个超时时间,当执行某个操作超过我们设置时间时,就不再继续进行操作了,而是返回一个超时错误,他们实现的方式基本也都是使用Context的超时取消功能。

func TestContextWithTimeout(t *testing.T) {
	ctx := context.Background()
	ctx, _ = context.WithTimeout(ctx, time.Second*3)
	select {
	case <-ctx.Done():
		fmt.Println("已超时,结束执行")
	}
}

1.3 定时取消某Goroutinue

当我们做某些对时间有限制的逻辑时,比如到某时刻停止某个协程时,就可以使用该函数。

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
	}
}

1.4 手动取消

Context 提供了一个WithCancel的函数可以帮我们手动取消该Context,该函数会返回一个Cancel方法,我们调用此方法即可。

func TestContextWithCancel(t *testing.T) {
	ctx := context.Background()
	ctx, cancel := context.WithCancel(ctx)
	cancel()
	select {
	case <-ctx.Done():
		fmt.Println("手动取消")
	}
}

1.5 功能实现说明

其实无论是超时取消,定时取消还是手动取消,它本质都是往Context的Done通道里面放一个值罢了,我们通过监听Done通道来达到实现这些目的。

2. 坑

2.2 子Context覆盖父Context的key

源码
Go语言中Context使用技巧_第1张图片

通过源码我们可以看到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"))
}

Go语言中Context使用技巧_第2张图片

2.2 GRPC无法传值问题

当我们调用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)
}

2.3 Context传递性问题

Go语言中Context使用技巧_第3张图片

这种情况下,被调用的 rpc 是收不到请求的。 PingGo 先return 掉了,整个 context 已经处于 cancel 的状态了。

除此之外,像gin框架,也会有类似的问题
Go语言中Context使用技巧_第4张图片

解决方案

重新创建一个 context,将该context作为调用rpc 传递的参数

你可能感兴趣的:(go)