# Golang Context 学习笔记
本文基于 Go 1.21 源码分析
# 📋 目录
- 前言
- Go 1.21 Context 的重要更新
- Context 核心接口
- emptyCtx:空上下文
- cancelCtx:可取消上下文
- timerCtx:定时取消上下文
- valueCtx:值传递上下文
- 性能优化详解
- 总结与最佳实践
# 前言
Context 是 Golang 中用于处理并发控制的经典工具,主要解决以下三个问题:
- 并发协调:在多个 goroutine 之间传递取消信号
- 生命周期管理:控制 goroutine 的启动和终止,统一超时、截止时间、取消等控制逻辑
- 数据传递:在调用链中传递请求范围的数据,跨层级传递共享数据
本文深入分析 context 的四种实现类型及其优化细节。
# Go 1.21 Context 的重要更新
# 核心变更
emptyCtx 类型改变:
- Go 1.20 及之前:
type emptyCtx int - Go 1.21 开始:
type emptyCtx struct{}
这是一个重要的底层实现变更,虽然对外部 API 无影响,但改变了内部设计哲学。
# 为什么要改?
旧设计(int 类型)的理由:
// Go 1.20 源码注释 | |
// An emptyCtx is never canceled, has no values, and has no deadline. | |
// It is not struct{}, since vars of this type must have distinct addresses. | |
type emptyCtx int |
- 使用
int是为了让background和todo两个实例有不同的内存地址 struct{}类型的所有实例在 Go 中可能共享同一个地址(零字节优化)
新设计(struct {} 类型)的考量:
// Go 1.21+ 源码 | |
type emptyCtx struct{} |
- Go 团队重新评估后认为区分
background和todo的地址并非必要 struct{}更符合语义:一个真正 "空" 的上下文- 简化了类型系统,更加直观
# Context 核心接口
# 接口定义
type Context interface { | |
Deadline() (deadline time.Time, ok bool) // 返回过期时间 | |
Done() <-chan struct{} // 返回取消信号通道 | |
Err() error // 返回取消原因 | |
Value(key any) any // 获取存储的值 | |
} |
# 四种核心实现
| 类型 | 用途 | 创建函数 |
|---|---|---|
| emptyCtx | 根上下文,永不取消 | Background() , TODO() |
| cancelCtx | 可手动取消 | WithCancel() |
| timerCtx | 定时自动取消 | WithDeadline() , WithTimeout() |
| valueCtx | 携带键值对数据 | WithValue() |
# 标准错误类型
// 主动取消错误 | |
var Canceled = errors.New("context canceled") | |
// 超时取消错误 (实现了 net.Error 接口) | |
var DeadlineExceeded error = deadlineExceededError{} | |
type deadlineExceededError struct{} | |
func (deadlineExceededError) Error() string { return "context deadline exceeded" } | |
func (deadlineExceededError) Timeout() bool { return true } | |
func (deadlineExceededError) Temporary() bool { return true } |
理解要点:
Canceled:调用 cancel 函数时返回DeadlineExceeded:达到截止时间时返回Temporary()返回 true 表示错误是临时的,可以重试
# emptyCtx:空上下文
# 数据结构
//emptyCtx 是一个空结构体,不占用任何实际内存 | |
type emptyCtx struct{} | |
// 四个接口方法都是空实现 | |
func (*emptyCtx) Deadline() (deadline time.Time, ok bool) { | |
return // 返回零值 time.Time 和 false | |
} | |
func (*emptyCtx) Done() <-chan struct{} { | |
return nil // 返回 nil channel | |
} | |
func (*emptyCtx) Err() error { | |
return nil | |
} | |
func (*emptyCtx) Value(key any) any { | |
return nil | |
} | |
func (*emptyCtx) String() string { | |
// 用于调试,区分 background 和 todo | |
switch e { | |
case background: | |
return "context.Background" | |
case todo: | |
return "context.TODO" | |
} | |
return "unknown empty Context" | |
} |
# 特点分析
emptyCtx 是最简单的 context 实现:
- 它的所有方法都返回空值
- 它永远不会被取消
- 没有任何 key/value
- 就是 一个 “纯空壳” 的 Context
| 特性 | 说明 | 影响 |
|---|---|---|
| 类型 | 底层是 struct{} 类型 |
零字节,但编译器可能对齐到 1 字节 |
| 过期 | Deadline 返回零值和 false | 永不过期 |
| 取消 | Done 返回 nil | 读写 nil channel 都会永久阻塞 |
| 错误 | Err 永远返回 nil | 无错误信息 |
| 存储 | Value 永远返回 nil | 无数据存储能力 |
# 内存布局对比
// Go 1.20 及之前 | |
type emptyCtx int | |
//- 占用 8 字节(64 位系统) | |
//- background 和 todo 有不同地址 | |
// Go 1.21+ | |
type emptyCtx struct{} | |
//- 占用 0 字节(理论上) | |
//- 实际可能对齐到 1 字节 | |
//- background 和 todo 可能共享地址(但无影响) |
为什么地址相同也没问题?
background和todo只是语义上的区分- 它们的行为完全相同
- 代码中通过函数名区分意图,而非通过地址比较
# 使用场景
var ( | |
background = new(emptyCtx) | |
todo = new(emptyCtx) | |
) | |
func Background() Context { | |
return background | |
} | |
func TODO() Context { | |
return todo | |
} |
两个单例实例的区别:
| 函数 | 使用场景 | 语义 |
|---|---|---|
Background() |
main 函数、初始化、测试的根上下文 | "这是一个明确的起点" |
TODO() |
暂时不确定用什么上下文 | "这里需要补充合适的 context" |
为什么返回相同类型却用两个函数?
- 语义区分:代码审查时能看出意图差异
- 静态分析友好:工具可以检测并提示 TODO 的使用
- 文档作用:
TODO()提醒开发者后续需要替换 - 后续扩展:未来版本可能有不同实现
实际使用示例:
// ✅ 正确:明确的根上下文 | |
func main() { | |
ctx := context.Background() | |
server.Run(ctx) | |
} | |
// ✅ 正确:暂时占位 | |
func processRequest(req *Request) error { | |
// TODO: 应该从请求中提取 context | |
ctx := context.TODO() | |
return doWork(ctx, req) | |
} | |
// ❌ 错误:滥用 TODO | |
func (s *Service) Start() { | |
ctx := context.TODO() // 应该用 Background | |
s.run(ctx) | |
} |
# cancelCtx:可取消上下文
# 数据结构
type cancelCtx struct { | |
Context // 父 context | |
mu sync.Mutex // 保护以下字段 | |
done atomic.Value // 类型为 chan struct {},懒加载 | |
children map[canceler]struct{} // 子 context 集合 | |
err error // 第一次取消时设置的错误 | |
cause error // 取消的根本原因 (Go 1.20+) | |
} | |
type canceler interface { | |
cancel(removeFromParent bool, err, cause error) | |
Done() <-chan struct{} | |
} |
# 设计亮点
-
atomic.Value + Mutex 双重保护
// 快速路径(无锁):读取 done channeld := c.done.Load()
// 慢速路径(加锁):初始化 done channelc.mu.Lock()
c.done.Store(make(chan struct{}))
c.mu.Unlock()
-
children map 的高效管理
// 使用空 struct {} 作为值,零内存占用children map[canceler]struct{}
// 添加子节点p.children[child] = struct{}{}
// 批量取消,O (N) 复杂度for child := range c.children {
child.cancel(false, err, cause)
} -
cause 字段的引入
// 区分直接原因和根本原因c.err = Canceled // 直接原因:被取消
c.cause = ErrTimeout // 根本原因:上游超时
# Done 方法:懒加载 + Double Check
func (c *cancelCtx) Done() <-chan struct{} { | |
// 第一次检查:无锁快速路径 | |
d := c.done.Load() | |
if d != nil { | |
return d.(chan struct{}) | |
} | |
// 慢速路径:需要初始化 | |
c.mu.Lock() | |
defer c.mu.Unlock() | |
// 第二次检查:防止并发重复创建 | |
d = c.done.Load() | |
if d == nil { | |
d = make(chan struct{}) | |
c.done.Store(d) | |
} | |
return d.(chan struct{}) | |
} |
性能分析:
// 场景 1:已初始化的 context (90% 的情况) | |
// 执行路径:Load → 类型断言 → 返回 | |
// 耗时:~1-2ns (无锁,CPU cache 友好) | |
// 场景 2:未初始化的 context (首次调用) | |
// 执行路径:Load → Lock → Load → make → Store → Unlock → 返回 | |
// 耗时:~50-100ns (含锁开销) | |
// 场景 3:并发初始化 (极少发生) | |
// 第一个协程:完整慢速路径,创建 channel | |
// 后续协程:在锁外等待 → 第二次检查成功 → 返回已创建的 channel |
为什么用 atomic.Value 而不是 sync.RWMutex?
| 方案 | Load 开销 | 并发性能 | 适用场景 |
|---|---|---|---|
| atomic.Value | ~1ns | 无锁,完美扩展 | 读多写少(done 只创建一次) |
| sync.RWMutex | ~20ns | RLock 仍有竞争 | 读写都频繁 |
| sync.Mutex | ~50ns | 读也需要加锁 | 写操作为主 |
done channel 的特点:
- 创建一次,读取多次(典型的读多写少)
- 创建后永不改变(非常适合 atomic.Value)
- 高并发读取(select 语句会频繁调用 Done ())
# WithCancel 创建流程
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) { | |
c := withCancel(parent) | |
// 返回 context 和取消函数 | |
return c, func() { c.cancel(true, Canceled, nil) } | |
} | |
func withCancel(parent Context) *cancelCtx { | |
// 参数校验 | |
if parent == nil { | |
panic("cannot create context from nil parent") | |
} | |
// 创建 cancelCtx | |
c := &cancelCtx{} | |
// 核心:建立父子关系,传播取消信号 | |
c.propagateCancel(parent, c) | |
return c | |
} |
使用示例:
// 创建可取消的 context | |
ctx, cancel := context.WithCancel(context.Background()) | |
defer cancel() // 确保资源释放 | |
// 启动多个 goroutine | |
for i := 0; i < 10; i++ { | |
go func(id int) { | |
select { | |
case <-ctx.Done(): | |
fmt.Printf("Worker %d: canceled\n", id) | |
return | |
case <-doWork(): | |
fmt.Printf("Worker %d: done\n", id) | |
} | |
}(i) | |
} | |
// 在某个条件下取消所有 goroutine | |
if someCondition { | |
cancel() // 一次调用,取消所有子 goroutine | |
} |
# propagateCancel:取消信号传播机制
这是 cancelCtx 最核心的方法,决定了如何响应父 context 的取消:
//propagateCancel 设置父子 context 之间的取消传播关系,确保父 context 被取消时子 context 也会被取消 | |
// 参数: | |
// - parent: 父级 Context,当它被取消时会触发子级取消 | |
// - child: 需要在父级取消时一同被取消的 canceler(子级) | |
func (c *cancelCtx) propagateCancel(parent Context, child canceler) { | |
// 将当前 cancelCtx 的父 context 设置为传入的 parent | |
c.Context = parent | |
// 获取父 context 的 Done channel | |
done := parent.Done() | |
// 如果父 context 永远不会被取消(Done 为 nil),则直接返回 | |
if done == nil { | |
return // parent is never canceled | |
} | |
// 检查父 context 是否已经被取消 | |
select { | |
case <-done: | |
// 父 context 已经被取消,立即取消子 context | |
// 使用 false 作为 removeFromParent 参数是因为父 context 已经取消, | |
// 不需要从父 context 的 children 中移除子 context | |
child.cancel(false, parent.Err(), Cause(parent)) | |
return | |
default: | |
} | |
// 尝试获取父 context 对应的 *cancelCtx | |
// 如果成功,则可以直接操作其 children 映射来建立父子关系 | |
if p, ok := parentCancelCtx(parent); ok { | |
//parent 是一个 *cancelCtx 或者派生自 *cancelCtx | |
p.mu.Lock() | |
// 再次检查父 context 是否已经被取消(双重检查锁定模式) | |
if err := p.err.Load(); err != nil { | |
// 父 context 已经被取消,立即取消子 context | |
child.cancel(false, err.(error), p.cause) | |
} else { | |
// 父 context 还未被取消,在其 children 映射中添加子 context | |
if p.children == nil { | |
p.children = make(map[canceler]struct{}) | |
} | |
p.children[child] = struct{}{} | |
} | |
p.mu.Unlock() | |
return | |
} | |
// 检查父 context 是否实现了 afterFuncer 接口(具有 AfterFunc 方法) | |
if a, ok := parent.(afterFuncer); ok { | |
// 父 context 实现了 AfterFunc 方法 | |
c.mu.Lock() | |
// 注册一个回调函数,当父 context 被取消时执行此函数来取消子 context | |
stop := a.AfterFunc(func() { | |
child.cancel(false, parent.Err(), Cause(parent)) | |
}) | |
// 将当前 context 包装成 stopCtx,保存 stop 函数以便后续清理 | |
c.Context = stopCtx{ | |
Context: parent, | |
stop: stop, | |
} | |
c.mu.Unlock() | |
return | |
} | |
// 上述优化路径都不适用时,启动一个新的 goroutine 来监听父 context 的取消信号 | |
// 增加 goroutine 计数器,用于测试目的 | |
goroutines.Add(1) | |
go func() { | |
select { | |
case <-parent.Done(): | |
// 父 context 被取消时,取消子 context | |
child.cancel(false, parent.Err(), Cause(parent)) | |
case <-child.Done(): | |
// 子 context 自己被取消时,什么都不做(退出 goroutine) | |
} | |
}() | |
} |
三种传播策略详解:
| 场景 | 策略 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 父是 emptyCtx | 直接返回 | 无任何开销 | - | Background/TODO 作为父 |
| 父是 cancelCtx | 加入 children map | ・取消时 O (1) 查找 ・无额外协程 ・内存高效 |
需要加锁 | 标准库 context(99%) |
| 父是自定义类型 | 启动监听协程 | ・通用性强 ・支持任意 Context 实现 |
・每个子一个协程 ・内存和调度开销 |
自定义 Context 接口实现 |
策略选择的性能影响:
// 场景 1:使用标准 context(高效) | |
parent := context.Background() | |
ctx1, _ := context.WithCancel(parent) | |
ctx2, _ := context.WithCancel(ctx1) | |
ctx3, _ := context.WithCancel(ctx2) | |
// 资源消耗:3 个 cancelCtx 结构体 | |
// 取消开销:O (N),N = 子节点数量 | |
// 场景 2:使用自定义 context(低效) | |
type myContext struct { | |
context.Context | |
} | |
parent := &myContext{context.Background()} | |
ctx1, _ := context.WithCancel(parent) // 启动 1 个监听协程 | |
ctx2, _ := context.WithCancel(parent) // 启动 1 个监听协程 | |
ctx3, _ := context.WithCancel(parent) // 启动 1 个监听协程 | |
// 资源消耗:3 个 cancelCtx + 3 个 goroutine | |
// 取消开销:channel 通信 + 协程调度 |
# parentCancelCtx:类型识别技巧
func parentCancelCtx(parent Context) (*cancelCtx, bool) { | |
// 快速检查:parent.Done () 返回 nil 或已关闭 | |
done := parent.Done() | |
if done == closedchan || done == nil { | |
return nil, false | |
} | |
// 利用特殊 key 识别 cancelCtx | |
p, ok := parent.Value(&cancelCtxKey).(*cancelCtx) | |
if !ok { | |
return nil, false | |
} | |
// 验证 done channel 一致性 | |
pdone, _ := p.done.Load().(chan struct{}) | |
if pdone != done { | |
return nil, false | |
} | |
return p, true | |
} | |
// 包私有变量,用作识别协议的 key | |
var cancelCtxKey int | |
//cancelCtx 的 Value 方法实现类型识别协议 | |
func (c *cancelCtx) Value(key any) any { | |
if key == &cancelCtxKey { | |
return c // 特殊 key 返回自身 | |
} | |
return value(c.Context, key) // 其他 key 向上查找 | |
} |
设计巧妙之处:
-
避免反射
// ❌ 低效方案(使用反射)if reflect.TypeOf(parent).String() == "*context.cancelCtx" {
return parent.(*cancelCtx), true
}// ✅ 高效方案(使用协议)if p, ok := parent.Value(&cancelCtxKey).(*cancelCtx); ok {
return p, true
} -
利用包私有变量地址
var cancelCtxKey int
//cancelCtxKey 的地址是包私有的,外部无法伪造// &cancelCtxKey 作为 key,不会与用户的 key 冲突 -
done channel 一致性检查
// 场景:自定义 context 可能伪造 Value 方法type fakeCtx struct {
context.Context}func (f *fakeCtx) Value(key any) any {
if key == &cancelCtxKey {
return &cancelCtx{} // 返回假的 cancelCtx
}return f.Context.Value(key)
}//done channel 检查防止这种伪造pdone, _ := p.done.Load().(chan struct{})
if pdone != done {
return nil, false // 检测到不一致,拒绝
}
# cancel 方法:级联取消
func (c *cancelCtx) cancel(removeFromParent bool, err, cause error) { | |
// 参数校验 | |
if err == nil { | |
panic("context: internal error: missing cancel error") | |
} | |
c.mu.Lock() | |
// 幂等性检查:已经取消过 | |
if c.err != nil { | |
c.mu.Unlock() | |
return // 重复调用 cancel 是安全的 | |
} | |
// 设置错误信息 | |
c.err = err | |
if cause == nil { | |
cause = err | |
} | |
c.cause = cause | |
// 关闭 done channel | |
d, _ := c.done.Load().(chan struct{}) | |
if d == nil { | |
// 未初始化:存储预先关闭的 channel | |
c.done.Store(closedchan) | |
} else { | |
// 已初始化:关闭 channel | |
close(d) | |
} | |
// 递归取消所有子 context | |
for child := range c.children { | |
// 注意:removeFromParent=false,避免子节点回调父节点 | |
child.cancel(false, err, cause) | |
} | |
c.children = nil // 释放 map 内存 | |
c.mu.Unlock() | |
// 从父 context 移除自己 | |
if removeFromParent { | |
removeChild(c.Context, c) | |
} | |
} |
关键设计决策:
-
幂等性保证
cancel, cancel := context.WithCancel(parent)
cancel() // 第一次调用
cancel() // 第二次调用:安全,直接返回
cancel() // 第三次调用:安全,直接返回
-
为什么先关闭 channel 再取消子节点?
// 关闭顺序很重要close(d) // ← 先关闭,确保监听者立即收到信号
for child := range c.children {
child.cancel(...) // ← 后取消,可能耗时较长
}// 这样保证:// 1. select {case <-ctx.Done ()} 立即返回// 2. 子节点取消不阻塞当前节点的通知 -
closedchan 全局单例
var closedchan = make(chan struct{})
func init() {
close(closedchan)
}// 优点://- 避免每次都 make + close,节省内存分配//- 所有未初始化就取消的 context 共享同一个已关闭 channel//- select {case <-closedchan} 立即返回 -
removeFromParent 参数的妙用
// 场景 1:用户主动调用 cancel ()cancel()
// → c.cancel(true, Canceled, nil)// → removeChild(parent, c)// 原因:需要从父的 children map 中删除自己// 场景 2:父节点取消触发子节点取消parent.cancel(...)
// → for child := range parent.children {// child.cancel(false, err, cause) // removeFromParent=false// }// → parent.children = nil// 原因:父节点会清空整个 children map,无需逐个删除 -
设置
c.children = nil的作用// 释放 map 占用的内存c.children = nil
// 效果:// 1. 允许 GC 回收 map 本身// 2. 允许 GC 回收子 context(如果没有其他引用)// 3. 防止内存泄漏
# removeChild:从父节点移除
func removeChild(parent Context, child canceler) { | |
// 类型检查:只有 cancelCtx 才有 children | |
p, ok := parentCancelCtx(parent) | |
if !ok { | |
return // 父不是 cancelCtx,无需操作 | |
} | |
p.mu.Lock() | |
if p.children != nil { | |
delete(p.children, child) | |
} | |
p.mu.Unlock() | |
} |
调用场景分析:
// 场景 1:子 context 先于父 context 取消 | |
ctx, cancel := context.WithCancel(parent) | |
cancel() | |
// → c.cancel(true, ...) | |
// → removeChild(parent, c) | |
// → delete(parent.children, c) | |
// 场景 2:父 context 取消导致子 context 取消 | |
parentCancel() | |
// → parent.cancel(...) | |
// → for child := range parent.children { | |
// child.cancel (false, ...) // 不调用 removeChild | |
// } | |
// → parent.children = nil // 直接清空整个 map |
# WithCancelCause:指定取消原因
func WithCancelCause(parent Context) (ctx Context, cancel CancelCauseFunc) { | |
c := withCancel(parent) | |
return c, func(cause error) { c.cancel(true, Canceled, cause) } | |
} | |
// 获取取消的根本原因 | |
func Cause(c Context) error { | |
if cc, ok := c.Value(&cancelCtxKey).(*cancelCtx); ok { | |
cc.mu.Lock() | |
cause := cc.cause | |
cc.mu.Unlock() | |
if cause != nil { | |
return cause | |
} | |
} | |
return c.Err() | |
} |
err vs cause 的区别:
ctx, cancel := context.WithCancelCause(context.Background()) | |
// 取消并指定原因 | |
cancel(fmt.Errorf("database connection failed: %w", err)) | |
// 获取错误信息 | |
fmt.Println(ctx.Err()) // 输出:context canceled | |
fmt.Println(context.Cause(ctx)) // 输出:database connection failed: ... |
| 字段 | 含义 | 用途 |
|---|---|---|
err |
直接原因 | 始终是 Canceled 或 DeadlineExceeded |
cause |
根本原因 | 业务层面的详细错误信息 |
实际应用场景:
// 场景 1:HTTP 请求处理 | |
func handleRequest(w http.ResponseWriter, r *http.Request) { | |
ctx, cancel := context.WithCancelCause(r.Context()) | |
defer cancel(nil) | |
result := make(chan string, 1) | |
go func() { | |
data, err := fetchData(ctx) | |
if err != nil { | |
cancel(fmt.Errorf("fetch failed: %w", err)) | |
return | |
} | |
result <- data | |
}() | |
select { | |
case data := <-result: | |
fmt.Fprintf(w, "Result: %s", data) | |
case <-ctx.Done(): | |
cause := context.Cause(ctx) | |
log.Printf("Request canceled: %v", cause) | |
http.Error(w, "Request failed", 500) | |
} | |
} | |
// 场景 2:分布式追踪 | |
func processWithTracing(ctx context.Context) error { | |
ctx, cancel := context.WithCancelCause(ctx) | |
defer cancel(nil) | |
if err := step1(ctx); err != nil { | |
cancel(fmt.Errorf("step1 failed: %w", err)) | |
return context.Cause(ctx) | |
} | |
if err := step2(ctx); err != nil { | |
cancel(fmt.Errorf("step2 failed: %w", err)) | |
return context.Cause(ctx) | |
} | |
return nil | |
} |
# timerCtx:定时取消上下文
# 数据结构
type timerCtx struct { | |
cancelCtx // 内嵌 cancelCtx,复用其能力 | |
timer *time.Timer // 定时器 | |
deadline time.Time // 截止时间 | |
} |
设计特点:
- 通过内嵌
cancelCtx复用取消逻辑 - 额外增加定时器和截止时间管理
- 支持手动取消和自动超时两种方式
# Deadline 方法
func (c *timerCtx) Deadline() (deadline time.Time, ok bool) { | |
return c.deadline, true | |
} |
这是唯一真正实现 Deadline() 接口的 context 类型:
emptyCtx:返回零值和 falsecancelCtx:返回零值和 falsevalueCtx:向上查找父 context- timerCtx:返回实际截止时间和 true ✓
# WithDeadline:带截止时间的 Context
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) { | |
return WithDeadlineCause(parent, d, nil) | |
} | |
func WithDeadlineCause(parent Context, d time.Time, cause error) (Context, CancelFunc) { | |
// 参数校验 | |
if parent == nil { | |
panic("cannot create context from nil parent") | |
} | |
// 父截止时间更早,直接返回 cancelCtx | |
if cur, ok := parent.Deadline(); ok && cur.Before(d) { | |
// 无需设置新的定时器,父结点超时会自动传播 | |
return WithCancel(parent) | |
} | |
// 创建 timerCtx | |
c := &timerCtx{ | |
deadline: d, | |
} | |
c.cancelCtx.propagateCancel(parent, c) | |
// 计算剩余时间 | |
dur := time.Until(d) | |
if dur <= 0 { | |
// 已过期,立即取消 | |
c.cancel(true, DeadlineExceeded, cause) // deadline has already passed | |
return c, func() { c.cancel(false, Canceled, nil) } | |
} | |
c.mu.Lock() | |
defer c.mu.Unlock() | |
if c.err.Load() == nil { | |
// 启动定时器 | |
c.timer = time.AfterFunc(dur, func() { | |
c.cancel(true, DeadlineExceeded, cause) | |
}) | |
} | |
return c, func() { c.cancel(true, Canceled, nil) } | |
} |
两个关键优化:
-
父截止时间检查
// 场景:parent 5 秒后超时,子 context 请求 10 秒超时parent, _ := context.WithTimeout(ctx, 5*time.Second)
child, _ := context.WithTimeout(parent, 10*time.Second)
// 实际:child 是一个 cancelCtx,不是 timerCtx// 原因:parent 会在 5 秒后超时并取消 child// 优点:节省一个 timer 资源 -
已过期检查
// 场景:请求过去的时间点deadline := time.Now().Add(-1 * time.Second)
ctx, cancel := context.WithDeadline(parent, deadline)
// 效果:ctx.Err () 立即返回 DeadlineExceeded// 优点:不创建 timer,不启动 goroutine
性能对比:
| 场景 | 是否创建 timer | 资源消耗 |
|---|---|---|
| 正常情况 | 是 | timer + timerCtx 结构体 |
| 父截止时间更早 | 否 | 仅 cancelCtx 结构体 |
| 请求过期时间 | 否 | 仅 timerCtx 结构体(已取消) |
# WithTimeout:便捷函数
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) { | |
return WithDeadline(parent, time.Now().Add(timeout)) | |
} |
常见用法:
// HTTP 请求超时 | |
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) | |
defer cancel() | |
req, _ := http.NewRequestWithContext(ctx, "GET", "https://example.com", nil) | |
resp, err := http.DefaultClient.Do(req) | |
// 数据库查询超时 | |
ctx, cancel := context.WithTimeout(ctx, 2*time.Second) | |
defer cancel() | |
rows, err := db.QueryContext(ctx, "SELECT * FROM users WHERE id = ?", userID) | |
// RPC 调用超时 | |
ctx, cancel := context.WithTimeout(ctx, 3*time.Second) | |
defer cancel() | |
reply, err := client.Call(ctx, "Service.Method", args) | |
// 文件操作超时 | |
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) | |
defer cancel() | |
data, err := os.ReadFile(ctx, "/path/to/large/file") |
# cancel 方法:增强版
func (c *timerCtx) cancel(removeFromParent bool, err, cause error) { | |
// 复用 cancelCtx 的取消逻辑 | |
c.cancelCtx.cancel(false, err, cause) | |
// 从父节点移除 | |
if removeFromParent { | |
removeChild(c.cancelCtx.Context, c) | |
} | |
// 停止定时器,防止资源泄漏 | |
c.mu.Lock() | |
if c.timer != nil { | |
c.timer.Stop() | |
c.timer = nil | |
} | |
c.mu.Unlock() | |
} |
为什么必须停止 timer?
// ❌ 错误示例:忘记 cancel | |
func badExample() { | |
for i := 0; i < 10000; i++ { | |
ctx, _ := context.WithTimeout(parent, time.Hour) | |
// 忘记调用 cancel | |
doQuickWork(ctx) // 1 秒内完成 | |
//timer 还会继续运行 59 分 59 秒 | |
} | |
// 结果:10000 个 timer 在后台运行,内存和 CPU 泄漏 | |
} | |
// ✅ 正确示例 | |
func goodExample() { | |
for i := 0; i < 10000; i++ { | |
ctx, cancel := context.WithTimeout(parent, time.Hour) | |
defer cancel() // 或在合适时机调用 | |
doQuickWork(ctx) | |
} | |
//timer 立即停止,资源释放 | |
} |
资源泄漏分析:
// 每个 timer 的资源消耗 | |
type Timer struct { | |
C <-chan Time // 至少 8 字节 | |
r runtimeTimer // 约 88 字节 | |
} | |
// 10000 个 timer | |
// 内存:~960 KB | |
// CPU:定期唤醒检查是否到期 | |
// 影响:GC 压力增大,调度延迟增加 |
正确的使用模式:
// 模式 1:defer cancel(最推荐) | |
func process() error { | |
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) | |
defer cancel() | |
return doWork(ctx) | |
} | |
// 模式 2:显式 cancel | |
func process() error { | |
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) | |
err := doWork(ctx) | |
cancel() // 立即释放 | |
return err | |
} | |
// 模式 3:goroutine 中 cancel | |
func processAsync() { | |
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) | |
go func() { | |
defer cancel() | |
doWork(ctx) | |
}() | |
} |
# timer.Stop () 的行为
//timer.Stop () 的返回值 | |
stopped := c.timer.Stop() | |
if stopped { | |
//timer 还未触发,成功停止 | |
// 定时器的回调函数不会执行 | |
} else { | |
//timer 已经触发或已经被停止 | |
// 可能的情况: | |
// 1. 定时器已经到期,回调已执行或正在执行 | |
// 2. 之前已经调用过 Stop () | |
} |
并发安全性:
// 场景:超时和手动取消同时发生 | |
ctx, cancel := context.WithTimeout(parent, 100*time.Millisecond) | |
//goroutine 1: 手动取消 | |
go func() { | |
time.Sleep(50 * time.Millisecond) | |
cancel() // 调用 timerCtx.cancel → timer.Stop () | |
}() | |
//goroutine 2: 等待超时 | |
// 定时器在 100ms 后自动调用 c.cancel (true, DeadlineExceeded, nil) | |
// 结果:两次 cancel 调用都是安全的 | |
// 第一次:设置 c.err,关闭 channel,停止 timer | |
// 第二次:检测到 c.err != nil,直接返回(幂等性) |
# valueCtx:值传递上下文
# 数据结构
type valueCtx struct { | |
Context // 父 context | |
key, val any // 只存储一对 key-value | |
} |
设计限制:
- 一个 valueCtx 实例只存一对 key-value
- 多个 kv 需要嵌套多层 valueCtx
- 形成链表结构,查找复杂度 O (N)
为什么不用 map?
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| map | O (1) 查找 | 需要复制整个 map 内存分配大 |
存储大量数据 |
| 链表 | 不可变性 结构共享 内存高效 |
O (N) 查找 | 少量数据传递 |
Go 选择链表的原因:
- Context 通常只存储少量元数据(3-5 个键值对)
- 不可变性保证并发安全
- 结构共享避免不必要的复制
# Value 方法:链式查找
func (c *valueCtx) Value(key any) any { | |
// 当前节点匹配 | |
if c.key == key { | |
return c.val | |
} | |
// 递归向父节点查找 | |
return value(c.Context, key) | |
} |
# 通用查找函数
//value 在 context 链中递归查找指定 key 的值 | |
// 通过类型断言遍历 context 链,避免使用接口方法调用带来的性能开销 | |
func value(c Context, key any) any { | |
// 循环遍历 context 链直到找到值或到达链的末尾 | |
for { | |
switch ctx := c.(type) { | |
case *valueCtx: | |
// 如果是 valueCtx 类型,检查 key 是否匹配 | |
if key == ctx.key { | |
return ctx.val | |
} | |
// 继续向上游 context 查询 | |
c = ctx.Context | |
case *cancelCtx: | |
// 如果查询的是 cancelCtxKey,则返回 cancelCtx 本身 | |
if key == &cancelCtxKey { | |
return c | |
} | |
// 继续向上游 context 查询 | |
c = ctx.Context | |
case withoutCancelCtx: | |
// 如果查询的是 cancelCtxKey 且是 withoutCancelCtx 类型,返回 nil | |
// 这实现了通过 WithoutCancel 创建的 context 的 Cause (ctx) == nil 语义 | |
if key == &cancelCtxKey { | |
return nil | |
} | |
// 继续向上游 context 查询 | |
c = ctx.c | |
case *timerCtx: | |
// 如果查询的是 cancelCtxKey,则返回嵌入的 cancelCtx | |
if key == &cancelCtxKey { | |
return &ctx.cancelCtx | |
} | |
// 继续向上游 context 查询 | |
c = ctx.Context | |
case backgroundCtx, todoCtx: | |
// 到达 context 链的末端,返回 nil | |
return nil | |
default: | |
// 对于其他类型的 context,调用其 Value 方法 | |
return c.Value(key) | |
} | |
} | |
} |
查找过程示例:
// 构建 context 链 | |
ctx := context.Background() // emptyCtx | |
ctx = context.WithValue(ctx, "request-id", "abc123") // valueCtx 1 | |
ctx = context.WithValue(ctx, "user-id", 42) // valueCtx 2 | |
ctx, cancel := context.WithTimeout(ctx, 5*time.Second) // timerCtx | |
ctx = context.WithValue(ctx, "trace-id", "xyz789") // valueCtx 3 | |
// 查找 "trace-id" | |
// 路径:valueCtx (trace-id) → 找到,返回 "xyz789" | |
// 复杂度:O (1) | |
// 查找 "user-id" | |
// 路径:valueCtx (trace-id) → timerCtx → valueCtx (user-id) → 找到,返回 42 | |
// 复杂度:O (3) | |
// 查找 "request-id" | |
// 路径:valueCtx (trace-id) → timerCtx → valueCtx (user-id) → valueCtx (request-id) → 找到 | |
// 复杂度:O (4) | |
// 查找 "不存在的 key" | |
// 路径:完整遍历到 emptyCtx → 返回 nil | |
// 复杂度:O (5) |
链表结构示意图:
valueCtx(trace-id="xyz789")
↓
timerCtx(deadline=...)
↓
valueCtx(user-id=42)
↓
valueCtx(request-id="abc123")
↓
emptyCtx (Background)
# WithValue:创建值上下文
func WithValue(parent Context, key, val any) Context { | |
// 参数校验 | |
if parent == nil { | |
panic("cannot create context from nil parent") | |
} | |
if key == nil { | |
panic("nil key") | |
} | |
if !reflectlite.TypeOf(key).Comparable() { | |
panic("key is not comparable") | |
} | |
// 包装成 valueCtx | |
return &valueCtx{parent, key, val} | |
} |
key 的可比较性检查:
// ✅ 可比较类型 (OK) | |
type UserID int | |
ctx = context.WithValue(ctx, UserID(1), user) | |
type contextKey string | |
ctx = context.WithValue(ctx, contextKey("trace-id"), "123") | |
ctx = context.WithValue(ctx, "request-id", "abc") //string 可比较 | |
// ❌ 不可比较类型 (Panic) | |
ctx = context.WithValue(ctx, []string{"key"}, value) // slice | |
ctx = context.WithValue(ctx, map[string]int{}, value) // map | |
ctx = context.WithValue(ctx, func(){}, value) // func | |
// ✅ 结构体(所有字段都可比较时才可比较) | |
type RequestInfo struct { | |
ID string | |
Time time.Time | |
} | |
ctx = context.WithValue(ctx, RequestInfo{"req1", time.Now()}, data) |
为什么需要可比较性?
// Value 方法使用 == 比较 key | |
if c.key == key { | |
return c.val | |
} | |
// 如果 key 不可比较,== 操作会 panic | |
// 所以在 WithValue 时就提前检查 |
# 最佳实践:定义专用 key 类型
// ❌ 错误做法:使用字符串 | |
ctx = context.WithValue(ctx, "user-id", 123) | |
ctx = context.WithValue(ctx, "user-id", "abc") // 可能被其他包覆盖 | |
// ✅ 正确做法:定义不导出的 key 类型 | |
type contextKey string | |
const ( | |
userIDKey contextKey = "user-id" | |
traceIDKey contextKey = "trace-id" | |
) | |
// 提供类型安全的访问函数 | |
func WithUserID(ctx context.Context, userID int) context.Context { | |
return context.WithValue(ctx, userIDKey, userID) | |
} | |
func GetUserID(ctx context.Context) (int, bool) { | |
userID, ok := ctx.Value(userIDKey).(int) | |
return userID, ok | |
} | |
// 使用 | |
ctx := WithUserID(context.Background(), 123) | |
if userID, ok := GetUserID(ctx); ok { | |
fmt.Println("User ID:", userID) | |
} |
# 使用限制与反模式
# ❌ 反模式 1:存储大量数据
// 错误:valueCtx 链过长 | |
ctx := context.Background() | |
for i := 0; i < 100; i++ { | |
ctx = context.WithValue(ctx, fmt.Sprintf("key%d", i), i) | |
} | |
// 问题: | |
//- 100 层嵌套 | |
//- 查找最后一个 key 需要遍历 100 次 | |
//- 内存浪费:100 个 valueCtx 结构体 |
正确做法:
type RequestData struct { | |
Values map[string]interface{} | |
} | |
ctx = context.WithValue(ctx, requestDataKey, &RequestData{ | |
Values: map[string]interface{}{ | |
"key0": 0, | |
"key1": 1, | |
// ... | |
"key99": 99, | |
}, | |
}) | |
// 优点:只有一层 valueCtx,查找 O (1) |
# ❌ 反模式 2:存储可选参数
// 错误:把 context 当作参数包 | |
func processUser(ctx context.Context) { | |
userID := ctx.Value("userID").(int) | |
userName := ctx.Value("userName").(string) | |
userAge := ctx.Value("userAge").(int) | |
// ... | |
} | |
// 问题: | |
//- 参数不明确 | |
//- 类型不安全 | |
//- 难以追踪数据流 |
正确做法:
// 使用显式参数 | |
func processUser(ctx context.Context, userID int, userName string, userAge int) { | |
// ... | |
} | |
// 或使用结构体 | |
type User struct { | |
ID int | |
Name string | |
Age int | |
} | |
func processUser(ctx context.Context, user User) { | |
// ... | |
} |
# ❌ 反模式 3:存储函数或 channel
// 错误:存储复杂类型 | |
ctx = context.WithValue(ctx, "callback", func() { ... }) | |
ctx = context.WithValue(ctx, "channel", make(chan int)) | |
// 问题: | |
//- 违反 context 的设计初衷 | |
//- 难以测试和维护 | |
//- 生命周期管理复杂 |
正确做法:
// 使用显式参数传递 | |
func process(ctx context.Context, callback func(), ch chan int) { | |
// ... | |
} |
# ✅ 正确用法:存储请求范围的元数据
// 适合存储在 context 中的数据 | |
type contextKey string | |
const ( | |
requestIDKey contextKey = "request-id" | |
userIDKey contextKey = "user-id" | |
traceIDKey contextKey = "trace-id" | |
authTokenKey contextKey = "auth-token" | |
) | |
// HTTP 中间件 | |
func TraceMiddleware(next http.Handler) http.Handler { | |
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |
traceID := generateTraceID() | |
ctx := context.WithValue(r.Context(), traceIDKey, traceID) | |
next.ServeHTTP(w, r.WithContext(ctx)) | |
}) | |
} | |
// 业务逻辑中使用 | |
func handleRequest(ctx context.Context) { | |
traceID, _ := ctx.Value(traceIDKey).(string) | |
log.Printf("[%s] Processing request", traceID) | |
} |
# 总结与最佳实践
# Context 使用原则
-
不要存储 Context
// ❌ 错误type Server struct {
ctx context.Context // 不要存储
}// ✅ 正确type Server struct {}
func (s *Server) Serve(ctx context.Context) {
// 作为参数传递} -
Context 作为第一个参数
// ✅ 正确func DoSomething(ctx context.Context, arg1 string, arg2 int) error
// ❌ 错误func DoSomething(arg1 string, ctx context.Context, arg2 int) error
-
不要传递 nil Context
// ❌ 错误DoSomething(nil, "arg")
// ✅ 正确DoSomething(context.Background(), "arg")
// 或DoSomething(context.TODO(), "arg")
-
只传递请求范围的数据
// ✅ 适合- Request ID- Trace ID- Auth Token- User ID// ❌ 不适合- 数据库连接- 配置对象- 日志对象- 可选参数 -
总是 defer cancel ()
// ✅ 正确ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
// ❌ 错误ctx, _ := context.WithTimeout(ctx, 5*time.Second)
// 忘记 cancel,泄漏 timer
# 性能最佳实践
-
避免过深的 context 链
// ❌ 每次请求都嵌套func handleRequest(ctx context.Context) {
ctx = context.WithValue(ctx, key1, val1)
ctx = context.WithValue(ctx, key2, val2)
ctx = context.WithValue(ctx, key3, val3)
// 链过长,查找慢}// ✅ 合并数据type RequestMeta struct {
Key1, Key2, Key3 string
}ctx = context.WithValue(ctx, metaKey, &RequestMeta{...})
-
复用 Background
// ✅ 复用单例ctx := context.Background()
// ❌ 不要重复创建for i := 0; i < 1000; i++ {
ctx := context.Background() // 没必要
} -
选择合适的 Context 类型
// 场景 1:不需要取消ctx := context.Background()
// 场景 2:需要手动取消ctx, cancel := context.WithCancel(parent)
// 场景 3:需要超时ctx, cancel := context.WithTimeout(parent, 5*time.Second)
// 场景 4:需要存储数据ctx = context.WithValue(parent, key, val)
# 常见陷阱
-
忘记检查 Done
// ❌ 错误func worker(ctx context.Context) {
for {
doWork() // 永不停止
}}// ✅ 正确func worker(ctx context.Context) {
for {
select {
case <-ctx.Done():
return // 响应取消
default:
doWork()
}}} -
阻塞操作不响应取消
// ❌ 错误func process(ctx context.Context) error {
data := blockingRead() // 不响应 ctx.Done ()
return processData(data)
}// ✅ 正确func process(ctx context.Context) error {
done := make(chan []byte, 1)
go func() {
done <- blockingRead()
}()
select {
case data := <-done:
return processData(data)
case <-ctx.Done():
return ctx.Err()
}} -
context 泄漏
// ❌ 错误:goroutine 泄漏func leak(ctx context.Context) {
ctx, cancel := context.WithCancel(ctx)
// 忘记 cancel,goroutine 永不退出go func() {
<-ctx.Done()
cleanup()
}()
}// ✅ 正确func noLeak(ctx context.Context) {
ctx, cancel := context.WithCancel(ctx)
defer cancel() // 确保清理
go func() {
<-ctx.Done()
cleanup()
}()
} -
错误的错误处理
// ❌ 错误:忽略具体错误func handle(ctx context.Context) error {
if ctx.Err() != nil {
return errors.New("context error") // 丢失了具体信息
}return nil
}// ✅ 正确:保留错误信息func handle(ctx context.Context) error {
if err := ctx.Err(); err != nil {
return fmt.Errorf("context error: %w", err) // 包装原始错误
}return nil
}// ✅ 更好:区分不同错误func handle(ctx context.Context) error {
if err := ctx.Err(); err != nil {
if errors.Is(err, context.Canceled) {
// 用户主动取消return nil
}if errors.Is(err, context.DeadlineExceeded) {
// 超时return fmt.Errorf("operation timeout: %w", err)
}}return nil
}
# 实战示例
# 示例 1:HTTP 服务器优雅关闭
func runServer() error { | |
server := &http.Server{Addr: ":8080"} | |
// 创建根 context | |
ctx, cancel := context.WithCancel(context.Background()) | |
defer cancel() | |
// 监听系统信号 | |
sigChan := make(chan os.Signal, 1) | |
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM) | |
// 启动服务器 | |
go func() { | |
if err := server.ListenAndServe(); err != http.ErrServerClosed { | |
log.Printf("Server error: %v", err) | |
cancel() | |
} | |
}() | |
// 等待信号或 context 取消 | |
select { | |
case sig := <-sigChan: | |
log.Printf("Received signal: %v", sig) | |
case <-ctx.Done(): | |
log.Printf("Context canceled") | |
} | |
// 优雅关闭 | |
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 10*time.Second) | |
defer shutdownCancel() | |
if err := server.Shutdown(shutdownCtx); err != nil { | |
return fmt.Errorf("server shutdown failed: %w", err) | |
} | |
log.Println("Server stopped gracefully") | |
return nil | |
} |
# 示例 2:并发请求处理
func fetchUserData(ctx context.Context, userID int) (*UserData, error) { | |
// 设置超时 | |
ctx, cancel := context.WithTimeout(ctx, 5*time.Second) | |
defer cancel() | |
// 并发请求多个服务 | |
type result struct { | |
profile *Profile | |
orders []Order | |
err error | |
} | |
profileCh := make(chan result, 1) | |
ordersCh := make(chan result, 1) | |
// 获取用户资料 | |
go func() { | |
profile, err := fetchProfile(ctx, userID) | |
profileCh <- result{profile: profile, err: err} | |
}() | |
// 获取订单列表 | |
go func() { | |
orders, err := fetchOrders(ctx, userID) | |
ordersCh <- result{orders: orders, err: err} | |
}() | |
// 收集结果 | |
var profile *Profile | |
var orders []Order | |
var errs []error | |
for i := 0; i < 2; i++ { | |
select { | |
case res := <-profileCh: | |
if res.err != nil { | |
errs = append(errs, fmt.Errorf("fetch profile: %w", res.err)) | |
} else { | |
profile = res.profile | |
} | |
case res := <-ordersCh: | |
if res.err != nil { | |
errs = append(errs, fmt.Errorf("fetch orders: %w", res.err)) | |
} else { | |
orders = res.orders | |
} | |
case <-ctx.Done(): | |
return nil, fmt.Errorf("fetch timeout: %w", ctx.Err()) | |
} | |
} | |
if len(errs) > 0 { | |
return nil, fmt.Errorf("fetch errors: %v", errs) | |
} | |
return &UserData{ | |
Profile: profile, | |
Orders: orders, | |
}, nil | |
} |
# 示例 3:数据库事务管理
func transferMoney(ctx context.Context, db *sql.DB, from, to int, amount float64) error { | |
// 设置事务超时 | |
ctx, cancel := context.WithTimeout(ctx, 10*time.Second) | |
defer cancel() | |
// 开启事务 | |
tx, err := db.BeginTx(ctx, nil) | |
if err != nil { | |
return fmt.Errorf("begin transaction: %w", err) | |
} | |
defer tx.Rollback() // 如果没有 commit,自动回滚 | |
// 扣款 | |
_, err = tx.ExecContext(ctx, | |
"UPDATE accounts SET balance = balance - ? WHERE id = ? AND balance >= ?", | |
amount, from, amount) | |
if err != nil { | |
return fmt.Errorf("debit failed: %w", err) | |
} | |
// 加款 | |
_, err = tx.ExecContext(ctx, | |
"UPDATE accounts SET balance = balance + ? WHERE id = ?", | |
amount, to) | |
if err != nil { | |
return fmt.Errorf("credit failed: %w", err) | |
} | |
// 提交事务 | |
if err := tx.Commit(); err != nil { | |
return fmt.Errorf("commit transaction: %w", err) | |
} | |
return nil | |
} |
# 示例 4:Worker Pool 模式
type Job struct { | |
ID int | |
Data string | |
} | |
func workerPool(ctx context.Context, numWorkers int, jobs <-chan Job) { | |
var wg sync.WaitGroup | |
// 启动 worker | |
for i := 0; i < numWorkers; i++ { | |
wg.Add(1) | |
go func(workerID int) { | |
defer wg.Done() | |
worker(ctx, workerID, jobs) | |
}(i) | |
} | |
// 等待所有 worker 完成 | |
wg.Wait() | |
log.Println("All workers stopped") | |
} | |
func worker(ctx context.Context, id int, jobs <-chan Job) { | |
log.Printf("Worker %d started", id) | |
defer log.Printf("Worker %d stopped", id) | |
for { | |
select { | |
case <-ctx.Done(): | |
log.Printf("Worker %d: context canceled", id) | |
return | |
case job, ok := <-jobs: | |
if !ok { | |
log.Printf("Worker %d: channel closed", id) | |
return | |
} | |
// 处理任务 | |
if err := processJob(ctx, job); err != nil { | |
log.Printf("Worker %d: job %d failed: %v", id, job.ID, err) | |
} else { | |
log.Printf("Worker %d: job %d completed", id, job.ID) | |
} | |
} | |
} | |
} | |
func processJob(ctx context.Context, job Job) error { | |
// 设置任务超时 | |
ctx, cancel := context.WithTimeout(ctx, 30*time.Second) | |
defer cancel() | |
// 模拟处理 | |
select { | |
case <-time.After(time.Second): | |
return nil | |
case <-ctx.Done(): | |
return ctx.Err() | |
} | |
} | |
// 使用示例 | |
func main() { | |
ctx, cancel := context.WithCancel(context.Background()) | |
defer cancel() | |
jobs := make(chan Job, 100) | |
// 启动 worker pool | |
go workerPool(ctx, 5, jobs) | |
// 提交任务 | |
for i := 0; i < 20; i++ { | |
jobs <- Job{ID: i, Data: fmt.Sprintf("data-%d", i)} | |
} | |
close(jobs) | |
// 等待完成或超时 | |
time.Sleep(10 * time.Second) | |
cancel() // 取消所有 worker | |
} |
# 示例 5:请求链路追踪
type contextKey string | |
const ( | |
traceIDKey contextKey = "trace-id" | |
spanIDKey contextKey = "span-id" | |
) | |
// 中间件:注入 trace ID | |
func TraceMiddleware(next http.Handler) http.Handler { | |
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |
// 从 header 获取或生成 trace ID | |
traceID := r.Header.Get("X-Trace-ID") | |
if traceID == "" { | |
traceID = generateTraceID() | |
} | |
// 注入 context | |
ctx := context.WithValue(r.Context(), traceIDKey, traceID) | |
ctx = context.WithValue(ctx, spanIDKey, generateSpanID()) | |
// 设置响应 header | |
w.Header().Set("X-Trace-ID", traceID) | |
next.ServeHTTP(w, r.WithContext(ctx)) | |
}) | |
} | |
// 业务逻辑 | |
func handleRequest(w http.ResponseWriter, r *http.Request) { | |
ctx := r.Context() | |
// 获取 trace 信息 | |
traceID, _ := ctx.Value(traceIDKey).(string) | |
spanID, _ := ctx.Value(spanIDKey).(string) | |
log.Printf("[%s:%s] Handling request", traceID, spanID) | |
// 调用下游服务 | |
data, err := callDownstream(ctx, "https://api.example.com/data") | |
if err != nil { | |
log.Printf("[%s:%s] Downstream error: %v", traceID, spanID, err) | |
http.Error(w, "Internal error", 500) | |
return | |
} | |
json.NewEncoder(w).Encode(data) | |
} | |
// 下游调用:传递 trace 信息 | |
func callDownstream(ctx context.Context, url string) (interface{}, error) { | |
traceID, _ := ctx.Value(traceIDKey).(string) | |
newSpanID := generateSpanID() | |
req, err := http.NewRequestWithContext(ctx, "GET", url, nil) | |
if err != nil { | |
return nil, err | |
} | |
// 传递 trace 信息 | |
req.Header.Set("X-Trace-ID", traceID) | |
req.Header.Set("X-Span-ID", newSpanID) | |
req.Header.Set("X-Parent-Span-ID", ctx.Value(spanIDKey).(string)) | |
resp, err := http.DefaultClient.Do(req) | |
if err != nil { | |
return nil, err | |
} | |
defer resp.Body.Close() | |
var data interface{} | |
if err := json.NewDecoder(resp.Body).Decode(&data); err != nil { | |
return nil, err | |
} | |
return data, nil | |
} | |
func generateTraceID() string { | |
return fmt.Sprintf("%016x", time.Now().UnixNano()) | |
} | |
func generateSpanID() string { | |
return fmt.Sprintf("%08x", rand.Uint32()) | |
} |
# 参考资料
- Go Context 官方文档
- Go 1.21 Release Notes
- Context 源码
- Effective Go - Context
最后的建议:
- 理解设计哲学:Context 是用于传递取消信号和请求范围数据的,不是万能的参数传递工具
- 遵循最佳实践:作为第一个参数,不存储,总是 defer cancel
- 注意性能影响:避免过深的嵌套,合理使用 WithValue
- 响应取消信号:在长时间运行的操作中定期检查 ctx.Done ()
- 保持简单:不要过度使用 Context,显式参数往往更清晰
Context 是 Go 并发编程的基石,正确使用它能让你的程序更加健壮和高效!