首页 / Go 语言入门教程 / context 包基础

Go 语言入门教程

context 包基础

本教程共 80 篇 · 第 65 篇 · 更新于 2026-07-27 · 约 8 分钟阅读

GoGo 入门教程contextWithCancelWithTimeoutWithValue并发控制

65. context 包基础

本节目标:搞懂 context.Context 接口,学会用 WithCancelWithTimeoutWithValue 控制 goroutine,理解 Background 和 TODO 的区别。

context 解决什么问题

goroutine 启动容易,怎么优雅地叫停?比如一个 HTTP 请求触发了 5 个子任务,客户端断开了,这 5 个任务都该停。context 就是干这个的。

它像一根「传递取消信号」的线,从父 goroutine 牵到子 goroutine,父取消时所有子跟着取消。

Context 接口

context.Context 是个接口,四个方法:

type Context interface {
	Deadline() (deadline time.Time, ok bool) // 截止时间
	Done() <-chan struct{}                    // 取消通知 channel
	Err() error                               // 取消原因
	Value(key any) any                        // 携带的值
}

最常用的是 Done():返回一个 channel,context 被取消时这个 channel 会关闭。配合 select 监听它就行。

两个根 context

所有 context 都从一个根开始:

  • context.Background():用于 main 函数、初始化、测试,表示顶层
  • context.TODO():还没想好用哪个时,先用它占位

两者本质都是空的 context,不能取消、没有 deadline、没有值。区别只在语义—告诉读代码的人「这是根」还是「这里待完善」。

ctx := context.Background() // 顶层根
ctx = context.WithCancel(ctx) // 派生可取消的

WithCancel 手动取消

WithCancel 返回一个 context 和一个取消函数:

func main() {
	ctx, cancel := context.WithCancel(context.Background())

	go worker(ctx)

	time.Sleep(2 * time.Second)
	cancel() // 取消,worker 收到信号退出

	time.Sleep(time.Second)
}

func worker(ctx context.Context) {
	for {
		select {
		case <-ctx.Done():
			fmt.Println("收到取消,退出:", ctx.Err())
			return
		default:
			fmt.Println("工作中...")
			time.Sleep(500 * time.Millisecond)
		}
	}
}

worker 里用 select 监听 ctx.Done(),一旦关闭就退出。

Warning

cancel 函数一定要调用,哪怕没手动取消也要 defer cancel()。否则 context 持有的资源不会释放,导致泄漏。

WithTimeout 超时取消

WithTimeout 在指定时间后自动取消,不用手动调 cancel:

func main() {
	// 1 秒后自动取消
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()

	go slowTask(ctx)

	<-ctx.Done()
	fmt.Println("主程序退出")
}

func slowTask(ctx context.Context) {
	select {
	case <-time.After(3 * time.Second):
		fmt.Println("任务完成")
	case <-ctx.Done():
		fmt.Println("超时取消:", ctx.Err()) // context deadline exceeded
	}
}

任务要 3 秒,1 秒就超时了,所以走 ctx.Done() 分支。

WithDeadline 指定时刻

WithDeadlineWithTimeout 类似,区别是它接收一个具体时间点:

// 今晚 23:59:59 取消
deadline := time.Date(2026, 7, 27, 23, 59, 59, 0, time.Local)
ctx, cancel := context.WithDeadline(context.Background(), deadline)
defer cancel()

WithTimeout 其实就是 WithDeadline(parent, time.Now().Add(timeout)) 的简写。

WithValue 携带值

WithValue 给 context 附带键值对,在 goroutine 间传递请求级别的数据:

func main() {
	ctx := context.WithValue(context.Background(), "userID", 42)

	handleRequest(ctx)
}

func handleRequest(ctx context.Context) {
	uid := ctx.Value("userID")
	fmt.Println("用户ID:", uid) // 42
}
Warning

WithValue 不是用来传业务参数的!它只适合传请求级别的元数据,比如 trace ID、用户身份、认证 token。别用它代替函数参数传业务数据,那会让代码难以理解和测试。

context 的传播

context 的核心是「传播」:父 context 取消,所有子 context 都取消。

func main() {
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	// 从 ctx 派生子 context
	childCtx, childCancel := context.WithCancel(ctx)
	defer childCancel()

	go func() {
		<-childCtx.Done()
		fmt.Println("child 收到取消")
	}()

	cancel() // 取消父,child 也会收到
	time.Sleep(time.Second)
}
Background
   |
WithCancel (cancel调用了)
   |
WithCancel (子,自动跟着取消)
   |
worker goroutine 监听 Done

这就是为什么 HTTP handler 第一个参数总是 ctx context.Context—请求取消时,整条调用链都能收到。

取消原因

ctx.Err() 返回取消原因:

  • context.Canceled:被 cancel() 主动取消
  • context.DeadlineExceeded:超时

Go 1.21 起还能用 context.Cause(ctx) 获取更具体的原因(配合 WithCancelCause 等)。

小结

  • Background() 是顶层根,TODO() 是占位
  • WithCancel:手动取消,cancel() 触发
  • WithTimeout/WithDeadline:自动超时取消
  • WithValue:携带请求级元数据,别传业务参数
  • context 传播:父取消,所有子跟着取消
  • cancel 函数必须调用,用 defer 兜底

下一节讲 context 进阶用法和最佳实践。