可以把 context 理解成 Go 语言里的“随身文件夹”。
一次业务请求开始时(例如一条 HTTP 请求、一次 CLI 命令),你会得到一个空文件夹;后续所有函数、协程如果都把这个文件夹传下去,就能:
- 给文件夹贴一张“到点就回收”或“随时可能回收”的标签——超时 / 取消信号。
- 在文件夹里夹几张小纸条——跨层共享的少量数据(比如当前登录用户、Trace-ID)。
这样,即使代码分散在很多包、起了很多 goroutine,只要拿着同一份文件夹,就能做到“同步撤退”和“统一携带元数据”。
────────────────────────────────────────
一、它究竟是什么?
context.Context 是一个接口,核心只有 4 个方法:
• Deadline() —— 这份“文件夹”什么时候到期?
• Done() —— 返回一个 channel,一旦被关闭表示“该收工了”。
• Err() —— 收工时告诉你原因:取消、超时还是没事。
• Value() —— 取“小纸条”:按 key 取出跨层共享的小量数据。
真正承载数据的是几个由它派生的结构体:
• context.WithCancel(parent) – 加“手动撤退”按钮
• context.WithTimeout(parent, duration) – 加“n 秒后自动撤退”标签
• context.WithValue(parent, key, val) – 塞纸条
它们之间串成一棵树:子 context 会把父 context 的信号、数据都继承下来。
────────────────────────────────────────
二、有什么用?
-
统一取消
HTTP 请求被客户端掐掉、系统关机、任务到点超时……只要调用 cancel() 或超时触发,所有拿着同一份 context 的数据库查询、Redis 调用、子 goroutine 都能立刻感知并提前返回,避免做无用功或资源泄漏。 -
超时控制
给一次外部调用(比如第三方接口)包一层WithTimeout(ctx, 2*time.Second)
,2 秒还没回就自动退出,防止卡死。 -
链路元数据传递
Trace-ID、登录用户、Locale 等零星信息不再用一长串函数形参传来传去,统一塞进 context,用到的人自己取。 -
多 goroutine 协作
主协程生成 ctx ,起 10 个 worker goroutine 干活;主协程 cancel 时,worker 们读到<-ctx.Done()
立刻优雅退出。
────────────────────────────────────────
三、为什么要用它,而不是全局变量或 channel?
• 作用域清晰:context 随参数显式传递,离开请求生命周期就没人再拿得到,避免全局变量的脏读写。
• 树状取消:一个 cancel 信号可级联到所有子 task,比为每个 goroutine 单独开 channel 简单得多。
• 与标准库 / 第三方库对接:http.Request
、sql.DB
、redis.Client
、grpc
等几乎都接受 ctx,想取消就传进同一个 ctx 即可。
• 少量安全数据:Value()
只放只读、小尺寸数据,性能开销可忽略。
────────────────────────────────────────
四、一个小例子
func main() {// 1) 创建根 context,3 秒后自动取消ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)defer cancel()// 2) 起一个子 goroutine,模拟耗时任务go func() {// 派生自己的子 ctx,附带 TraceIDctx2 := context.WithValue(ctx, "TraceID", "abc-123")doSomething(ctx2)}()// 3) 主协程等待任务,也监听 ctx.Done()select {case <-ctx.Done(): // 只要 3 秒到或手动 cancel,就会走这里log.Println("main: timeout or cancelled:", ctx.Err())}
}func doSomething(ctx context.Context) {for {select {case <-ctx.Done(): // 收到“收工”信号log.Println("worker: stop quickly:", ctx.Err(),"trace =", ctx.Value("TraceID"))returndefault:time.Sleep(500 * time.Millisecond)log.Println("worker: working... trace =", ctx.Value("TraceID"))}}
}
运行效果(截断):
worker: working... trace = abc-123
worker: working... trace = abc-123
worker: stop quickly: context deadline exceeded trace = abc-123
main: timeout or cancelled: context deadline exceeded
当 3 秒超时后,ctx.Done()
被关闭,doSomething
立刻退出,主协程也感知到并结束。
────────────────────────────────────────
五、小结(一句话)
context 就是一份在调用链上可传递的“控制+元数据”包:
• 控制:超时 / 取消信号让所有协程同步撤退;
• 元数据:少量键值方便跨层共享。
它让 Go 程序在并发、网络场景下既高效又干净可控。
举例子
想像你和几位朋友(多个 goroutine)一起去游乐园 (一次业务请求)。
大家约定:只要领队(主 goroutine)把手里的“召集令”旗子收起来,就立刻停止当前项目集合回去;同时,这面旗子上还贴着一张便签,写着今天的团编号 (Trace-ID)。
在 Go 中,这面“召集令”旗子就是 context:
- 旗子竖着 → 还在玩,随时可读取团编号等信息。
- 旗子收起 → 收到统一信号,大家马上停止当前活动,不管是在排队还是在游戏里。
下面用 30 行代码演示这一过程。
package mainimport ("context""log""time"
)func main() {// 1. 领队拿一面旗子,10 秒后强制集合ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)// 在旗子上贴便签:团号ctx = context.WithValue(ctx, "groupID", "G-20240618")defer cancel() // 程序结束前记得收旗// 2. 派 3 个朋友去不同项目玩for i := 1; i <= 3; i++ {go play(ctx, i)}// 3. 领队 6 秒后临时决定提前走,收旗time.Sleep(6 * time.Second)cancel() // 相当于把旗子收起来// 等一会儿,看大家是否都回来time.Sleep(2 * time.Second)log.Println("全部集合完毕,准备离园")
}func play(ctx context.Context, id int) {for {select {case <-ctx.Done(): // 看到旗子被收,马上结束log.Printf("游客 %d 收到集合信号,原因:%v,团号:%s\n",id, ctx.Err(), ctx.Value("groupID"))returndefault:log.Printf("游客 %d 正在玩项目,团号:%s\n",id, ctx.Value("groupID"))time.Sleep(1 * time.Second) // 模拟玩项目}}
}
运行结果(精简):
游客 1 正在玩项目,团号:G-20240618
游客 2 正在玩项目,团号:G-20240618
游客 3 正在玩项目,团号:G-20240618
...(每秒输出一次)...
游客 2 收到集合信号,原因:context canceled,团号:G-20240618
游客 1 收到集合信号,原因:context canceled,团号:G-20240618
游客 3 收到集合信号,原因:context canceled,团号:G-20240618
全部集合完毕,准备离园
解析:
• context.WithTimeout
:给旗子加了“10 秒后必须集合”的规定。
• cancel()
:6 秒时领队提前收旗,所有 goroutine 看到 <-ctx.Done()
立即停下。
• ctx.Value("groupID")
:任何人随时都能读到团编号,而不用层层传参数。
这就是 context 的两大核心:
- 统一取消(旗子收起所有人同步结束);
- 携带小量共享数据(旗子上贴的便签)。