Go 从 0 到精通 · 第 29 课:context 与取消机制

学习定位:这是整套 Go 教程的第 29 课,也是阶段五(并发与工程阶段)的第五课。
前置要求:已经完成第 28 课,掌握了 Mutex、RWMutex、Once 等同步原语。需要熟悉 goroutine、channel 和 select。
本课目标:理解 context 的设计目的,掌握 WithCancelWithTimeoutWithDeadline 的使用方式,能在 goroutine 中监听取消信号,理解 context 的传递约定和使用规范。


1. 本课你要解决的核心问题

到目前为止你学了创建 goroutine、用 channel 通信、用锁保护数据。但有一个重要问题还没解决:

怎么让一个正在运行的 goroutine 停下来?

想象几个真实场景:

  • 用户发起了一个搜索请求,但等了 3 秒没结果就关闭了页面。后台的搜索 goroutine 还在傻傻跑着,浪费资源。
  • 你同时向 3 个服务器发请求,第一个返回了结果。另外两个还在跑,但你已经不需要它们了。
  • 一个任务有子任务,子任务又有子子任务。最顶层取消了,下面所有的都应该停下来。

Go 1.7 引入的 context 包就是解决这个问题的:控制 goroutine 的生命周期

你需要搞明白以下问题:

  • context 是什么,为什么需要它
  • context.Background()context.TODO() 是什么
  • 怎么用 WithCancel 手动取消
  • 怎么用 WithTimeoutWithDeadline 自动超时
  • 怎么在 goroutine 中监听取消信号
  • context 怎么传递,有什么规范
  • 怎么用 context 传值

2. context 是什么

2.1 一句话定义

context 是 goroutine 的生命周期管理器。它能传递取消信号、超时限制和请求级数据。

2.2 核心接口

1
2
3
4
5
6
type Context interface {
Deadline() (deadline time.Time, ok bool) // 返回截止时间
Done() <-chan struct{} // 返回一个 channel,取消时关闭
Err() error // 取消的原因
Value(key any) any // 获取关联的值
}

你不需要自己实现这个接口。标准库提供了创建 context 的函数,直接用就行。

最核心的是 Done() 方法——它返回一个 channel。当 context 被取消时,这个 channel 会被关闭。goroutine 通过 select 监听这个 channel 来知道"该停下来了"。

2.3 context 的树形结构

context 是父子关系:从一个父 context 派生出子 context,子 context 又能派生出孙 context。

1
2
3
4
5
Background (根)
├── WithCancel → ctx1
│ ├── WithTimeout → ctx2
│ └── WithCancel → ctx3
└── WithTimeout → ctx4

取消会向下传播:父 context 取消时,所有子 context 自动取消。但子 context 取消不会影响父 context。


3. 创建 context

3.1 根 context

每个 context 树都需要一个根。Go 提供了两个根 context:

1
2
ctx := context.Background()  // 最常用的根 context
ctx := context.TODO() // 占位用,表示"还没想好用什么 context"
  • Background():程序启动、main 函数、测试的顶层 context
  • TODO():暂时不确定用什么 context 时的占位符

两者功能完全相同(都是空 context,永远不会取消),只是语义不同。99% 的情况用 Background()

3.2 派生 context

从根 context 派生出有具体能力的子 context:

函数 作用
context.WithCancel(parent) 手动取消
context.WithTimeout(parent, duration) 超时自动取消
context.WithDeadline(parent, time) 到达指定时间自动取消
context.WithValue(parent, key, value) 携带值

4. WithCancel——手动取消

4.1 基本用法

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
29
30
31
32
33
34
35
36
37
38
package main

import (
"context"
"fmt"
"time"
)

func worker(ctx context.Context, id int) {
for {
select {
case <-ctx.Done():
fmt.Printf("Worker %d: 收到取消信号,退出 (原因: %v)\n", id, ctx.Err())
return
default:
fmt.Printf("Worker %d: 工作中...\n", id)
time.Sleep(500 * time.Millisecond)
}
}
}

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

go worker(ctx, 1)
go worker(ctx, 2)

// 让 worker 工作 2 秒
time.Sleep(2 * time.Second)

// 取消所有 worker
fmt.Println("Main: 发送取消信号")
cancel()

// 等一下让 worker 完成退出
time.Sleep(500 * time.Millisecond)
fmt.Println("Main: 退出")
}

输出:

1
2
3
4
5
6
7
8
9
10
11
12
Worker 1: 工作中...
Worker 2: 工作中...
Worker 1: 工作中...
Worker 2: 工作中...
Worker 2: 工作中...
Worker 1: 工作中...
Worker 1: 工作中...
Worker 2: 工作中...
Main: 发送取消信号
Worker 2: 收到取消信号,退出 (原因: context canceled)
Worker 1: 收到取消信号,退出 (原因: context canceled)
Main: 退出

4.2 工作原理

1
ctx, cancel := context.WithCancel(parent)
  • ctx:派生的子 context,传给 goroutine
  • cancel:取消函数,调用后 ctx.Done() 返回的 channel 被关闭
  • goroutine 通过 select + <-ctx.Done() 监听取消信号
1
2
3
4
5
6
7
调用 cancel()

ctx.Done() 的 channel 被关闭

所有在 <-ctx.Done() 上等待的 goroutine 被唤醒

goroutine 检查 ctx.Err() 得知取消原因,执行清理后退出

4.3 cancel 必须调用

1
2
3
4
ctx, cancel := context.WithCancel(context.Background())
defer cancel() // 养成习惯:创建后立刻 defer cancel

go doWork(ctx)

即使你不打算手动取消,也要 defer cancel()。不调用 cancel 会导致 context 内部的资源(goroutine、timer 等)泄漏。Go 的 vet 工具会对此发出警告。


5. WithTimeout——超时自动取消

5.1 基本用法

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
29
30
31
package main

import (
"context"
"fmt"
"time"
)

func slowOperation(ctx context.Context) (string, error) {
// 模拟一个耗时操作
select {
case <-time.After(3 * time.Second):
return "操作完成", nil
case <-ctx.Done():
return "", ctx.Err()
}
}

func main() {
// 最多等 2 秒
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()

fmt.Println("开始操作...")
result, err := slowOperation(ctx)
if err != nil {
fmt.Println("失败:", err) // context deadline exceeded
} else {
fmt.Println("成功:", result)
}
}

输出:

1
2
开始操作...
失败: context deadline exceeded

操作需要 3 秒,但 context 只给了 2 秒。2 秒后 context 自动取消,ctx.Done() 的 channel 关闭,slowOperation 收到信号退出。

5.2 WithTimeout vs WithDeadline

1
2
3
4
// WithTimeout:从现在起的相对时间
ctx, cancel := context.WithTimeout(parent, 5*time.Second)
// 等价于
ctx, cancel := context.WithDeadline(parent, time.Now().Add(5*time.Second))
  • WithTimeout(parent, 5*time.Second):5 秒后取消
  • WithDeadline(parent, someTime):到达 someTime 时取消

大多数情况用 WithTimeout,更直观。WithDeadline 适合需要精确时间点的场景。

5.3 查看截止时间

1
2
3
4
5
6
7
8
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

deadline, ok := ctx.Deadline()
if ok {
fmt.Println("截止时间:", deadline.Format("15:04:05"))
fmt.Println("剩余时间:", time.Until(deadline))
}

5.4 子 context 不能延长父 context 的期限

1
2
3
4
5
6
7
8
9
// 父 context 2 秒超时
parentCtx, parentCancel := context.WithTimeout(context.Background(), 2*time.Second)
defer parentCancel()

// 子 context 想要 5 秒——没用,最多 2 秒就会被父 context 取消
childCtx, childCancel := context.WithTimeout(parentCtx, 5*time.Second)
defer childCancel()

// childCtx 最多活 2 秒(受父 context 限制)

子 context 的截止时间取父 context 和自己设置的较早者。你可以给子 context 更短的超时,但不能比父 context 更长。


6. ctx.Err()——取消的原因

1
err := ctx.Err()
返回值 含义
nil context 还没取消
context.Canceled 被手动调用 cancel() 取消
context.DeadlineExceeded 超时或到达截止时间
1
2
3
4
5
6
7
8
9
select {
case <-ctx.Done():
switch ctx.Err() {
case context.Canceled:
fmt.Println("被手动取消")
case context.DeadlineExceeded:
fmt.Println("超时了")
}
}

7. 在 goroutine 中正确监听取消

7.1 标准模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func doWork(ctx context.Context) error {
for {
select {
case <-ctx.Done():
// 收到取消信号,做清理然后退出
return ctx.Err()
default:
// 做一步工作
if err := doOneStep(); err != nil {
return err
}
}
}
}

7.2 耗时操作中检查取消

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func processItems(ctx context.Context, items []string) error {
for i, item := range items {
// 每处理一个就检查一次 context
select {
case <-ctx.Done():
fmt.Printf("在第 %d 个元素处被取消\n", i)
return ctx.Err()
default:
}

// 处理当前元素
process(item)
}
return nil
}

每做一步就检查一次 ctx.Done(),这样 goroutine 能尽快响应取消信号。如果你的操作是一个大循环,不要在循环外面检查——那样要等整个循环跑完才能响应取消。

7.3 传递 context 给子操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func handler(ctx context.Context) error {
// 把 ctx 传给每一个可能耗时的子操作
data, err := fetchData(ctx)
if err != nil {
return err
}

result, err := processData(ctx, data)
if err != nil {
return err
}

return saveResult(ctx, result)
}

func fetchData(ctx context.Context) ([]byte, error) {
select {
case <-ctx.Done():
return nil, ctx.Err()
case <-time.After(2 * time.Second):
return []byte("data"), nil
}
}

context 像一条线,串起整个调用链。最顶层取消后,链上所有操作都能收到信号。


8. WithValue——通过 context 传值

8.1 基本用法

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
package main

import (
"context"
"fmt"
)

type contextKey string

const (
keyRequestID contextKey = "request_id"
keyUserID contextKey = "user_id"
)

func handleRequest(ctx context.Context) {
reqID := ctx.Value(keyRequestID)
userID := ctx.Value(keyUserID)
fmt.Printf("处理请求: reqID=%v, userID=%v\n", reqID, userID)
}

func main() {
ctx := context.Background()
ctx = context.WithValue(ctx, keyRequestID, "req-12345")
ctx = context.WithValue(ctx, keyUserID, 42)

handleRequest(ctx)
}

输出:

1
处理请求: reqID=req-12345, userID=42

8.2 key 的类型

不要用 string 或其他内置类型做 key。不同包可能用相同的 string,造成冲突。

1
2
3
4
5
6
7
// 错误:可能和别的包冲突
ctx = context.WithValue(ctx, "user_id", 42)

// 正确:自定义类型做 key,包级别私有
type contextKey string
const keyUserID contextKey = "user_id"
ctx = context.WithValue(ctx, keyUserID, 42)

自定义类型即使底层是 string,也是不同的类型,不会和其他包的 key 冲突。

8.3 什么该用 WithValue 传,什么不该

该传的:请求级别的元数据(request ID、用户认证信息、追踪 ID)

不该传的:函数的参数。如果一个值是函数运行必需的,应该作为参数显式传递,不要塞到 context 里。

1
2
3
4
5
6
// 坏:把业务参数塞到 context 里
ctx = context.WithValue(ctx, "username", "alice")
processUser(ctx)

// 好:业务参数显式传递
processUser(ctx, "alice")

原则:context.Value 只用来传请求范围的横切关注点,不用来替代函数参数。


9. 实战示例

9.1 HTTP 请求超时

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
package main

import (
"context"
"fmt"
"time"
)

// 模拟 HTTP 请求
func httpGet(ctx context.Context, url string) (string, error) {
// 模拟网络延迟
delay := 1*time.Second + time.Duration(len(url)%3)*time.Second

select {
case <-time.After(delay):
return fmt.Sprintf("响应 from %s (耗时 %v)", url, delay), nil
case <-ctx.Done():
return "", fmt.Errorf("请求 %s 被取消: %w", url, ctx.Err())
}
}

func main() {
// 所有请求共享 3 秒的总超时
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()

urls := []string{
"api.example.com/users",
"api.example.com/products",
"api.example.com/orders",
"api.example.com/inventory",
}

for _, url := range urls {
result, err := httpGet(ctx, url)
if err != nil {
fmt.Println("失败:", err)
break // 超时后后续请求也没必要了
}
fmt.Println("成功:", result)
}
}

9.2 父子任务取消传播

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
package main

import (
"context"
"fmt"
"sync"
"time"
)

func subTask(ctx context.Context, name string, wg *sync.WaitGroup) {
defer wg.Done()
for i := 1; ; i++ {
select {
case <-ctx.Done():
fmt.Printf(" [%s] 第 %d 步被取消: %v\n", name, i, ctx.Err())
return
default:
fmt.Printf(" [%s] 第 %d 步\n", name, i)
time.Sleep(300 * time.Millisecond)
}
}
}

func parentTask(ctx context.Context) {
// 从父 context 派生子 context,给子任务更短的超时
childCtx, childCancel := context.WithTimeout(ctx, 1500*time.Millisecond)
defer childCancel()

var wg sync.WaitGroup

wg.Add(3)
go subTask(childCtx, "子任务A", &wg)
go subTask(childCtx, "子任务B", &wg)
go subTask(childCtx, "子任务C", &wg)

wg.Wait()
fmt.Println("父任务: 所有子任务已退出")
}

func main() {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

fmt.Println("开始执行")
parentTask(ctx)
fmt.Println("执行结束")
}

输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
开始执行
[子任务C]1
[子任务A]1
[子任务B]1
[子任务B]2
[子任务C]2
[子任务A]2
[子任务A]3
[子任务B]3
[子任务C]3
[子任务C]4
[子任务A]4
[子任务B]4
[子任务B]5 步被取消: context deadline exceeded
[子任务C]5 步被取消: context deadline exceeded
[子任务A]5 步被取消: context deadline exceeded
父任务: 所有子任务已退出
执行结束

父 context 有 5 秒,但 parentTask 给子任务只分配了 1.5 秒。1.5 秒后子 context 超时,三个子任务同时收到取消信号退出。

9.3 竞速请求——最快的响应胜出

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
29
30
31
32
33
34
35
36
37
38
39
package main

import (
"context"
"fmt"
"math/rand"
"time"
)

func queryServer(ctx context.Context, server string, ch chan<- string) {
delay := time.Duration(200+rand.Intn(800)) * time.Millisecond

select {
case <-time.After(delay):
ch <- fmt.Sprintf("%s 响应 (耗时 %v)", server, delay)
case <-ctx.Done():
fmt.Printf(" %s: 被取消,停止工作\n", server)
}
}

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

result := make(chan string, 3)

servers := []string{"服务器A", "服务器B", "服务器C"}
for _, s := range servers {
go queryServer(ctx, s, result)
}

// 只要最快的那个结果
fastest := <-result
fmt.Println("最快响应:", fastest)

// 取消其他还在运行的请求
cancel()
time.Sleep(100 * time.Millisecond) // 等一下看取消日志
}

可能的输出:

1
2
3
最快响应: 服务器A 响应 (耗时 234ms)
服务器C: 被取消,停止工作
服务器B: 被取消,停止工作

三个请求同时发出,第一个返回结果后立刻取消其他两个。这在分布式系统中很常见——冗余请求降低延迟。

9.4 带取消的 Worker Pool

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
package main

import (
"context"
"fmt"
"sync"
"time"
)

func worker(ctx context.Context, id int, jobs <-chan int, wg *sync.WaitGroup) {
defer wg.Done()
for {
select {
case <-ctx.Done():
fmt.Printf("Worker %d: 收到取消,退出\n", id)
return
case job, ok := <-jobs:
if !ok {
fmt.Printf("Worker %d: 任务队列关闭,退出\n", id)
return
}
fmt.Printf("Worker %d: 处理任务 %d\n", id, job)
time.Sleep(200 * time.Millisecond)
}
}
}

func main() {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()

jobs := make(chan int, 20)
var wg sync.WaitGroup

// 启动 3 个 worker
for i := 1; i <= 3; i++ {
wg.Add(1)
go worker(ctx, i, jobs, &wg)
}

// 发送 50 个任务(超时前可能处理不完)
go func() {
for i := 1; i <= 50; i++ {
select {
case jobs <- i:
case <-ctx.Done():
fmt.Printf("发送方: 第 %d 个任务时超时,停止发送\n", i)
close(jobs)
return
}
}
close(jobs)
}()

wg.Wait()
fmt.Println("程序结束")
}

2 秒超时后,所有 worker 和发送方都会收到取消信号并退出。不会有 goroutine 泄漏。


10. context 的使用规范

10.1 函数签名约定

1
2
3
4
// context 是第一个参数,命名为 ctx
func DoSomething(ctx context.Context, arg1 string, arg2 int) error {
// ...
}
  • context 作为函数的第一个参数
  • 参数名叫 ctx
  • 不要把 context 放在结构体里

10.2 不要传 nil context

1
2
3
4
5
// 错误
DoSomething(nil, "hello", 42)

// 正确:用 context.Background() 或 context.TODO()
DoSomething(context.Background(), "hello", 42)

10.3 context 是不可变的

每次 WithCancelWithTimeoutWithValue 都返回一个新的 context。原始 context 不会被修改。

1
2
3
4
5
ctx := context.Background()
ctx2 := context.WithValue(ctx, key, "value")

// ctx 没有 key 这个值
// ctx2 有 key 这个值

10.4 cancel 函数可以多次调用

1
2
3
4
ctx, cancel := context.WithCancel(parent)
cancel() // 第一次:取消 context
cancel() // 第二次:安全,无操作
cancel() // 第三次:安全,无操作

多次调用 cancel 不会 panic,第一次之后的调用没有效果。所以 defer cancel() 是安全的。

10.5 不要在结构体中存储 context

1
2
3
4
5
6
7
8
9
// 坏:context 存在结构体里
type Server struct {
ctx context.Context // 不要这样做
}

// 好:每次请求传入 context
func (s *Server) HandleRequest(ctx context.Context, req Request) Response {
// ...
}

context 是请求级别的,每次请求应该有自己的 context。存在结构体里意味着所有请求共享一个 context,无法单独取消。


11. 常见坑总结

11.1 忘记调用 cancel

1
2
3
4
5
6
7
8
9
10
11
12
13
// 每次调用都泄漏一点资源
func bad() {
ctx, _ := context.WithTimeout(context.Background(), 5*time.Second)
doWork(ctx)
// cancel 被忽略了,内部的 timer 无法释放
}

// 正确
func good() {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
doWork(ctx)
}

11.2 超时后没检查 context

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 操作本身尊重 context 超时
func query(ctx context.Context) (string, error) {
// 模拟查询
time.Sleep(2 * time.Second)
return "result", nil // 即使 ctx 超时了,这里也不知道
}

// 正确:用 select 监听 ctx.Done()
func query(ctx context.Context) (string, error) {
ch := make(chan string, 1)
go func() {
time.Sleep(2 * time.Second)
ch <- "result"
}()

select {
case result := <-ch:
return result, nil
case <-ctx.Done():
return "", ctx.Err()
}
}

11.3 把 context 存在结构体里

前面规范里讲过,但再强调一遍。context 应该通过参数传递,不存在结构体中。

11.4 用 WithValue 传业务参数

1
2
3
4
5
6
// 坏:用 context 传递必要参数
ctx = context.WithValue(ctx, "name", "Alice")
greet(ctx) // 函数签名看不出需要 name

// 好:用函数参数
greet(ctx, "Alice") // 一看就知道需要什么

11.5 阻塞操作不检查 context

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 如果 ch 一直没有数据,即使 ctx 超时也不会退出
func bad(ctx context.Context, ch chan int) {
v := <-ch // 不检查 ctx,可能永远阻塞
fmt.Println(v)
}

// 正确
func good(ctx context.Context, ch chan int) {
select {
case v := <-ch:
fmt.Println(v)
case <-ctx.Done():
fmt.Println("超时退出")
}
}

所有可能阻塞的操作都应该配合 ctx.Done() 使用。 这是 context 发挥作用的前提。


12. 本课练习

练习 1:可取消的倒计时

要求:

  • 实现一个 10 秒倒计时,每秒打印剩余时间
  • 用一个 goroutine 监听用户输入,输入 “stop” 时取消倒计时
  • context.WithCancel 实现

练习 2:多服务聚合查询

要求:

  • 模拟 3 个服务(用户服务、订单服务、推荐服务),各自耗时不同
  • 同时发起 3 个查询,总超时 2 秒
  • 收集所有在超时前返回的结果
  • 超时后打印哪些服务成功了,哪些超时了

提示:用 context.WithTimeout + 多个 goroutine + channel 收集结果。


练习 3:分级超时

要求:

  • 整体操作超时 5 秒
  • 步骤 1(获取数据)超时 2 秒
  • 步骤 2(处理数据)超时 2 秒
  • 步骤 3(保存结果)超时 1 秒
  • 每一步用子 context 设置独立超时

练习 4:带 context 的 Worker Pool

要求:

  • 启动 3 个 worker goroutine
  • 不断向它们分发任务
  • 5 秒后通过 context 取消所有 worker
  • 打印每个 worker 处理了多少个任务

练习 5:请求追踪

要求:

  • context.WithValue 在 context 中携带 request ID
  • 实现 3 个函数:handleRequestqueryDBcallAPI
  • 每个函数从 context 中取出 request ID 并在日志中打印
  • 模拟一个请求的完整链路

13. 自测题

13.1 概念题

  1. context 解决的核心问题是什么?
  2. context.Background()context.TODO() 有什么区别?
  3. WithCancelWithTimeoutWithDeadline 各自的使用场景是什么?
  4. 父 context 取消后,子 context 会怎样?反过来呢?
  5. 子 context 能比父 context 有更长的超时时间吗?
  6. 为什么创建 context 后必须 defer cancel()
  7. ctx.Err() 可能返回哪些值?分别代表什么?
  8. 为什么不应该把 context 存在结构体中?

13.2 代码阅读题

预测以下代码的行为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import (
"context"
"fmt"
"time"
)

func main() {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()

childCtx, childCancel := context.WithTimeout(ctx, 5*time.Second)
defer childCancel()

select {
case <-childCtx.Done():
fmt.Println("子 context 结束:", childCtx.Err())
}
}
点击查看答案

3 秒后输出:

1
context 结束: context deadline exceeded

解释:

  1. 父 context 超时 3 秒
  2. 子 context 尝试设置 5 秒超时,但父 context 只有 3 秒
  3. 子 context 的实际超时 = min(父的 3 秒, 自己的 5 秒) = 3 秒
  4. 3 秒后父 context 超时,子 context 也被取消
  5. childCtx.Err() 返回 context.DeadlineExceeded

子 context 不能比父 context 活得更久。


14. 本课总结

这一课你学到了用 context 管理 goroutine 的生命周期。

工具 用途 关键点
context.Background() 根 context 程序入口和顶层调用使用
WithCancel 手动取消 调用 cancel() 触发取消
WithTimeout 超时取消 指定持续时间
WithDeadline 截止时间取消 指定具体时间点
WithValue 传递值 只传请求级元数据,不替代函数参数
ctx.Done() 取消信号 channel 用 select 监听
ctx.Err() 取消原因 CanceledDeadlineExceeded

最重要的三件事:

  1. context 控制 goroutine 的生命周期——取消会从父 context 自动传播到所有子 context
  2. 创建 context 后立刻 defer cancel()——不调用会泄漏资源
  3. 所有可能阻塞的操作都用 select 配合 ctx.Done()——这是 context 生效的前提

15. 下一课预告

到这里,你已经学完了 Go 并发编程的四大核心工具:goroutine、channel、select、context。下一课把它们组合起来,学习几个经典的并发设计模式

下一课:并发模式实战

会重点讲:

  • 生产者-消费者模式
  • Worker Pool(工作池)模式
  • Pipeline(流水线)模式
  • Fan-out / Fan-in(扇出/扇入)模式
  • 怎么选择合适的并发模式

学完下一课,你就能写出结构清晰、可维护的并发程序了。