学习GO:学习context包

120 阅读6分钟

早在2016年8月,Go 1.7发布了,它包括一个新的包,名为 context.这个包最初是在 golang.org/x/net/context中实现的,但在这次发布中,它被复制到了标准库中。

这一变化使得其他标准库包,如netnet/httpdatabase/sql ,需要更新以增加对context 包的支持**,但**不会破坏现有的API。这就是为什么我们看到名称和参数相似的函数,但多加了一个context 的参数作为第一个参数,比如database/sql.*DB '的Ping 方法。

func (db *DB) Ping() error
func (db *DB) PingContext(ctx context.Context) error

作出这个决定是因为第一版的兼容性承诺,然而在幕后,这些方法大多使用context ,但有默认值,通常是context.Background()

func (db *DB) Ping() error {
	return db.PingContext(context.Background())
}

关于这些新方法,需要注意的一个重要事情是,他们定义了一个事实上的惯例,如果我们需要在我们的函数中使用context.Context ,那么它应该是第一个参数,称为ctx

让我们深入了解一下context 包。


context 包里有什么?

context 包定义了一个叫做Context 的类型,用于最后期限取消信号以及使用请求范围内的值的方法。

context.Context 是一个接口类型,定义了四个函数。

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

根据我们计划如何使用context.Context ,我们可能会或可能不会使用所有的方法。

  • Deadline():返回上下文应该结束的时间,如果没有设置最后期限,那么返回的bool值是false 。这个函数在收到context ,我们想计算是否有足够的时间来完成要做的工作的情况下很有用。
  • Done(): 返回一个在上下文结束时被关闭的通道,这个通道的关闭方式取决于context.Context 的初始化方式,具体细节请参考文档。
  • Err():如果有错误,那么当Done() 中返回的通道被关闭时,它会返回一个非零的错误,要么是context.DeadlineExceeded ,要么是context.Canceled ,否则是nil
  • Value(key interface{}): 它用于获取存储在上下文中的请求范围内的值,与以下内容一起使用 context.WithValue.

很可能你已经以某种方式使用了context ,例如你正在使用database/sqlnet/http ;一些项目严重依赖context 来实现他们的目标,例如OpenTelemetry大量使用它来做仪器。

最后期限

截止日期定义了一种用时间来表示某件事情已经完成的方式,有两种方式可以做到。

Deadlines

  • context.WithDeadline: 使用一个具体的time.Time 来表示上下文应该何时结束,以及
  • context.WithTimeout:使用一个相对的time.Duration 来指示上下文应该何时结束。

如果你参考源代码,你会注意到context.WithTimeout 在幕后使用了context.WithDeadline ,但在当前时间上增加了超时时间来表示最后期限。

func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
	return WithDeadline(parent, time.Now().Add(timeout))
}

这两个函数都返回一个新的context.Context 以及一个context.CancelFunc 函数;这个新的context.Context 是父函数的副本,上面附加了截止日期的细节,它的目的是作为任何后续调用的参数,这些调用应该是使用截止日期。

当使用新的context.Context 的相应代码块完成后,应该调用该函数context.CancelFunc (通常通过defer ),这是为了在最后期限到达的情况下,将取消传播给使用context 的其他函数。

比如说

ctx, cancel := context.WithTimeout(context.Background(), 1 * 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 deadline exceeded ,因为我们在调用context.WithTimeout 中指出的值是1毫秒,如果我们把这个值修改为高于1秒的值,那么它就会打印出overslept

这是因为select 期待两个通道中的一个收到消息,要么是由time (通过time.After() )返回的,要么是在context (通过ctx.Done() )指示的。

取消信号

取消信号定义了一种方法,通过明确地调用CancelFunc 函数来表示某事已经完成,有一种方法可以做到。

Cancellation

  • context.WithCancel:它返回一个上下文的副本和一个CancelFunc ,以指示何时取消一些工作。

类似于截止日期,对于取消信号,会返回一个context.Context以及一个CancelFunc ;这个函数应该被显式调用,以指示当返回的context被取消时,传播到使用相同context的其他函数,工作应该停止。

context.WithCancelcontext.WithDeadline/context.WithTimeout 的区别在于明确性,所以应该明确调用CancelFunc 而不是定义一个超时。在所有这三种情况下,我们总是需要调用返回的CancelFunc ,以正确地将取消的细节传播给使用相同上下文的其他函数。

当使用新的context.Context 的相应代码块完成后,应该调用函数context.CancelFunc (通常是通过defer ),这是为了将取消的情况传播给使用context 的其他函数,以防达到最后期限。

比如说

ch := make(chan struct{})

run := func(ctx context.Context) {
	n := 1
	for {
		select {
		case <-ctx.Done(): // 2. "ctx" is cancelled, we close "ch"
			fmt.Println("exiting")
			close(ch)
			return // returning not to leak the goroutine
		default:
			time.Sleep(time.Millisecond * 300)
			fmt.Println(n)
			n++
		}
	}
}

ctx, cancel := context.WithCancel(context.Background())
go func() {
	time.Sleep(time.Second * 2)
	fmt.Println("goodbye")
	cancel() // 1. cancels "ctx"
}()

go run(ctx)

fmt.Println("waiting to cancel...")

<-ch // 3. "ch" is closed, we exit

fmt.Println("bye")

上面的代码比用于Deadlines的代码要详细一些,关键部分是goroutine中明确的cancel() 调用;该cancel() 调用最终是停止接收可取消上下文run 函数。

请求范围的值

Request-scoped values定义了一种设置和获取适用于context.Context具体实例的值的方法,它们在用户请求期间使用,例如在HTTP请求期间,将信息传递给后续的内部调用,有一种方法可以做到。

Request-scoped values

比如说

ctx := context.WithValue(context.Background(), auth, "Bearer hi")

//-

bearer := ctx.Value(auth)
str, ok := bearer.(string)
if !ok {
	log.Fatalln("not a string")
}

fmt.Println("value:", str)

上面的代码同时使用了context.WithValuecontext.Context.Value;context.WithValue 返回一个带有相关值和键的父级上下文的副本,然后我们可以使用这个返回的上下文并调用其方法Value 来获得之前分配的值。

context.WithValue 的复杂性不在于实现或使用,而在于何时进行这些调用,回顾一下,使用这个函数的意义在于请求范围内的值,而不是在程序执行过程中要一直存在的东西。

一些常见的例子包括为JSON Web Tokens或额外的头文件定义一个值,在这两种情况下,这些值都是为了在多个请求中传递,以增加后续请求。

总结

在处理需要取消的指令时,理解如何使用context.Context 是很重要的,例如HTTP请求、数据库命令或远程生产调用;这不仅是因为我们需要定义一个合理的值来指示何时停止正在运行的请求以避免永远等待,而且还需要识别远程调用何时被取消以对该事件作出适当的反应。

context.Context 提供了一个更简单的方法来处理多个goroutine,以协调它们的工作,识别超时并确定这些超时发生的时间。因为仪表是任何分布式系统的一个重要部分,知道这些值是如何在不同的调用之间发送和定义的,对于理解我们程序的流程是很有用的。