「Go」Contex使用详解

618 阅读4分钟

什么是Context

总结来说Context就是用来在父子goroutine间进行值传递以及发送cancel信号的一种机制。

为什么要使用Context

在golang中的创建一个新的协程并不会返回像c语言创建一个线程一样类似的pid,这样就导致我们不能从外部杀死某个线程,所以我们就得让它自己结束。(goroutine不能返回pid的原因,应该是协程的实现原理有很大关系,多个协程对应1个线程的实现机制。)

当然我们可以采用channel+select的方式,来解决这个问题,不过场景很复杂的时候,我们就需要花费很大的精力去维护channel与这些协程之间的关系,这就导致了我们的并发代码变得很难维护和管理。

虽然goroutine之间是平行的,没有继承关系,但是Context设计成是包含父子关系的,这样可以更好的描述goroutine调用之间的树型关系。

Context的使用

接口定义

context.Context 是 Go 语言在 1.7 版本中引入标准库的接口,该接口定义了四个需要实现的方法,包括:

  • 1.Deadline — 返回 context.Context 被取消的时间,也就是完成工作的截止日期;
  • 2.Done — 返回一个 Channel,这个 Channel 会在当前工作完成或者上下文被取消之后关闭,多次调用 Done 方法会返回同一个 Channel;
  • 3.Err — 返回 context.Context 结束的原因,它只会在 Done 返回的 Channel 被关闭时才会返回非空的值;
    • 如果 context.Context 被取消,会返回 Canceled 错误;
    • 如果 context.Context 超时,会返回 DeadlineExceeded 错误;
  • 4.Value — 从 context.Context 中获取键对应的值,对于同一个上下文来说,多次调用 Value 并传入相同的 Key 会返回相同的结果,该方法可以用来传递请求特定的数据;

代码如下

type Context interface {
	Deadline() (deadline time.Time, ok bool)
	Done() <-chan struct{}
	Err() error
	Value(key interface{}) interface{}
}

常用方法

context包定义的方法如下:

func Background() Context  //生成父上下文
func TODO() Context    //生成父上下文

func WithCancel(parent Context) (ctx Context, cancel CancelFunc)    //生成子上下文
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)   //生成子上下文
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)    //生成子上下文
func WithValue(parent Context, key, val interface{}) Context    //生成子上下文
context.Background、context.TODO

context 包中最常用的方法就是context.Backgroundcontext.TODO,这两个方法都会返回预先初始化好的私有变量 background 和 todo,它们会在同一个 Go 程序中被复用.

从源码来看,context.Backgroundcontext.TODO 函数其实也只是互为别名,没有太大的差别。它们只是在使用和语义上稍有不同:

1.context.Background 是上下文的默认值,所有其他的上下文都应该从它衍生(Derived)出来;

2.context.TODO 应该只在不确定应该使用哪种上下文时使用; 在多数情况下,如果当前函数没有上下文作为入参,我们都会使用 context.Background 作为起始的上下文向下传递。

context.WithValue

context 包中的 context.WithValue 函数能从父上下文中创建一个子上下文用于传值

func func1(ctx context.Context) {
	ctx = context.WithValue(ctx, "k1", "v1")
	func2(ctx)
}
func func2(ctx context.Context) {
	fmt.Println(ctx.Value("k1").(string))
}

func main() {
	ctx := context.Background()
	func1(ctx)  //v1
}
context.WithCancel

context.WithCancel 函数能够从 context.Context 中衍生出一个新的子上下文以及用于取消该上下文的函数(CancelFunc)。一旦我们执行返回的取消函数,当前上下文以及它的子上下文都会被取消,所有的 Goroutine 都会同步收到这一取消信号。

func isCancelled(ctx context.Context) bool {
	select{
	case <-ctx.Done():	//通过ctx.Done 判断任务是否取消
		return true
	default :
		return false
	}
}

func TestCancel(t *testing.T){
	ctx,cancel := context.WithCancel(context.Background())	//创建context
	for i:=0;i<5;i++{
		go func(i int,ctx context.Context) {	//将context传入每个协程中
			for {
				if isCancelled(ctx) {
					break
				}
				time.Sleep(time.Millisecond * 5)
			}
			fmt.Println(i,"Cancelled")
		}(i,ctx)
	}
	cancel()	//发送cancel通知
	time.Sleep(time.Second * 1)
}

context.WithDeadline、context.WithTimeout

除了 context.WithCancel 之外,context 包中的另外两个函数 context.WithDeadline 和 context.WithTimeout 也都能创建可以被取消的计时器上下文。context.WithDeadline 方法在创建 context.timerCtx 的过程中,判断了父上下文的截止日期与当前日期,并通过 time.AfterFunc 创建定时器,当时间超过了截止日期后会调用 context.timerCtx.cancel 方法同步取消信号。

context.WithTimeout 代码示例

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
    defer cancel()

    select {
    case <-time.After(1 * time.Second):
        fmt.Println("overslept")
    case <-ctx.Done():
        fmt.Println(ctx.Err()) // prints "context deadline exceeded"
    }
}

context.WithDeadline 代码示例

func main() {
    d := time.Now().Add(50 * time.Millisecond)
    ctx, cancel := context.WithDeadline(context.Background(), d)
    defer cancel()

    select {
    case <-time.After(1 * time.Second):
        fmt.Println("overslept")
    case <-ctx.Done():
        fmt.Println(ctx.Err())
    }
}

小结

Go 语言中的 context.Context 的主要作用还是在多个 Goroutine 组成的树中同步取消信号以减少对资源的消耗和占用,虽然它也有传值的功能,但是这个功能我们还是很少用到。

在真正使用传值的功能时我们也应该非常谨慎,使用 context.Context 进行传递参数请求的所有参数一种非常差的设计,比较常见的使用场景是传递请求对应用户的认证令牌以及用于进行分布式追踪的请求 ID。