什么是go context
context 是 Go 语言标准库(context 包)提供的核心工具,用于在 Goroutine 之间传递取消信号、超时控制、请求元数据(如 trace ID、用户信息),是构建健壮、可管控的并发程序的基础。本文将从核心原理、核心类型、实战用法、最佳实践四个维度,全面解析 context 的使用方式。
一、Context 核心原理
1. 为什么需要 Context?
Go 程序的并发依赖 Goroutine,但 Goroutine 之间默认没有直接的通信方式来传递“取消/超时”信号:
- 场景1:HTTP 请求处理超时,需要取消所有关联的 Goroutine(如数据库查询、RPC 调用);
- 场景2:分布式追踪时,需要在多个 Goroutine 之间传递 trace ID;
- 场景3:用户取消操作(如关闭客户端),需要终止后台运行的 Goroutine。
context 解决的核心问题:在树形结构的 Goroutine 之间安全传递取消信号、超时时间和元数据,且保证并发安全。
2. Context 核心接口
context.Context 是一个接口,定义了 4 个核心方法,所有 Context 类型都实 现了该接口:
type Context interface {
// 取消信号:返回一个只读通道,当 Context 被取消/超时时,通道会关闭
Done() <-chan struct{}
// 取消原因:返回 Context 被取消的原因(nil 表示未取消)
Err() error
// 超时时间:返回 Context 的截止时间(ok=false 表示无截止时间)
Deadline() (deadline time.Time, ok bool)
// 元数据:根据 key 获取传递的元数据(并发安全)
Value(key any) any
}
3. Context 的核心特性
- 树形派生:Context 采用“父-子”派生模式,父 Context 取消时,所有子 Context 会被递归取消;
- 不可修改:Context 的值和取消状态只能由创建者修改,使用者仅能读取;
- 并发安全:所有方法均可被多个 Goroutine 安全调用;
- 空 Context:
context.Background()和context.TODO()是所有 Context 的根,永不取消、无超时、无值。
二、Context 核心类型与创建方式
Go 提供了 4 种核心的 Context 派生方式,覆盖绝大多数场景:
| 类型 | 创建函数 | 核心作用 |
|---|---|---|
| 空 Context | context.Background()/context.TODO() | 根 Context,无取消、无超时、无值 |
| 可取消 Context | context.WithCancel(parent) | 手动触发取消信号 |
| 超时 Context | context.WithTimeout(parent, d) | 超时自动取消(或手动取消) |
| 截止时间 Context | context.WithDeadline(parent, t) | 指定时间自动取消(与 Timeout 本质相同) |
| 带值 Context | context.WithValue(parent, k, v) | 传递元数据(如 trace ID、用户信息) |
1. 空 Context:Background 与 TODO
context.Background():推荐作为所有 Context 的根,用于主函数、初始化、测试等顶层场景;context.TODO():用于“暂时不确定使用哪个 Context”的场景(如重构阶段),本质与 Background 一致。
// 示例:创建根 Context
ctx := context.Background()
// 或
ctx := context.TODO()
2. 可取消 Context(WithCancel)
context.WithCancel 会返回一个子 Context 和一个取消函数(CancelFunc),调用取消函数时,子 Context 的 Done() 通道会关闭,所有监听该通道的 Goroutine 会收到取消信号。
核 心用法:手动取消 Goroutine
package main
import (
"context"
"fmt"
"time"
)
// 模拟一个耗时任务(如数据库查询)
func doTask(ctx context.Context, taskID int) {
for {
select {
case <-ctx.Done():
// 收到取消信号,退出任务
fmt.Printf("任务%d:收到取消信号,原因:%v\n", taskID, ctx.Err())
return
default:
// 执行任务逻辑
fmt.Printf("任务%d:运行中...\n", taskID)
time.Sleep(500 * time.Millisecond)
}
}
}
func main() {
// 1. 创建根 Context
rootCtx := context.Background()
// 2. 派生可取消的子 Context
ctx, cancel := context.WithCancel(rootCtx)
defer cancel() // 确保最终调用取消,避免内存泄漏
// 3. 启动 2 个 Goroutine 执行任务
go doTask(ctx, 1)
go doTask(ctx, 2)
// 4. 3 秒后手动取消
time.Sleep(3 * time.Second)
fmt.Println("主 Goroutine:触发取消")
cancel()
// 等待 Goroutine 退出
time.Sleep(1 * time.Second)
fmt.Println("程序退出")
}
运行结果:
任务1:运行中...
任务2:运行中...
任务1:运行中...
任务2:运行中...
任务1:运行中...
任务2:运行中...
任务1:运行中...
任务2:运行中...
任务1:运行中...
任务2:运行中...
主 Goroutine:触发取消
任务1:收到取消信号,原因:context canceled
任务2:收到取消信号,原因:context canceled
程序退出
3. 超时/截止时间 Context(WithTimeout/WithDeadline)
(1)WithTimeout:超时自动取消
context.WithTimeout(parent, d) 会创建一个子 Context,当超过 d 时间后自动取消,也可手动调用 CancelFunc 提前取消。
package main
import (
"context"
"fmt"
"time"
)
// 模拟 RPC 调用,支持超时控制
func rpcCall(ctx context.Context, service string) error {
fmt.Printf("开始调用 %s...\n", service)
select {
case <-ctx.Done():
// 超时/取消时返回错误
return fmt.Errorf("调用 %s 失败:%v", service, ctx.Err())
case <-time.After(2 * time.Second): // 模拟 RPC 耗时 2 秒
fmt.Printf("调用 %s 成功\n", service)
return nil
}
}
func main() {
// 1. 创建超时 Context(1 秒超时)
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel() // 即使超时,也建议调用 cancel 释放资源
// 2. 执行 RPC 调用
err := rpcCall(ctx, "user-service")
if err != nil {
fmt.Println("错误:", err)
return
}
fmt.Println("执行完成")
}
运行结果(超时场景):
开始调用 user-service...
错误:调用 user-service 失败:context deadline exceeded
(2)WithDeadline:指定截止时间取消
context.WithDeadline(parent, t) 与 WithTimeout 本质相同,WithTimeout 是 WithDeadline 的封装(t = time.Now().Add(d)):
// 创建截止时间为 1 秒后的 Context
deadline := time.Now().Add(1 * time.Second)
ctx, cancel := context.WithDeadline(context.Background(), deadline)
defer cancel()
4. 带值 Context(WithValue)
用于在 Goroutine 之间传递只读的元数据(如 trace ID、用户 ID、请求头),核心规则:
- 键(key)必须是可比较的类型(如自定义类型、string),避免与其他包的键冲突;
- 值(value)必须是线程安全的(如不可变字符串、int);
- 仅传递“请求域”的元数据,不传递业务参数(业务参数建议通过函数参数传递)。
实战:传递 Trace ID
package main
import (
"context"
"fmt"
"time"
)
// 定义自定义 key 类型(避免与其他包冲突)
type traceIDKey struct{}
// 模拟日志函数,从 Context 中获取 trace ID
func log(ctx context.Context, msg string) {
traceID, ok := ctx.Value(traceIDKey{}).(string)
if !ok {
traceID = "unknown"
}
fmt.Printf("[traceID: %s] %s\n", traceID, msg)
}
// 模拟业务函数,嵌套 Goroutine
func business(ctx context.Context) {
log(ctx, "开始处理 业务")
// 启动子 Goroutine
go func() {
log(ctx, "子 Goroutine 执行中")
time.Sleep(100 * time.Millisecond)
log(ctx, "子 Goroutine 执行完成")
}()
time.Sleep(200 * time.Millisecond)
log(ctx, "业务处理完成")
}
func main() {
// 1. 创建根 Context
rootCtx := context.Background()
// 2. 派生带值 Context(传递 trace ID)
ctx := context.WithValue(rootCtx, traceIDKey{}, "trace-123456")
// 3. 执行业务
business(ctx)
time.Sleep(300 * time.Millisecond)
}
运行结果:
[traceID: trace-123456] 开始处理业务
[traceID: trace-123456] 子 Goroutine 执行中
[traceID: trace-123456] 子 Goroutine 执行完成
[traceID: trace-123456] 业务处理完成
三、Context 实战 场景
1. HTTP 服务中的 Context 用法
Go 的 net/http 包天然支持 Context:http.Request 包含 Context() 方法,可传递取消信号和元数据。
示例:HTTP 请求超时控制
package main
import (
"context"
"fmt"
"net/http"
"time"
)
// 处理 HTTP 请求,模拟耗时操作
func handleRequest(w http.ResponseWriter, r *http.Request) {
// 1. 获取请求的 Context(客户端断开连接时会自动取消)
ctx := r.Context()
fmt.Println("请求开始")
defer fmt.Println("请求结束")
// 2. 模拟耗时操作(数据库查询)
select {
case <-ctx.Done():
// 客户端断开/超时,返回错误
fmt.Printf("请求取消:%v\n", ctx.Err())
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("请求被取消"))
return
case <-time.After(5 * time.Second):
// 正常响应
w.Write([]byte("请求处理完成"))
}
}
func main() {
http.HandleFunc("/", handleRequest)
// 启动 HTTP 服务
fmt.Println("服务启动在 :8080")
http.ListenAndServe(":8080", nil)
}
测试:访问 http://localhost:8080 后立即关闭浏览器,服务端会打印 请求取消:context canceled。
