Go的Context包解析

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)。

参考资料

updatedupdated2025-10-052025-10-05