Context(上下文) 是 Go 语言标准库中的一个核心工具,主要用于:
控制 Goroutine 的生命周期(取消、超时)
在 Goroutine 之间传递请求级别的元数据(如请求ID、用户信息等)
Context 到底怎么用?
想象一个场景:
你(主 goroutine)安排 3 个同事(子 goroutine)一起做一个"整理文件"的任务,要求:
- 如果任务中途不需要做了(比如老板说不整理了),你要能通知所有同事停下,别白干活;
- 如果整理时间超过 5 分钟,不管有没有做完,都必须停下;
- 你还想告诉同事们"整理的是 2026 年的文件"这个关键信息。
Context 就是干这个的:它是 goroutine 之间的"通信员",负责传递"取消信号""超时时间""共享数据",让多个 goroutine 协同工作、优雅退出。
核心:Context 的 4 个基础用法
Context 本身是一个接口,但我们不用自己实现,Go 提供了 4 个最常用的"创建函数",只要掌握这 4 个就够了:
context.Background():根 Context,所有 Context 的"祖宗",无超时、不可取消、无数据;context.WithCancel(parent):基于父 Context 创建"可手动取消"的 Context;context.WithTimeout(parent, 超时时间):基于父 Context 创建"超时自动取消"的 Context;context.WithValue(parent, key, value):基于父 Context 创建"带共享数据"的 Context。
实例 1:手动取消 Context(通知 goroutine 停止工作)
需求:主 goroutine 启动 2 个子 goroutine 模拟"干活",3 秒后手动通知它们停止。
go
package main
import (
"context"
"fmt"
"time"
)
// 子goroutine干活的函数
func work(ctx context.Context, name string) {
// 循环干活,直到收到取消信号
for {
select {
// 监听Context的取消信号(ctx.Done()通道关闭就触发)
case <-ctx.Done():
fmt.Printf("【%s】收到停止信号,停止干活!\n", name)
return // 优雅退出
default:
// 没收到信号,继续干活
fmt.Printf("【%s】正在干活...\n", name)
time.Sleep(500 * time.Millisecond) // 干0.5秒歇一下
}
}
}
func main() {
// 1. 创建根Context(background是所有Context的基础)
rootCtx := context.Background()
// 2. 创建可手动取消的Context,返回:子Context + 取消函数
ctx, cancel := context.WithCancel(rootCtx)
defer cancel() // 好习惯:确保函数结束时取消,避免泄露
// 3. 启动2个子goroutine干活
go work(ctx, "同事1")
go work(ctx, "同事2")
// 4. 主goroutine等3秒,然后通知子goroutine停止
time.Sleep(3 * time.Second)
fmt.Println("主goroutine:老板说不用干了,通知所有人停下!")
cancel() // 手动触发取消
// 等子goroutine退出(可忽略,只是为了看输出)
time.Sleep(1 * time.Second)
fmt.Println("所有任务结束")
}
运行结果:
【同事1】正在干活...
【同事2】正在干活...
...(循环3秒)
主goroutine:老板说不用干了,通知所有人停下!
【同事1】收到停止信号,停止干活!
【同事2】收到停止信号,停止干活!
所有任务结束
关键理解:
cancel()是"开关":调用它,ctx.Done()通道就会关闭,子 goroutine 能监听到;select + <-ctx.Done()是"监听开关":子 goroutine 靠这个知道要不要停;defer cancel():防止主 goroutine 提前退出,导致 Context 没取消,子 goroutine 一直跑(goroutine 泄露)。
实例 2:超时自动取消 Context(不用手动关,到点就停)
需求:子 goroutine 干活,但最多干 2 秒,超时自动停止(比如 DNS 解析超时)。
go
package main
import (
"context"
"fmt"
"time"
)
func workWithTimeout(ctx context.Context) {
for {
select {
case <-ctx.Done():
// ctx.Err() 能看取消原因:超时/手动取消
fmt.Printf("干活停止,原因:%v\n", ctx.Err())
return
default:
fmt.Println("正在干活...")
time.Sleep(500 * time.Millisecond)
}
}
}
func main() {
// 1. 创建带超时的Context:2秒后自动取消
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel() // 即使超时了,cancel()也能清理资源,好习惯
// 2. 启动子goroutine
go workWithTimeout(ctx)
// 3. 主goroutine等3秒,看结果
time.Sleep(3 * time.Second)
fmt.Println("主goroutine:任务结束")
}
运行结果:
正在干活...
正在干活...
正在干活...
正在干活...
干活停止,原因:context deadline exceeded
主goroutine:任务结束
关键理解:
WithTimeout比WithCancel多了"自动触发":到时间不用手动调cancel(),Context 会自己关闭Done()通道;ctx.Err()很有用:返回context.DeadlineExceeded表示超时,返回context.Canceled表示手动取消。
实例 3:带共享数据的 Context(传递关键信息)
需求:主 goroutine 给子 goroutine 传递"任务ID""用户ID"这类轻量数据(注意:只能传轻量数据,不能传大对象)。
go
package main
import (
"context"
"fmt"
"time"
)
// 定义key(推荐用自定义类型,避免和其他包的key冲突)
type ctxKey string
func workWithValue(ctx context.Context) {
// 从Context中取值:key要和存的时候一致
taskID := ctx.Value(ctxKey("taskID")).(string) // 类型断言(记得加.(类型))
userID := ctx.Value(ctxKey("userID")).(int)
fmt.Printf("收到任务信息:taskID=%s, userID=%d,开始干活...\n", taskID, userID)
time.Sleep(1 * time.Second)
fmt.Println("干活完成!")
}
func main() {
// 1. 根Context
rootCtx := context.Background()
// 2. 第一层:加taskID
ctxWithTask := context.WithValue(rootCtx, ctxKey("taskID"), "task_001")
// 3. 第二层:再加userID(Context是链式的,可多层叠加)
ctxWithUser := context.WithValue(ctxWithTask, ctxKey("userID"), 10086)
// 4. 启动子goroutine,传递带数据的Context
go workWithValue(ctxWithUser)
// 等子goroutine完成
time.Sleep(2 * time.Second)
fmt.Println("主goroutine:任务结束")
}
运行结果:
收到任务信息:taskID=task_001, userID=10086,开始干活...
干活完成!
主goroutine:任务结束
关键理解:
WithValue是"链式叠加":可以基于一个 Context 不断加新数据,子 Context 能拿到所有父 Context 的数据;- 取数据要做类型断言 :
ctx.Value(key).(类型),因为Value()返回的是interface{}; - 不要传大数据/频繁修改的数据:Context 设计目的是传递"请求级"的轻量元数据(比如请求ID、用户ID),不是用来做数据共享的"容器"。
实例 4:综合案例(结合取消 + 超时 + 传数据)
模拟之前的 DNS 解析场景:启动子 goroutine 解析 DNS,传递"域名"信息,设置 3 秒超时,若手动取消则立即停止。
go
package main
import (
"context"
"fmt"
"time"
)
// 模拟DNS解析
func resolveDNS(ctx context.Context) {
// 取传递的域名
domain := ctx.Value(ctxKey("domain")).(string)
fmt.Printf("开始解析域名:%s\n", domain)
for {
select {
case <-ctx.Done():
fmt.Printf("解析停止,原因:%v\n", ctx.Err())
return
default:
fmt.Println("解析中...")
time.Sleep(500 * time.Millisecond)
}
}
}
type ctxKey string
func main() {
// 1. 根Context + 传域名 + 3秒超时
rootCtx := context.Background()
ctxWithDomain := context.WithValue(rootCtx, ctxKey("domain"), "dnspod.mymei.tv")
ctx, cancel := context.WithTimeout(ctxWithDomain, 3*time.Second)
defer cancel()
// 2. 启动解析goroutine
go resolveDNS(ctx)
// 3. 模拟2秒后手动取消(注释这行就是超时取消)
// time.Sleep(2 * time.Second)
// fmt.Println("手动取消解析!")
// cancel()
// 等结果
time.Sleep(4 * time.Second)
fmt.Println("主程序结束")
}
运行结果(不手动取消,超时):
开始解析域名:dnspod.mymei.tv
解析中...
解析中...
解析中...
解析中...
解析中...
解析中...
解析停止,原因:context deadline exceeded
主程序结束
运行结果(手动取消):
开始解析域名:dnspod.mymei.tv
解析中...
解析中...
解析中...
手动取消解析!
解析停止,原因:context canceled
主程序结束
总结(3 个关键点)
- 核心作用:Context 是 goroutine 之间的"通信员",主要用来传递「取消信号」「超时时间」「轻量共享数据」;
- 3 个常用创建方式 :
WithCancel:手动取消,适合"主动叫停任务";WithTimeout:超时自动取消,适合"有时间限制的任务"(如网络请求、DNS解析);WithValue:传递轻量元数据,适合"请求级数据传递"(如请求ID、用户ID);
- 核心用法 :子 goroutine 用
select <-ctx.Done()监听取消信号,收到后优雅退出,避免 goroutine 泄露。