目录
1 context的应用场景
在学习这个包前我们肯定得清楚这个包的应用场景,知道什么时候用,我们的学习才有方向。
再次我简单举例:
- 业务需要对访问的数据库,RPC ,或API接口,为了防止这些依赖导致我们的服务超时,需要针对性的做超时控制。
- 为了详细了解服务性能,记录详细的调用链Log。
Go 语言中的每一个请求的都是通过一个单独的Goroutine进行处理的,HTTP/RPC请求的处理器往往都会启动新的 Goroutine访问数据库和 RPC 服务,我们可能会创建多个 Goroutine 来处理一次请求,而 Context 的主要作用就是在不同的 Goroutine 之间同步请求特定的数据、取消信号以及处理请求的截止日期
。
每一个 Context 都会从最顶层的 Goroutine 一层一层传递到最下层,这也是Golang中上下文最常见的使用方式,如果没有 Context,当上层执行的操作出现错误时,下层其实不会收到错误而是会继续执行下去:
2 创建context
Context提供了两个方法做初始化
// Background 一般是所有 Context 的基础,所有 Context 的源头都应该是它。
func Background() Context{}
// TODO 方法一般用于当传入的方法不确定是哪种类型的 Context 时,
// 为了避免 Context 的参数为nil而初始化的 Context。
func TODO() Context {}
这两个方法都是通过 new(emptyCtx) 语句初始化的,它们是指向私有结构体 context.emptyCtx 的指针,这是最简单、最常用的上下文类型。context.Background 和 context.TODO 函数其实也只是互为别名,没有太大的差别。它们只是在使用和语义上稍有不同。
- context.Background 是上下文的默认值
- context.TODO 应该只在不确定应该使用哪种上下文时使用
- 如果当前函数没有上下文作为入参,我们都会使用 context.Background 作为起始的上下文向下传递
var (
background = new(emptyCtx)
todo = new(emptyCtx)
)
type emptyCtx int
// Deadline 返回 context.Context 被取消的时间,也就是完成工作的截止日期;
func (*emptyCtx) Deadline() (deadline time.Time, ok bool) {
return
}
// Done 返回一个 Channel,这个 Channel 会在当前工作完成或者上下文被取消之后关闭,
// 多次调用 Done 方法会返回同一个 Channel
func (*emptyCtx) Done() <-chan struct{} {
return nil
}
// Err — 返回 context.Context 结束的原因,它只会在 Done 返回的 Channel 被关闭时才会返回非空的值;
// 如果 context.Context 被取消,会返回 Canceled 错误;
// 如果 context.Context 超时,会返回 DeadlineExceeded 错误;
func (*emptyCtx) Err() error {
return nil
}
// Value 从 context.Context 中获取键对应的值,对于同一个上下文来说,多次调用 Value 并传入相同的 Key 会返回相同的结果,该方法可以用来传递请求特定的数据;
func (*emptyCtx) Value(key interface{}) interface{} {
return nil
}
func (e *emptyCtx) String() string {
switch e {
case background:
return "context.Background"
case todo:
return "context.TODO"
}
return "unknown empty Context"
}
2.1 派生Context方法
func WithCancel(parent Context) (ctx Context, cancel CancelFunc){}
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {}
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {}
上面三种方法比较类似均会基于 parent Context生成一个子ctx,以及一个Cancel方法。如果调用了cancel 方法,ctx 以及基于ctx构造的子context都会被取消。不同点在于
- WithCancel必须要手动调用cancel 方法。
- WithDeadline可以设置一个时间点。
- WithTimeout是设置调用的持续时间,到指定时间后,会调用cancel做取消操作。
func WithValue(parent Context, key, val interface{}) Context
withValue 会构造一个新的context,新的context 会包含一对 Key-Value 数据,可以通过Context.Value(Key) 获取存在 ctx 中的 Value 值。
2.2 举例(withCancel)
下面是一个使用WithCancel
的例子:
func main() {
// 创建一个父context
parentCtx := context.Background()
// 创建一个带有取消功能的子context
ctx, cancel := context.WithCancel(parentCtx)
// 启动一个goroutine执行一些任务
go func() {
// 模拟耗时操作
time.Sleep(3 * time.Second)
// 取消子context
cancel()
}()
// 在主goroutine中检查是否已经取消
select {
case <-ctx.Done():
fmt.Println("Task cancelled")
case <-time.After(5 * time.Second):
fmt.Println("Task completed successfully")
}
}
在上面的例子中,我们创建了一个父context parentCtx
,然后使用WithCancel
创建了一个带有取消功能的子context ctx
,同时返回了一个cancel
函数。在goroutine中,我们模拟了一个耗时操作,并在操作完成后调用了cancel
函数来取消子context。
在主goroutine中,我们使用select
语句来检查是否已经取消。如果已经取消,我们会收到一个来自ctx.Done()
通道的信号,然后打印出"Task cancelled"。如果没有取消,我们会等待5秒钟后打印出"Task completed successfully"。
通过使用WithCancel
,我们可以手动控制何时取消一个context,并在需要的时候执行相应的操作。
// 输出结果
Task cancelled
2.3 举例(WithDeadline)
下面是一个使用WithDeadline
的例子:
func main() {
// 创建一个父context
parentCtx := context.Background()
// 设置截止时间为2秒后
deadline := time.Now().Add(2 * time.Second)
// 创建一个带有截止时间的子context
ctx, cancel := context.WithDeadline(parentCtx, deadline)
// 在主goroutine中检查是否已经超时
select {
case <-ctx.Done():
fmt.Println("Task deadline exceeded")
case <-time.After(1 * time.Second):
fmt.Println("Task completed successfully")
}
// 取消子context
cancel()
}
在上面的例子中,我们创建了一个父context parentCtx
。然后,我们使用time.Now().Add(2 * time.Second)
来计算一个截止时间,即2秒后的时间点。接下来,我们使用WithDeadline
创建了一个带有截止时间的子context ctx
,同时返回了一个cancel
函数。
在主goroutine中,我们使用select
语句来检查是否已经超时。如果超时了,我们会收到一个来自ctx.Done()
通道的信号,然后打印出"Task deadline exceeded"。如果没有超时,我们会等待1秒钟后打印出"Task completed successfully"。
最后,我们调用cancel
函数来取消子context。
通过使用WithDeadline
,我们可以设置一个具体的时间点作为截止时间,并在超过该时间点后执行相应的操作。
// 输出结果为
Task completed successfully
2.4举例(WithTimeout)
使用WithTimeout
的例子:
func main() {
// 创建一个父context
parentCtx := context.Background()
// 设置超时时间为3秒
timeout := 3 * time.Second
// 创建一个带有超时的子context
ctx, cancel := context.WithTimeout(parentCtx, timeout)
// 在主goroutine中检查是否已经超时
select {
case <-ctx.Done():
fmt.Println("Task timeout")
case <-time.After(5 * time.Second):
fmt.Println("Task completed successfully")
}
// 取消子context
cancel()
}
在上面的例子中,我们创建了一个父context parentCtx
。然后,我们设置了一个超时时间为3秒。接下来,我们使用WithTimeout
创建了一个带有超时时间的子context ctx
,同时返回了一个cancel
函数。
在主goroutine中,我们使用select
语句来检查是否已经超时。如果超时了,我们会收到一个来自ctx.Done()
通道的信号,然后打印出"Task timeout"。如果没有超时,我们会等待5秒钟后打印出"Task completed successfully"。
最后,我们调用cancel
函数来取消子context。
通过使用WithTimeout
,我们可以设置一个持续时间作为超时时间,并在超过该时间后执行相应的操作。
// 输出结果为
Task timeout
2.5 举例(WithValue)
下面是一个使用WithValue
的例子:
type key string
func main() {
// 创建一个父context
parentCtx := context.Background()
// 在父context中设置一个key-value对
parentCtx = context.WithValue(parentCtx, key("name"), "Alice")
// 在子context中获取父context中的value
ctx := context.WithValue(parentCtx, key("age"), 30)
// 在子context中获取value并打印
name := ctx.Value(key("name")).(string)
age := ctx.Value(key("age")).(int)
fmt.Printf("Name: %s, Age: %d\n", name, age)
}
在上面的例子中,我们创建了一个父context parentCtx
。然后,我们使用WithValue
在父context中设置了一个key-value对,其中key是自定义类型key
,value是字符串"Alice"
。
接下来,我们使用WithValue
创建了一个子context ctx
,同时在子context中设置了另一个key-value对,其中key是相同的自定义类型key
,value是整数30
。
在子context中,我们可以使用ctx.Value(key)
来获取父context中设置的value。我们使用断言将获取的value转换为相应的类型,并打印出来。
通过使用WithValue
,我们可以在context中存储一些与特定任务相关的数据,并在需要时进行访问和使用。
//结果为
Name: Alice, Age: 30
3 总结
Go 语言中的 context.Context 的主要作用还是在多个 Goroutine 组成的树中同步取消信号以减少对资源的消耗和占用,虽然它也有传值的功能,但是这个功能我们还是很少用到。在真正使用传值的功能时我们也应该非常谨慎,使用 context.Context 进行传递参数请求的所有参数一种非常差的设计,比较常见的使用场景是传递请求对应用户的认证令牌以及用于进行分布式追踪的请求 ID。