context包提供了以下功能:传递 Key-Value 值、取消信号、超时时间。由于 context 可以被安全的传递给任意数量的 goroutine,context 常被用于控制并发操作。
context 的接口如下:
1
2
3
4
5
6
type Context interface {
Done <- chan struct {}
Err error ()
Deadline () ( deadline time . Time , ok bool )
Value ( key interface ) interface {}
}
Done 方法将返回一个 channel,该 channel 会在 context 退出时被关闭,goroutine 可以主动监听该 channel 以实现退出机制。
Err 方法将返回一个错误,说明该 context 为什么被关闭。
如果 context.Context 被取消,则返回 Canceled。
如果 context.Context 超时,则返回 DeadlineExceeded。
Deadline 方法将返回一个超时时间和context是否具有超时时间 的布尔值,当 context 将持续运行是 ok 为 false。
Value 方法将允许 context 携带 key-value 数据。
context 的派生
context 可以从现有的 context 派生得到。这些 context 会形成树状结构,同时某个 context 被取消时,其子 context 也会被取消。
Context 包提供了一个空的 context,即 Background。Background 作为一个空的 context,可以被用作所有 context 树的根,同时 Background 永远不会取消。
context 提供了以下接口以派生新的 context:
1
2
3
4
func WithCancel ( parent Context ) ( ctx Context , cancel CancelFunc )
func WithDeadline ( parent Context , deadline time . Time ) ( Context , CancelFunc )
func WithTimeout ( parent Context , timeout time . Duration ) ( Context , CancelFunc )
func WithValue ( parent Context , key , val interface {}) Context
WithCancel 和 WithTimeout 可以包装一个 context,这两个方法将返回一个比父 context 更早取消的 context,比如以下示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
func func1 ( ctx context . Context , wg * sync . WaitGroup ) {
for {
select {
case <- ctx . Done ():
fmt . Printf ( "[%s]func1: cancel.\n" , time . Now (). String ())
defer wg . Done ()
return
default :
fmt . Printf ( "[%s]func1: I'm running.\n" , time . Now (). String ())
time . Sleep ( 100 * time . Millisecond )
}
}
}
func main () {
wg := sync . WaitGroup {}
root := context . Background ()
ctx , cancel := context . WithCancel ( root )
fmt . Printf ( "[%s]main: start func1.\n" , time . Now (). String ())
wg . Add ( 1 )
go func1 ( ctx , & wg )
time . Sleep ( 3 * time . Second )
cancel ()
wg . Wait ()
}
WithDeadline 和 WithTimeout 的区别
WithDeadline:指定一个绝对时间点(deadline),比如 “2024-06-01 10:00:00”。
WithTimeout:指定一个相对时长(timeout),比如 “5秒后”。
从可读性和意图表达角度,WithTimeout 更适合表达“执行最多等待多久”,这是大多数业务场景的需求(如 HTTP 请求超时、数据库查询超时)。WithDeadline 更适合与外部系统协调(比如 gRPC 的截止时间传播),因为截止时间是可以跨服务传递的绝对时间。
对于 context 包,cancel 是可以反复调用的幂等操作 !与底层 channel 的行为不同,cancel 一个已经退出的 context 不会出错。
WithValue 的实现与性能
WithValue 用于在 context 中携带上下文数据,其使用方法如下:
1
2
3
4
5
6
7
8
9
func doSomething ( ctx context . Context ) {
fmt . Printf ( "doSomething: key's value is %s\n" , ctx . Value ( "key" ))
}
func main () {
ctx := context . Background ()
ctx = context . WithValue ( ctx , "key" , "value" )
doSomething ( ctx )
}
但问题是,从 context 的源码中可以看出 key-value 是递归查询的:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// src/context/context.go
// A valueCtx carries a key-value pair. It implements Value for that key and
// delegates all other calls to the embedded Context.
type valueCtx struct {
Context
key , val any
}
func WithValue ( parent Context , key , val any ) Context {
if parent == nil {
panic ( "cannot create context from nil parent" )
}
if key == nil {
panic ( "nil key" )
}
if ! reflectlite . TypeOf ( key ). Comparable () {
panic ( "key is not comparable" )
}
return & valueCtx { parent , key , val }
}
func ( c * valueCtx ) Value ( key any ) any {
if c . key == key {
return c . val
}
return value ( c . Context , key )
}
即插入操作是直接派生一个新的 context,复杂度为 O(1),而查询操作需要沿着链条一直向上查询,复杂度为 O(n)。
同时,从检查 key 的方式 c.key == key 也能够看出,key 必须可比较(comparable)。
参考资料