「笔记006」Go语言Context初识 上
1. 为什么需要 context
?
1.1 背景问题
在 Go 中,协程(goroutine)是轻量级线程,可以轻松地启动数以千计的并发任务。然而,这种便利也带来了一些问题:
无法优雅地终止协程:
- 一旦一个协程启动,除非显式退出,否则它可能一直运行下去,导致资源泄露。
- 如果一个任务失败了,依赖它的其他任务也需要取消,而传统的手段难以统一管理。
任务的超时控制复杂:
- 某些操作(如 HTTP 请求、数据库查询)需要超时控制,如果任务超时没有终止,可能会拖垮整个系统。
函数间依赖数据传递麻烦:
- 在多层函数调用中传递元数据(如用户身份、请求 ID)时,需要修改函数签名,这会增加代码的复杂性。
1.2 context
的设计目标
为了解决这些问题,Go 设计了 context
,其目标包括:
- 取消信号传递:在协程树中优雅地传递取消信号。
- 超时控制:提供简单一致的超时处理方式。
- 上下文数据传递:避免修改函数签名的情况下传递元数据。
2. context
的基本概念
在 Go 中,context
是一个接口,定义了以下主要方法:
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key any) any
}
2.1 context.Background
和 context.TODO
这两个是创建 context
的入口点。
context.Background
:- 是一个顶层空
context
,通常作为根context
使用。 - 常用于主函数、初始化逻辑或测试代码中。
示例:
ctx := context.Background()
- 是一个顶层空
context.TODO
:- 是一个临时占位的
context
,表示还不确定要用什么类型的context
。 - 使用场景:
- 开发中的代码尚未决定使用何种
context
。 - 快速原型开发时的占位。
- 开发中的代码尚未决定使用何种
- 是一个临时占位的
实际开发中,context.Background
是更常用的选择,context.TODO
主要用于过渡。
2.2 context.WithCancel
context.WithCancel
是创建可取消 context
的常用方法。
用法:
ctx, cancel := context.WithCancel(parentCtx) defer cancel() // 确保资源释放
原理:
- 返回一个子
context
,它继承了父context
的取消信号。 - 调用
cancel()
会关闭ctx.Done()
通道,并通知所有监听该通道的协程。
- 返回一个子
示例:
func main() {
ctx, cancel := context.WithCancel(context.Background())
go func(ctx context.Context) {
for {
select {
case <-ctx.Done():
fmt.Println("Worker canceled")
return
default:
fmt.Println("Working...")
time.Sleep(1 * time.Second)
}
}
}(ctx)
time.Sleep(3 * time.Second)
cancel() // 取消任务
time.Sleep(1 * time.Second)
}
运行结果:
Working...
Working...
Working...
Worker canceled
注意:如果不调用 cancel()
,协程将一直运行,可能导致资源泄露。
2.3 context.WithTimeout
和 context.WithDeadline
这两者是用于设置任务超时的核心方法。
context.WithTimeout
:- 创建一个带有超时时间的
context
,超时后自动取消。 - 示例:
- 创建一个带有超时时间的
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
select {
case <-time.After(3 * time.Second):
fmt.Println("Task completed")
case <-ctx.Done():
fmt.Println("Timeout:", ctx.Err())
}
context.WithDeadline
:- 与
WithTimeout
类似,但通过设定截止时间控制超时。 - 示例:
- 与
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(2*time.Second))
defer cancel()
运行时,超时触发 ctx.Done()
通道关闭,并返回 context.DeadlineExceeded
错误。
2.4 context.WithValue
context.WithValue
用于在 context
中存储和传递数据。
- 用法:
ctx := context.WithValue(context.Background(), "userID", 12345)
value := ctx.Value("userID")
fmt.Println("User ID:", value)
- 底层实现:
context
并未使用map
存储键值对,而是通过链表结构存储每个key, value
。- 这种设计使得创建子
context
更高效。
注意事项:
避免滥用
WithValue
:- 它仅适合传递少量的元数据(如用户身份、请求范围信息)。
- 传递大量数据或复杂对象应使用其他方式。
使用自定义类型作为键:
- 为避免键冲突,推荐使用自定义类型:
type ctxKey string
const UserIDKey ctxKey = "userID"
3. 基础示例:协程控制
下面为 context
管理协程对例子:
package main
import (
"context"
"fmt"
"time"
)
func worker(ctx context.Context, id int) {
for {
select {
case <-ctx.Done():
fmt.Printf("Worker %d canceled: %v\n", id, ctx.Err())
return
default:
fmt.Printf("Worker %d is working...\n", id)
time.Sleep(500 * time.Millisecond)
}
}
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// 启动多个协程
for i := 1; i <= 3; i++ {
go worker(ctx, i)
}
// 等待一段时间后取消
time.Sleep(2 * time.Second)
cancel()
time.Sleep(1 * time.Second) // 等待所有协程退出
}
运行结果:
Worker 1 is working...
Worker 2 is working...
Worker 3 is working...
Worker 1 is working...
Worker 2 is working...
Worker 3 is working...
Worker 1 canceled: context canceled
Worker 2 canceled: context canceled
Worker 3 canceled: context canceled
4. 小结
context.Background
是程序的根context
,推荐用作起点。context.WithCancel
用于显式取消任务,而WithTimeout
和WithDeadline
实现了自动超时控制。WithValue
用于传递轻量级的元数据,但应避免滥用。