引入
服务调用了下游 API,等了 30 秒还没返回,用户早跑了。goroutine 启动后怎么优雅取消?多个请求共享的用户信息怎么传递?context.Context 统一管理超时、取消和请求级数据,是 Go 并发编程的基础设施。
正文
定义
定义
context.Context是 Go 标准库中用于传递截止时间、取消信号和请求级数据的机制。它通过WithCancel、WithTimeout、WithValue等函数创建派生 context,沿调用链传播,控制 goroutine 的生命周期。
语法
import "context"
ctx := context.Background()
ctx, cancel := context.WithCancel(ctx)
defer cancel()
select {
case <-ctx.Done():
fmt.Println(ctx.Err())
default:
// 正常工作
}例子
WithCancel
func worker(ctx context.Context) {
for {
select {
case <-ctx.Done():
fmt.Println("收到取消信号:", ctx.Err())
return
default:
fmt.Println("工作中...")
time.Sleep(500 * time.Millisecond)
}
}
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
go worker(ctx)
time.Sleep(2 * time.Second)
cancel()
time.Sleep(100 * time.Millisecond)
}WithCancel 返回一个可取消的 context 和 cancel 函数,调用 cancel() 后 ctx.Done() 通道关闭,所有监听方收到信号。
WithTimeout
func fetchData(ctx context.Context) (string, error) {
ch := make(chan string, 1)
go func() {
time.Sleep(3 * time.Second)
ch <- "数据"
}()
select {
case result := <-ch:
return result, nil
case <-ctx.Done():
return "", ctx.Err()
}
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
result, err := fetchData(ctx)
if err != nil {
fmt.Println("超时:", err) // context deadline exceeded
} else {
fmt.Println(result)
}
}WithTimeout 设置截止时间,到期后自动取消。比手动算时间再用 WithCancel 方便得多。
WithValue
type key string
const userIDKey key = "userID"
func handleRequest(ctx context.Context) {
uid := ctx.Value(userIDKey)
fmt.Println("用户ID:", uid)
}
func main() {
ctx := context.WithValue(context.Background(), userIDKey, "12345")
handleRequest(ctx) // 用户ID: 12345
}WithValue 在 context 中存请求级数据,沿调用链向下传递。key 要用自定义类型避免冲突。
WithDeadline
deadline := time.Now().Add(2 * time.Second)
ctx, cancel := context.WithDeadline(context.Background(), deadline)
defer cancel()
select {
case <-ctx.Done():
fmt.Println(ctx.Err())
case <-time.After(3 * time.Second):
fmt.Println("完成")
}WithDeadline 指定具体截止时间点,WithTimeout 是它的简化版(基于当前时间加偏移)。
context 传递
func doWork(ctx context.Context) {
subCtx, cancel := context.WithTimeout(ctx, 500*time.Millisecond)
defer cancel()
select {
case <-subCtx.Done():
fmt.Println("子任务超时:", subCtx.Err())
case <-time.After(1 * time.Second):
fmt.Println("子任务完成")
}
}子 context 继承父 context 的取消信号和截止时间,父取消则子也取消,形成树状传播。
常见写法
| 写法 | 说明 |
|---|---|
context.Background() | 根 context,用于 main 和初始化 |
context.TODO() | 占位用,表示还没想好用什么 context |
context.WithCancel(parent) | 创建可取消的 context |
context.WithTimeout(parent, d) | 创建带超时的 context |
context.WithDeadline(parent, t) | 创建带截止时间的 context |
context.WithValue(parent, k, v) | 存储请求级数据 |
ctx.Done() | 返回取消信号通道 |
ctx.Err() | 返回取消原因(Canceled 或 DeadlineExceeded) |
ctx.Value(key) | 获取存储的值 |
defer cancel() | 确保 cancel 被调用,防止泄漏 |
特点
context是不可变的,每个WithXxx返回新的子 contextcancel()必须调用,否则 goroutine 和资源会泄漏,用defer cancel()保底- context 形成树状结构,父取消则所有子节点取消
WithValue只存请求级数据(如 trace ID、用户 ID),别拿来存配置WithValue的 key 要用自定义类型,避免和其他包冲突context.Background()用于顶层,context.TODO()用于还没确定的地方- 函数签名把
context.Context作为第一个参数是 Go 的惯例
理解
context.Context 就是 Go 并发世界的”信号灯系统”。WithCancel 手动关,WithTimeout 自动关,WithValue 带行李。父 context 一取消,所有子 context 跟着取消,像树一样一层层传下去。记住 defer cancel() 别忘写,context 当第一个参数传,这两条是铁律。
引出
到这里,Go 的核心知识点基本过了一遍。从基础类型、函数、结构体、接口到并发、测试、工具链、泛型、反射、正则和 context,这些知识组合起来就能应对大部分实际项目了。下一步就是动手写项目——一个小工具、一个 API 服务、一个 CLI 程序,把学到的东西用起来,才是真正学会 Go 的开始。