• 主页
  • 架构
  • 编程语言
  • 数据存储
  • 网络
  • VMware
  • 服务器
  • 组网
  • AI
  • 算法系列
  • 设计模式
  • 读书笔记
  • 思考
  • 工具
  • 其它技术

  • 主页
  • 架构
  • 编程语言
  • 数据存储
  • 网络
  • VMware
  • 服务器
  • 组网
  • AI
  • 算法系列
  • 设计模式
  • 读书笔记
  • 思考
  • 工具
  • 其它技术

golang之ctx cancel

2025-05-04

在 Go 语言中,context包提供了ctx(上下文)和cancel相关的函数,用于管理多个goroutine的生命周期和传递截止日期、取消信号等信息。以下是一些常用的相关函数:

1.context.WithCancel(parent Context) (ctx Context, cancel CancelFunc)

  • 功能:创建一个可取消的上下文ctx,以及一个取消函数cancel。当调用cancel函数时,与该ctx关联的所有goroutine都会收到取消信号,从而可以停止执行。

2.context.WithTimeout(parent Context, timeout time.Duration) (ctx Context, cancel CancelFunc)

  • 功能:创建一个带有超时时间的上下文ctx和取消函数cancel。timeout参数指定了从创建上下文开始的最长持续时间。当超过这个时间后,上下文会自动取消,与该ctx关联的所有goroutine都会收到取消信号。

3.context.WithDeadline(parent Context, d time.Time) (ctx Context, cancel CancelFunc)

  • 功能:创建一个带有绝对截止时间d的上下文ctx和取消函数cancel。当到达指定的截止时间d时,上下文会自动取消,关联的goroutine会收到取消信号。

这些函数在处理并发编程时非常有用,特别是在需要控制goroutine的生命周期、处理超时和取消操作的场景中。通过使用上下文,你可以确保在程序的不同部分之间有效地传递和管理这些信息。

使用示例

我们以WithCancel函数为例,看看具体怎么使用的:

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"
"time"
)

func main() {
// 模拟一个长时间运行的任务
ctx := context.Background()
// 使用 context.WithCancel 创建一个可取消的上下文 ctx 和取消函数 cancel
ctx, cancel := context.WithCancel(ctx)

// 模拟一个长时间运行的任务
go func(ctx context.Context) {
for {
select {
case <-ctx.Done():
fmt.Println("任务被取消")
return
default:
fmt.Println("任务正在运行...")
time.Sleep(1 * time.Second)
}
}
}(ctx)

go func(ctx context.Context) {
for {
fmt.Print("hello\n")
time.Sleep(1 * time.Second)
}
}(ctx)

// 主线程休眠3秒
time.Sleep(3 * time.Second)
// 手动调用取消函数
cancel()
fmt.Println(ctx.Err())
cancel()
fmt.Println(ctx.Err())

// 防止主线程提前退出
time.Sleep(2 * time.Second)

}

执行结果为:

1
2
3
4
5
6
7
8
9
10
11
12
➜  my go run main.go
任务正在运行...
hello
hello
任务正在运行...
hello
任务正在运行...
context canceled
context canceled
hello
任务被取消
hello

解释一下为什么会出现这种情况:

首先创建了一个可以取消的ctx,起了两个goroutine,然后主线程休眠3秒,所以“任务正在运行”和“hello”输出了三次。

这时候执行cancel,ctx就有error了,所以会打印出“context canceled”。好处是多次cancel也没啥问题。因为这里cancel了两次,打印了两次错误。

当执行完cancel后,第一个goroutine监听了ctx.Done,立即退出,所以显示“任务被取消”。但是第二个不感知,所以继续执行。

上下文结构

Go 语言的上下文是有层级关系的树状结构。一个父上下文取消时,会级联取消所有子上下文。

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

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

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

// 创建子上下文
childCtx, childCancel := context.WithCancel(ctx)

go func() {
for {
select {
case <-childCtx.Done():
fmt.Println("子上下文收到取消信号", time.Now().UnixMicro())
return
default:
fmt.Println("子上下文没有收到取消信号")
time.Sleep(1 * time.Second)
}
}
}()

// 假设ctx.Done()已经执行(这里简单模拟通过其他方式触发了ctx取消)
go func() {
select {
case <-ctx.Done():
fmt.Println("父上下文收到取消信号", time.Now().UnixMicro())
}
}()
// 模拟一些工作
time.Sleep(6 * time.Second)
fmt.Println("hello1")
// 再次调用cancel
cancel()
fmt.Println("hello2")
// 这里虽然ctx.Done()已经执行,但调用cancel仍会影响子上下文
// 确保子上下文也能正确处理取消
childCancel()
fmt.Println("hello3")
time.Sleep(1 * time.Second)

}

输出结果为:

1
2
3
4
5
6
7
8
➜  my go run main.go
子上下文没有收到取消信号
子上下文没有收到取消信号
父上下文收到取消信号 1746288171861277
子上下文收到取消信号 1746288172861440
hello1
hello2
hello3

可以看到,到1s的时候,父子上下文的Done都被唤起了。

现实使用

客户端主动取消

客户端程序

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"
"net/http"
"time"
)

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

req, err := http.NewRequestWithContext(ctx, "GET", "http://127.0.0.1:8088", nil)
if err != nil {
fmt.Println("创建请求失败:", err)
return
}

client := http.Client{}
//go func() {
// time.Sleep(2 * time.Second) // 模拟一些操作,2秒后取消请求
// cancel()
//}()

resp, err := client.Do(req)
if err != nil {
fmt.Println("请求错误:", err)
return
}
fmt.Println(resp)
defer resp.Body.Close()

// 处理响应
//...
}

服务端程序

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 (
"fmt"
"net/http"
"time"
)

func handler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
go func() {
for {
select {
case <-ctx.Done():
fmt.Println("客户端取消了请求")
return
default:
fmt.Println("客户端请求")
time.Sleep(1 * time.Second)
// 处理业务逻辑
//...
}
}
}()

go func() {
for {
select {
default:
fmt.Println("默默执行")
time.Sleep(1 * time.Second)
// 处理业务逻辑
//...
}
}
}()
// 返回响应
//...
time.Sleep(20 * time.Second)
fmt.Println("finish")
}

func main() {
http.HandleFunc("/", handler)
http.ListenAndServe(":8088", nil)
}

执行结果

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
➜  my go run main.go
默默执行
客户端请求
客户端请求
默默执行
默默执行
客户端请求
客户端请求
默默执行
默默执行
客户端请求
客户端请求
默默执行
默默执行
客户端请求
客户端请求
默默执行
默默执行
客户端请求
客户端请求
默默执行
默默执行
客户端取消了请求
默默执行
默默执行
默默执行
默默执行
默默执行
默默执行
默默执行
默默执行
默默执行
finish
默默执行
默默执行
默默执行
默默执行
默默执行
默默执行

可以看到,客户端10s的时候取消了请求,所以“客户端请求”执行了10次,取消之后,打印“客户端取消了请求”后便不再执行。但因为“默默执行”的goroutine没有关注ctx.done,所以即使已经finish了,goroutine仍然会继续执行。

服务端主动停止所有任务

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"
"time"
)

// 模拟一个任务
func task(ctx context.Context, taskID int) {
for {
select {
case <-ctx.Done():
// 当接收到取消信号时,打印任务已取消并返回
fmt.Printf("Task %d 已取消\n", taskID)
return
default:
// 模拟任务执行
fmt.Printf("Task %d 正在运行\n", taskID)
time.Sleep(1 * time.Second)
}
}
}

func main() {
// 创建一个可取消的上下文
ctx, cancel := context.WithCancel(context.Background())

// 启动多个任务
numTasks := 3
for i := 1; i <= numTasks; i++ {
go task(ctx, i)
}

// 模拟一些业务逻辑
time.Sleep(3 * time.Second)

// 调用取消函数
fmt.Println("调用取消函数")
cancel()

// 给任务一些时间来处理取消信号
time.Sleep(2 * time.Second)

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

执行结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
➜  my go run main.go
Task 1 正在运行
Task 3 正在运行
Task 2 正在运行
Task 2 正在运行
Task 3 正在运行
Task 1 正在运行
Task 3 正在运行
Task 2 正在运行
Task 1 正在运行
调用取消函数
Task 3 已取消
Task 2 已取消
Task 1 已取消
程序结束

扫一扫,分享到微信

微信分享二维码
MySQL事务的一些奇奇怪怪知识
如何做报警治理
© 2025 John Doe
Hexo Theme Yilia by Litten