
context.Context是什么?
ctx.Done() 通道 说明书上的“取消信号”指示灯
至于web框架hertz,接口结束了,异步方法用到的ctx到底会不会取消呢?我感觉应该是会取消的,但是在我的本地测试的情况是并没有取消。 我的环境是window,不知道是不是环境的原因。不管了,小问题
你不能直接创建 Context,但可以通过 context.Background() 派生出新的 context:
go
// 1. 基于 Background 创建可取消的 context
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// 2. 带超时的 context(推荐用于 HTTP 请求)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// 3. 带截止时间的 context
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(3*time.Second))
defer cancel()
// 4. 带值的 context(用于传递请求数据)
ctx := context.WithValue(context.Background(), "user_id", "123")
核心比喻:快递配送任务(Go Routine)和它的“任务说明书”(Context)
想象一下,你是一个仓库管理员(主 Goroutine,比如一个 HTTP 服务器),你需要派一个快递员(一个子 Goroutine)去送一个包裹。
为了让快递员高效、安全地工作,你不会只告诉他地址就完事了。你会给他一张 “任务说明书”,这张说明书就是 context.Context
。
这张说明书可以包含多种关键信息:
1. context.Background()
- 空白任务书
这是所有说明书的底版。就像一张空白的、盖了公章的派遣单。它本身不包含任何具体指令,但你可以基于它来创建各种具体的说明书。
go
ctx := context.Background() // 拿到一张空白的派遣单
2. context.WithCancel()
- 可随时取消的任务
- 取消是协作式的。(Goroutine 必须主动检查 ctx.Done(),不能强制杀死goroutine。这是一种"礼貌的请求停止"而不是"强制终止"。)
- 一个Cancel函数控制多个Goroutine。(这是最强大的特性!你不需要维护每个goroutine的引用,只需要调用一次 cancel(),所有监听这个context的goroutine都会收到信号。)
你告诉快递员:“这是地址,开始送吧。但是,如果我给你打信号(比如对讲机里喊‘取消’),你必须立刻停止配送,马上回来!”
- Go 代码:go在
ctx, cancel := context.WithCancel(context.Background()) go deliverPackage(ctx) // 派快递员出发,并把“可取消说明书”给他 // ... 过了一会,客户取消订单了 cancel() // 你拿起对讲机喊“取消任务!”
deliverPackage
函数里,快递员需要不断地检查说明书:select { case <-ctx.Done(): return }
。一旦收到cancel
信号,ctx.Done()
这个通道就会关闭,快递员就知道该返回了。
完整的示例demo:
go
package main
import (
"context"
"fmt"
"time"
)
// 模拟一个耗时的任务,比如处理数据、调用API等
func longRunningTask(ctx context.Context, taskName string) {
for i := 1; i <= 10; i++ {
// 关键:在每次循环开始时检查context是否被取消
select {
case <-ctx.Done(): // 当cancel()被调用时,这个case会被触发
// 如果context被取消,就停止任务
fmt.Printf("【%s】任务被取消啦!原因:%v\n", taskName, ctx.Err())
return
default:
// 如果没有被取消,就继续执行任务
fmt.Printf("【%s】正在执行第 %d/10 步...\n", taskName, i)
time.Sleep(500 * time.Millisecond) // 模拟耗时操作
}
}
fmt.Printf("【%s】任务完成!\n", taskName)
}
func main() {
fmt.Println("=== 场景1:正常完成任务 ===")
demoNormalCompletion()
fmt.Println("\n=== 场景2:中途取消任务 ===")
demoCancellation()
fmt.Println("\n=== 场景3:多个goroutine共享同一个context ===")
demoMultipleGoroutines()
}
// 场景1:让任务正常完成
func demoNormalCompletion() {
// 创建可取消的context
ctx, cancel := context.WithCancel(context.Background())
defer cancel() // 确保函数退出时释放资源
fmt.Println("启动任务,3秒后任务会自动完成...")
go longRunningTask(ctx, "正常任务")
// 等待足够长的时间让任务完成
time.Sleep(6 * time.Second)
fmt.Println("场景1结束")
}
// 场景2:中途取消任务
func demoCancellation() {
ctx, cancel := context.WithCancel(context.Background())
fmt.Println("启动任务,但2秒后会取消它...")
go longRunningTask(ctx, "可取消任务")
// 2秒后取消任务
time.AfterFunc(2*time.Second, func() {
fmt.Println("主程序发出取消信号!")
cancel() // 这里调用cancel()来取消context
})
// 等待一段时间观察结果
time.Sleep(4 * time.Second)
fmt.Println("场景2结束")
}
// 场景3:多个goroutine共享同一个context
func demoMultipleGoroutines() {
ctx, cancel := context.WithCancel(context.Background())
// 启动3个worker
for i := 1; i <= 3; i++ {
go func(workerID int) {
workerTask(ctx, workerID)
}(i)
}
fmt.Println("3个worker已启动,3秒后全部取消...")
time.Sleep(3 * time.Second)
fmt.Println("取消所有worker!")
cancel() // 一个cancel()调用可以取消所有相关的goroutine
time.Sleep(1 * time.Second)
fmt.Println("场景3结束")
}
// Worker任务,会持续运行直到被取消
func workerTask(ctx context.Context, workerID int) {
for {
select {
case <-ctx.Done():
fmt.Printf("Worker %d 收到取消信号,停止工作\n", workerID)
return
default:
fmt.Printf("Worker %d 正在工作...\n", workerID)
time.Sleep(1 * time.Second)
}
}
}
/**
=== 场景1:正常完成任务 ===
启动任务,3秒后任务会自动完成...
【正常任务】正在执行第 1/10 步...
【正常任务】正在执行第 2/10 步...
【正常任务】正在执行第 3/10 步...
【正常任务】正在执行第 4/10 步...
【正常任务】正在执行第 5/10 步...
【正常任务】正在执行第 6/10 步...
【正常任务】正在执行第 7/10 步...
【正常任务】正在执行第 8/10 步...
【正常任务】正在执行第 9/10 步...
【正常任务】正在执行第 10/10 步...
【正常任务】任务完成!
场景1结束
=== 场景2:中途取消任务 ===
启动任务,但2秒后会取消它...
【可取消任务】正在执行第 1/10 步...
【可取消任务】正在执行第 2/10 步...
【可取消任务】正在执行第 3/10 步...
【可取消任务】正在执行第 4/10 步...
主程序发出取消信号!
【可取消任务】任务被取消啦!原因:context canceled
场景2结束
=== 场景3:多个goroutine共享同一个context ===
3个worker已启动,3秒后全部取消...
Worker 2 正在工作...
Worker 1 正在工作...
Worker 3 正在工作...
Worker 3 正在工作...
Worker 1 正在工作...
Worker 2 正在工作...
Worker 3 正在工作...
Worker 2 正在工作...
Worker 1 正在工作...
取消所有worker!
Worker 3 收到取消信号,停止工作
Worker 2 收到取消信号,停止工作
Worker 1 收到取消信号,停止工作
场景3结束
*/
3. context.WithTimeout()
和 context.WithDeadline()
- 带时间限制的任务
你告诉快递员:“你必须在下单后 2小时内 送到(Timeout),否则算超时失败!” 或者 “你必须在 今天下午5点前 送到(Deadline),否则仓库关门了!”
- Go 代码:go快递员的工作方式和上面一样,他会监听说明书。时间一到,说明书会自动发出“取消”信号,不需要你手动打电话。这对于防止 HTTP 请求等操作无限期挂起至关重要。
// 方式一:2小时超时 ctx, cancel := context.WithTimeout(context.Background(), 2 * time.Hour) defer cancel() // 无论任务成功还是超时,都释放资源 // 方式二:下午5点截止 deadline := time.Date(2023, 10, 27, 17, 0, 0, 0, time.Local) ctx, cancel := context.WithDeadline(context.Background(), deadline) defer cancel() go deliverPackage(ctx)
context.WithTimeout
go
package main
import (
"context"
"fmt"
"time"
)
func apiCall(ctx context.Context) error {
select {
case <-time.After(3 * time.Second):
return nil // 正常完成
case <-ctx.Done():
return fmt.Errorf("请求被取消: %v", ctx.Err())
}
}
func main() {
// HTTP请求设置1秒超时
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
if err := apiCall(ctx); err != nil {
fmt.Println("错误:", err) // 会输出这里
} else {
fmt.Println("请求成功")
}
}
// 错误: 请求被取消: context deadline exceeded
context.WithDeadline
go
package main
import (
"context"
"fmt"
"time"
)
func main() {
// 设置具体的截止时间(3秒后)
deadline := time.Now().Add(3 * time.Second)
ctx, cancel := context.WithDeadline(context.Background(), deadline)
defer cancel()
// 模拟需要5秒的操作
select {
case <-time.After(5 * time.Second):
fmt.Println("长时间操作完成")
case <-ctx.Done():
fmt.Println("到达截止时间:", ctx.Err()) // 3秒后会执行这里
}
}
// 到达截止时间: context deadline exceeded
4. context.WithValue()
- 携带额外信息的任务
你告诉快递员:“这个包裹是易碎品(fragile=true
),收件人王太太脾气不好(attitude=grumpy
),这些信息你路上看看,处理包裹时小心点。” 这些信息不是配送指令,而是和这次特定任务相关的元数据。
- Java 类比:有点像把数据放入
ThreadLocal
,但它是显式传递的、更安全的。或者在 Web 框架中,类似于HttpServletRequest
的 attribute,用于在请求处理链中传递用户信息、认证令牌等。 - Go 代码:go在
// 把用户ID和认证令牌附加到说明书上 ctx := context.WithValue(context.Background(), "userID", 12345) ctx = context.WithValue(ctx, "authToken", "abc123") go deliverPackage(ctx)
deliverPackage
内部,或者更深层的函数(比如需要认证的函数)里,可以读取这些值:gotoken := ctx.Value("authToken")