Golang context 原理
Context 的作用
-
取消传播 (Cancellation Propagation):
- 这是
context最核心和最常用的功能。当一个操作因为某种原因(例如,用户取消了请求、上游服务超时或出错、父操作不再需要结果)需要被终止时,可以使用context来通知所有相关的、为此操作派生出来的 Goroutine 停止它们的工作。 - 这有助于避免不必要的资源消耗(如 CPU、内存、网络连接),并及时释放资源。例如,一个 HTTP 请求可能触发多个后台 Goroutine 去查询数据库、调用其他微服务等。如果客户端断开了连接,服务器应该能够取消这些后台任务。
- 这是
-
超时控制 (Timeout/Deadline Management):
context允许你为一个操作或一系列操作设置一个截止时间点 (Deadline) 或一个超时时长 (Timeout)。- 如果操作在指定的时间内未能完成,
context会自动发出取消信号。 - 这对于防止操作无限期阻塞、保证系统的响应性和可靠性至关重要。例如,在调用外部 API 时,设置一个超时,如果对方服务长时间未响应,则主动放弃并返回错误。
-
传递请求作用域的值 (Request-scoped Values):
context可以携带键值对数据,这些数据可以在一个请求的处理链中(跨越多个函数调用和不同的 Goroutine)安全地传递。- 这些值通常是与特定请求相关的信息,例如追踪 ID (trace ID)、用户身份信息、授权令牌等。
- 重要提示:
context.WithValue不应该被滥用。它主要用于传递贯穿整个调用链的、与请求本身相关的元数据,而不是用来替代函数的可选参数。函数的显式参数通常是更清晰的选择。
-
控制 Goroutine 的生命周期:
- 通过将
context传递给新启动的 Goroutine,父 Goroutine 可以有效地控制子 Goroutine 的生命周期。当父context被取消时,所有衍生的子context也会被取消,子 Goroutine 可以监听到这个信号并优雅地退出。
- 通过将
核心接口:Context
context 包的核心是 Context 接口,它定义了所有上下文类型要实现的方法:
type Context interface {
// Deadline 返回一个时间点,当到达这个时间点时,context 会被自动取消。
// 如果没有设置 deadline,ok 会是 false。
Deadline() (deadline time.Time, ok bool)
// Done 返回一个 channel。当此 context 被取消或超时时,该 channel 会被关闭。
// 如果 context 不能被取消,Done 可能返回 nil。
// Done 通常在 select 语句中使用,以监听取消信号。
Done() <-chan struct{}
// Err 返回 context 被取消的原因。
// 如果 Done channel尚未关闭,Err 返回 nil。
// 如果 Done channel已关闭,Err 返回一个非 nil 的错误:
// - Canceled: context 是通过调用 cancel 函数取消的。
// - DeadlineExceeded: context 是因为截止时间到达而被取消的。
Err() error
// Value 返回与此 context关联的键key对应的值,如果不存在则返回 nil。
// key 应该是不易冲突的类型,通常是自定义的非导出类型。
// 主要用于在请求范围内传递数据。
Value(key any) any
}
-
Done()Channel (核心取消机制):- 每个可被取消的
context(即通过WithCancel、WithDeadline、WithTimeout创建的)都会有一个Done()方法,返回一个只读的 channel (<-chan struct{})。 - 当该
context被取消或其截止时间到达时,这个Done()channel 会被关闭 (closed)。 - Goroutine 可以通过
select语句监听这个Done()channel。一旦 channel 关闭,Goroutine 就知道它应该停止当前的工作、进行清理并退出。
select { case <-ctx.Done(): // Context 被取消了,执行清理操作 log.Println("Operation cancelled:", ctx.Err()) return ctx.Err() case result := <-doSomeWork(): // 工作正常完成 return result } - 每个可被取消的
-
Err()方法 (取消原因):- 一旦
Done()channel 被关闭,Err()方法就会返回一个非nil的错误,用来说明context被取消的原因:context.Canceled: 如果context是通过显式调用其cancel函数而被取消的。context.DeadlineExceeded: 如果context是因为其设置的截止时间已过或超时而被取消的。
- 一旦
上下文的树状结构与取消传播
Context 的实例可以形成一个树状结构。除了最顶层的 Background 和 TODO 上下文,每个上下文都有一个父上下文。这种父子关系是实现取消信号传播的关键。
核心规则:当一个父上下文被取消时,它的所有子上下文也会被立即取消。
这个机制是通过 cancelCtx 类型和 propagateCancel 函数实现的。
-
cancelCtx: 这是可取消上下文的核心实现。它包含:- 一个指向父
Context的引用。 - 一个
donechannel,在取消时关闭。 - 一个
childrenmap,用于存储所有可取消的子上下文。 - 一个
err字段,用于存储取消原因。
- 一个指向父
-
propagateCancel函数: 当创建一个新的可取消上下文(如WithCancel)时,这个函数会将新的子上下文“附加”到其父上下文上。- 高效路径:如果父上下文也是一个
cancelCtx,子上下文会被直接添加到父上下文的childrenmap 中。 - 兼容路径:如果父上下文是自定义类型,
context会启动一个新的 goroutine。这个 goroutine 会监听父上下文的Done()channel。一旦父上下文被取消,它就会调用子上下文的cancel方法。
- 高效路径:如果父上下文也是一个
-
cancel()方法: 当一个cancelCtx的cancel()方法被调用时,它会:- 关闭自己的
donechannel。 - 设置自己的
err字段。 - 遍历
childrenmap,并递归地调用所有子上下文的cancel()方法,从而将取消信号传播下去。 - 将自己从父上下文的
childrenmap 中移除,以便垃圾回收。
- 关闭自己的
对于value context 是用于补足协程没有的ThreadLocal
- 值继承:
Value方法在查找值时,如果当前context没有存储对应的键,它会递归地向其父context查找。
Context 的四种主要实现
context 包提供了几种具体的 Context 实现,它们通过不同的函数创建:
-
emptyCtx(Background,TODO)context.Background()和context.TODO()返回的都是emptyCtx的实例。- 它是所有上下文树的根节点。
- 它永远不会被取消,没有截止日期,也不携带任何值。它的
Done()方法总是返回nil。
-
cancelCtx(WithCancel,WithCancelCause)WithCancel(parent)会创建一个cancelCtx。- 它嵌入了父上下文,并实现了上面描述的取消传播逻辑。
- 它返回一个新的
Context和一个CancelFunc。调用CancelFunc会触发取消操作。
-
timerCtx(WithDeadline,WithTimeout)WithDeadline(parent, d)和WithTimeout(parent, t)会创建一个timerCtx。timerCtx内嵌了一个cancelCtx,所以它具备所有可取消上下文的功能。- 此外,它还包含一个
deadline时间和一个*time.Timer。 - 在创建时,它会启动一个定时器 (
time.AfterFunc)。当到达deadline时,定时器会自动调用cancel()方法,从而触发超时取消。 - 如果在超时前手动调用了
cancel函数,它会停止内部的定时器以释放资源。
-
valueCtx(WithValue)WithValue(parent, key, val)会创建一个valueCtx。valueCtx内嵌了父上下文,并存储了一个键值对。- 当调用
Value(key)方法时:- 如果
valueCtx存储的键与要查找的键匹配,就返回对应的值。 - 否则,它会递归地调用父上下文的
Value(key)方法,沿着上下文树向上查找,直到找到匹配的键或到达根节点。
- 如果
- 这形成了一个类似链表的结构来存储和检索值。
具体的 Context 类型和创建函数
- 创建函数
-
context.Background():- 返回一个空的
Context,它是所有context树的根节点。 - 它永远不会被取消,没有值,也没有截止时间。
- 通常用在主函数、初始化代码、测试代码中,或者作为顶级请求的起始
Context。
- 返回一个空的
-
context.TODO():- 也返回一个空的
Context,类似于Background()。 - 当不确定应该使用哪个
Context,或者当前函数将来可能会更新以接收一个Context参数时,可以使用TODO()作为占位符。它表明相关的context还没有确定或者还没有实现。 - 静态分析工具可能会提示你将
TODO()替换为更具体的context。
- 也返回一个空的
-
context.WithCancel(parent Context) (ctx Context, cancel CancelFunc):- 创建一个新的
Context(ctx),它是parent的子节点。 - 同时返回一个
CancelFunc类型的函数cancel。 - 调用这个
cancel函数会关闭ctx.Done()channel,从而取消ctx及其所有派生出来的子context。 - NOTE:
cancel函数必须被调用(通常通过defer cancel()),以释放与该context相关的资源,即使操作正常完成。否则可能导致context树的内存泄漏。
- 创建一个新的
-
context.WithDeadline(parent Context, d time.Time) (Context, CancelFunc):- 创建一个新的
Context(ctx),它会在指定的时间点d到达时自动取消,或者当其parent被取消时,或者当返回的cancel函数被调用时。 - 内部通常会启动一个定时器,在时间点
d到达时调用cancel函数。 - 同样需要调用返回的
cancel函数。
- 创建一个新的
-
context.WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc):- 这是
WithDeadline的一个便捷包装。它接受一个持续时间timeout。 WithTimeout(parent, timeout)等价于WithDeadline(parent, time.Now().Add(timeout))。- 同样需要调用返回的
cancel函数。
- 这是
-
context.WithValue(parent Context, key, val any) Context:- 创建一个新的
Context(ctx),它携带了提供的键值对 (key,val)。 key通常是一个自定义的、不可导出的类型(例如type myKey string),以避免不同包之间的键名冲突。- 当调用
ctx.Value(someKey)时,它会首先检查ctx自身是否存储了someKey。如果没有,它会递归地调用parent.Value(someKey),直到找到值或者到达树的根部。
- 创建一个新的
-