学习笔记-Golang中的Context
文章目录
- 1、什么是Context
- 2、Context的作用
- 3、Context的解析
- 3.1、Context的源码解析
- 3.2、 context包中实现context接口的四种结构体类型
- 3.2.1、emptyCtx
- 3.2.2、cancelCtx
- 3.2.3、timerCtx
- 3.2.4、valueCtx
- 4、总结
1、什么是Context
Context是 Go 语言中的一个标准库,用于在处理并发编程时,提供对请求的截止时间、取消信号以及请求范围内共享数据的管理。它在多个 goroutine 之间传递,是一种上下文管理工具。
Context 的主要目的是在不同的 goroutine 之间传递控制信号和数据。通过 Context,你可以:
- 设置请求的超时时间或截止时间。
- 传递取消信号,以便可以通知 goroutine 任务已经完成或需要停止。
- 在多个 goroutine 之间共享值。
2、Context的作用
设想这样一种场景:你正在开发一个 Web 应用,当用户发送一个请求(request)后,可能会启动多个 goroutine 来处理该请求的不同部分。如果单个请求的 goroutine 结构比较简单,处理起来也不麻烦,但是如果启动多个、深层次的 goroutine,则会导致代码的复杂性和可维护性变得更加困难。而有了context,我们就可以通过它来管理这些 goroutine,并在它们之间传递控制信号和数据。
Context 在这种场景中的作用主要体现在以下几个方面:
- 取消信号传递:处理用户请求的过程中,可能会因为用户主动取消请求或请求超时而需要提前停止某些 goroutine 的执行。Context 提供了一种机制,可以在多个 goroutine 之间传递取消信号,当父 Context 取消时,所有与之关联的子 goroutine 都会收到取消通知并可以安全地停止工作,避免不必要的资源消耗。
- 数据共享:在处理用户请求时,某些全局信息(例如用户认证信息、追踪 ID 等)需要在不同的 goroutine 之间共享。通过 Context,这些数据可以被安全地传递,而不需要通过全局变量或其他不安全的方式共享。这样可以保持代码的简洁性和安全性,尤其在并发环境下避免数据竞争。
- 超时控制:在处理用户请求时,可能会遇到一些耗时长的操作,例如数据库查询或网络请求。Context 提供了一种机制,可以设置超时时间,当超时时间到达后,Context 会自动取消相关的 goroutine,避免资源占用过多。
除了上述作用外,Context 还可以更高效地管理系统资源,例如在请求超时或任务完成时主动释放资源,避免资源浪费,防止 goroutine 泄漏。
3、Context的解析
3.1、Context的源码解析
Context 实际上是一个定义了4个方法的接口,凡是实现了该接口的都称为一种 Context。
type Context interface {
// 返回 context 的截止时间(deadline),表示任务应该在什么时候之前完成。
// 如果没有设置截止时间,就返回 false 和一个空的时间值。
Deadline() (deadline time.Time, ok bool)
//当 context 被取消或者超时时,这个方法返回的 channel 会被关闭。通过监听这个信号,可以决定是否要终止正在进行的任务。
Done() <-chan struct{}
//这个方法用来返回 context 被取消的具体原因,比如是因为超时、手动取消等,通常会在 Done() 关闭后使用。
Err() error
// 从 context 中获取与某个 key 关联的数据,用于在 goroutine 之间传递一些共享信息。
// key 可以是任何类型,用于区分不同的数据。
Value(key interface{}) interface{}
}
3.2、 context包中实现context接口的四种结构体类型
在context
包中,有四个结构体类型实现了Context接口,分别是emptyCtx
、cancelCtx
、timerCtx
和valueCtx
。
3.2.1、emptyCtx
emptyCtx
是一个没有任何取消、截止时间或值的 Context,它通常作为默认的上下文,在创建顶级 Context 时使用。
解释:
emptyCtx
实现了 Context 接口的所有方法,但它们都返回默认值或 nil,因此
emptyCtx`是没有实际功能的。那么它存在的目的是什么呢?
原因是,在 Go 的 Context 体系中,Context 对象通常是分层次的,即通过创建一个父 Context 来衍生出子 Context,并在这些子 Context 中传递控制信号、共享数据等。因此,为了构建这棵 Context 对象树,我们需要一个基础的、没有任何附加信息的根节点。emptyCtx 正是为了提供这样一个基本的起点。
此外,有时你在编写代码时需要一个 Context 占位符,但暂时不确定具体的 Context 类型。这时你可以使用 context.TODO(),它返回的实际上也是 emptyCtx。这样可以让代码先运行,稍后再决定具体的 Context 实现。
总之,emptyCtx
是一个没有任何功能的 Context,它的使用场景有两个:
- 用于顶层上下文,作为 Context 对象树的根节点。
- 作为占位符,用于在编写代码时暂时不确定具体的 Context 类型。
注意:这四种结构体中,只有emptyCtx
不需要通过已有context
实例创建,因此一个context树的根context一定是emptyCtx
。
使用示例:
ctx := context.Background()
// 或者
ctx := context.TODO()
// 以下代码中,ctx 就是一个空的上下文
3.2.2、cancelCtx
cancelCtx
是带有取消功能的 Context,它允许父 Context 取消,并通知所有子 Context 和相关 goroutine。
解释:
- cancelCtx 通过 WithCancel 函数创建,提供一种手动取消 Context 的机制。当调用 cancel() 函数时,所有依赖此 Context 的子 goroutine 都会收到取消通知。
- Done() 方法会返回一个关闭的 channel,表示 Context 已经被取消。
使用场景:
- 用于主动取消某个操作,比如超时、用户主动取消等。
- 适用于请求处理被取消、提前终止任务等场景。
使用示例:
package main
import (
"context"
"fmt"
"time"
)
func main() {
// 通过emptyCtx创建一个带有取消功能的上下文(context.Background()创建的即为emptyCtx)
ctx, cancel := context.WithCancel(context.Background())
go func() {
// 模拟执行任务
for {
select {
case <-ctx.Done(): // 接收到取消信号,退出 goroutine
fmt.Println("Task canceled")
return
default:
fmt.Println("Task running...")
time.Sleep(1 * time.Second)
}
}
}()
// 运行 3 秒后取消操作
time.Sleep(3 * time.Second)
cancel()
// 给 goroutine 充分的时间来处理取消操作
time.Sleep(1 * time.Second)
}
注意:当 Done() 信号触发时,虽然上下文 Context 已经被取消,但具体的 goroutine 或子进程是否终止,取决于代码逻辑。cancel() 只是通知 goroutine Context 已被取消,具体的退出逻辑需要我们手动处理。
3.2.3、timerCtx
timerCtx
是带有超时或截止时间(Deadline)的 Context,当超过指定时间时,Context 会自动取消。
解释:
timerCtx
是通过WithTimeout
或WithDeadline
创建的。它允许你为 Context 设置一个时间限制,当超时或达到截止时间时,Context 会自动取消,所有关联的 goroutine 都会收到取消通知。- 主要用于那些有执行时间限制的任务。
使用场景:
- 当你需要控制某个任务的最大执行时间(如网络请求、数据库操作等),防止任务无限期地运行时,使用 timerCtx。
- 适用于设置请求超时、操作超时等场景。
使用示例:
// // 通过emptyCtx创建一个带有取消功能的上下文(context.Background()创建的即为emptyCtx)
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
select {
case <-time.After(3 * time.Second):
fmt.Println("Task finished")
case <-ctx.Done():
fmt.Println("Timeout:", ctx.Err()) // 这里会输出超时的错误
}
3.2.4、valueCtx
valueCtx
是可以在 goroutine 之间传递键值对的 Context,用于携带与请求相关的信息。context.WithValue() 可以将特定的值与某个 key 关联起来,并且这个 key 通常是一个自定义类型,而不是直接使用内置的 string 或 int 类型。
解释:
valueCtx
是通过 WithValue
创建的,用于在不同的 goroutine 之间传递一些与上下文相关的数据。这个数据可以通过 Context 的Value()
方法获取。 context.WithValue()
可以将特定的值与某个 key
关联起来,并且这个 key
通常是一个自定义类型,而不是直接使用内置的数据类型,如string
或 int
类型。
使用场景:
- 适用于在多个 goroutine 之间传递一些共享信息,如用户 ID、请求 ID 等。
- 避免使用全局变量或复杂的依赖传递,确保上下文中的数据和 goroutine 的生命周期一致。
使用示例:
type key string
func main() {
ctx := context.WithValue(context.Background(), key("userID"), 123)
go func(ctx context.Context) {
userID := ctx.Value(key("userID")) // 定义了一个新的类型 key,底层类型为 string
fmt.Println("User ID:", userID)
}(ctx)
time.Sleep(1 * time.Second)
}
注意:在上述代码中,如果直接使用 string
类型作为 key
,很可能会出现 key
重名的情况。比如,不同的模块可能会不小心使用相同的字符串 “userID” 作为 key
,这会导致冲突。因此,为了避免这种情况,我们会创建一个自定义类型的 key
,比如 type key string
。这样,尽管它本质上仍然是字符串类型,但它已经是一个新的类型,这样就避免了潜在的冲突问题。
4、总结
Context 是 Go 语言中的一个标准库,用于在处理并发编程时,提供对请求的截止时间、取消信号以及请求范围内共享数据管理的管理。它在多个 goroutine 之间传递,是一种上下文管理工具。
通过context包给出的四种结构体类型,我们可以轻松实现对 goroutine 的管理:
emptyCtx
:基础的空 Context,没有取消、超时或值,适合作为顶层 Context 使用。cancelCtx
:提供手动取消功能,适合需要手动控制任务生命周期的场景。timerCtx
:带有超时或截止时间,适合限制任务执行时间的场景。valueCtx
:允许传递数据,适合跨 goroutine 传递少量与请求相关的上下文信息。