golang context 原理
0.1 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 可以监听到这个信号并优雅地退出。
- 通过将
0.2 核心接口: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
是因为其设置的截止时间已过或超时而被取消的。
- 一旦
0.3 上下文的树状结构与取消传播
Context
的实例可以形成一个树状结构。除了最顶层的 Background
和 TODO
上下文,每个上下文都有一个父上下文。这种父子关系是实现取消信号传播的关键。
核心规则:当一个父上下文被取消时,它的所有子上下文也会被立即取消。
这个机制是通过 cancelCtx
类型和 propagateCancel
函数实现的。
-
cancelCtx
: 这是可取消上下文的核心实现。它包含:- 一个指向父
Context
的引用。 - 一个
done
channel,在取消时关闭。 - 一个
children
map,用于存储所有可取消的子上下文。 - 一个
err
字段,用于存储取消原因。
- 一个指向父
-
propagateCancel
函数: 当创建一个新的可取消上下文(如WithCancel
)时,这个函数会将新的子上下文“附加”到其父上下文上。- 高效路径:如果父上下文也是一个
cancelCtx
,子上下文会被直接添加到父上下文的children
map 中。 - 兼容路径:如果父上下文是自定义类型,
context
会启动一个新的 goroutine。这个 goroutine 会监听父上下文的Done()
channel。一旦父上下文被取消,它就会调用子上下文的cancel
方法。
- 高效路径:如果父上下文也是一个
-
cancel()
方法: 当一个cancelCtx
的cancel()
方法被调用时,它会:- 关闭自己的
done
channel。 - 设置自己的
err
字段。 - 遍历
children
map,并递归地调用所有子上下文的cancel()
方法,从而将取消信号传播下去。 - 将自己从父上下文的
children
map 中移除,以便垃圾回收。
- 关闭自己的
对于value context 是用于补足协程没有的ThreadLocal
- 值继承:
Value
方法在查找值时,如果当前context
没有存储对应的键,它会递归地向其父context
查找。
0.4 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)
方法,沿着上下文树向上查找,直到找到匹配的键或到达根节点。
- 如果
- 这形成了一个类似链表的结构来存储和检索值。
0.5 具体的 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)
,直到找到值或者到达树的根部。
- 创建一个新的
-