🧠 前言:你的 Goroutine 为什么失控了?
你是否遇到过这样的窘境:
- 用户已经取消了请求,后台的 goroutine 却还在「无脑蹦迪」;
- 微服务调用链中某个服务超时了,但整个调用流程还在傻等;
- 每个函数都得手动传递
request_id
,搞得代码臃肿又难维护?
这些问题的根源,其实都指向一个关键点:缺乏统一的并发控制机制。
Go 官方早在 1.7 就推出了 context
包,用于优雅管理并发任务的生命周期,是并发编程中不可或缺的「指挥官」。
💡 为什么需要 context
?
在现代 Go 项目中,一个 HTTP 请求常常会衍生出多个 goroutine,这些 goroutine 会:
- 调数据库
- 调用 RPC
- 请求第三方 API
- 处理业务逻辑计算
问题就来了:
1. 如何统一取消这些任务?
比如,用户关闭了浏览器,但服务器的 goroutine 还在跑,浪费资源。
2. 如何实现统一的超时控制?
我们希望整个链路最多耗时 2 秒,超时后整个任务链要停下来。
3. 如何优雅传递上下文信息?
如 request_id
,若每个函数都加参数,太冗余。
所以 Go 引入 context
,作为并发控制、数据传递的核心解决方案,简洁而强大。
🔍 context 是什么?
context.Context
是一个接口,定义了以下方法:
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key any) any
}
各个方法解释如下:
Done()
返回一个只读 channel,当任务被取消或超时后,该 channel 会关闭,用于通知 goroutine 停止执行。Err()
在Done()
被触发后返回错误类型,可能是:context.Canceled
:被取消context.DeadlineExceeded
:超时
Deadline()
返回截止时间及是否设置。Value(key)
获取上下文中存储的键值对,常用于传递如request_id
等信息。
🌳 context 的派生方式
Go 提供了几个派生 context 的函数:
函数 | 说明 |
---|---|
context.Background() | 永不会被取消的根 context |
context.TODO() | 占位用,当你还没决定传什么 context |
context.WithCancel(parent) | 可手动取消的子 context |
context.WithTimeout(parent, duration) | 设定超时自动取消 |
context.WithValue(parent, key, val) | 存储键值对,用于传递上下文数据 |
派生结构树示意图:
Background()
├── WithCancel → ctxA
│ ├── WithTimeout → ctxC
│ └── ctxD
└── WithValue → ctxB
└── WithCancel → ctxE
当 ctxA 被取消时,ctxC 和 ctxD 也会被级联取消。
🛠️ 实战案例精选
示例 1:主动取消任务(WithCancel)
ctx, cancel := context.WithCancel(context.Background())
go monitor(ctx)
time.Sleep(5 * time.Second)
cancel() // 发送取消信号
监听任务内部用 ctx.Done()
检查是否取消:
select {
case <-ctx.Done():
fmt.Println("被取消:", ctx.Err())
return
default:
// 继续执行
}
示例 2:设置超时时间(WithTimeout)
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
select {
case <-time.After(5 * time.Second):
fmt.Println("任务完成")
case <-ctx.Done():
fmt.Println("任务超时:", ctx.Err())
}
示例 3:传递 trace_id(WithValue)
type traceKey string
ctx := context.WithValue(context.Background(), traceKey("trace_id"), "abc-123")
id := ctx.Value(traceKey("trace_id")).(string)
fmt.Println("当前请求 trace_id:", id)
建议:自定义类型做 key,避免键名冲突。
示例 4:HTTP 请求上下文取消
Go 的 http.Request.Context()
会在客户端断开连接时自动取消。
func handler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
select {
case <-time.After(10 * time.Second):
fmt.Fprintln(w, "处理完成")
case <-ctx.Done():
http.Error(w, "请求取消", http.StatusRequestTimeout)
}
}
客户端一旦中断连接,后台任务自动结束。
📏 context 使用的 5 条最佳实践
把 context 作为第一个参数传入函数,并命名为
ctx
func DoSomething(ctx context.Context, arg string) error
不要将 context 保存在结构体中
显式传递生命周期更清晰。WithValue 用于请求范围内的值,不是参数传递工具
取消是“建议性”的
下游任务需主动监听ctx.Done()
,否则不会中止。永远不要传 nil context
如果不确定用什么,至少用context.TODO()
。
🥚 彩蛋:合并多个 context
Go 原生没有 context.Or()
,但我们可以手动实现。
func MergeContexts(ctxs ...context.Context) (context.Context, context.CancelFunc) {
ctx, cancel := context.WithCancel(context.Background())
go func() {
defer cancel()
cases := make([]reflect.SelectCase, len(ctxs))
for i, c := range ctxs {
cases[i] = reflect.SelectCase{
Dir: reflect.SelectRecv,
Chan: reflect.ValueOf(c.Done()),
}
}
reflect.Select(cases)
}()
return ctx, cancel
}
这个技巧在多个取消源场景(如多通道监听)非常实用。
✅ 总结
context
是 Go 并发控制的核心机制;- 它解决取消、超时、传参等场景;
- 掌握 Done、Err、WithCancel、WithTimeout、WithValue 是基本功;
- 编写高质量的 Go 程序,必须熟练掌握 context。
📌 附录:官方推荐文档