上一篇文章从实现原理出发,系统讲解了 context 的源码机制,本文聚焦实际应用场景,帮助理解 context 在不同情境下的使用方式。
1. 请求链路传值
通过上下文传值可以在调用链中逐层传递键值对:
示例:
func func1(ctx context.context) {
ctx = context.WithValue(ctx, “k1”, “v1”)
func2(ctx)
}
func func2(ctx context.context) {
fmt.Println(“func2:”, ctx.Value(“k1”).(string))
ctx = context.WithValue(ctx, “k2”, “v2”)
func3(ctx)
}
func func3(ctx context.context) {
fmt.Println(“func3:”, ctx.Value(“k1”).(string))
fmt.Println(“func3:”, ctx.Value(“k2”).(string))
}
func main() {
ctx := context.Background()
func1(ctx)
}
注意:在 func1 通过 WithValue 设置的键值对 k1-v1 可以在 func2、func3 中获取到,但只有自上而下传递的值会一直沿用,后续在同一调用链中对 ctx 的修改不会改变之前的值。
2. 取消耗时操作,及时释放资源
在处理耗时任务时,常结合 channel 与 select 实现超时控制:
示例:超时检测
func func1() error {
resCh := make(chan int) // 结果通知信道
go func() {
time.Sleep(time.Second * 3) // 模拟耗时任务
close(resCh)
}()
select {
case r := <-resCh:
fmt.Printf("Resp: %d\n", r)
return nil
case <-time.After(time.Second * 2):
fmt.Println("timeout")
return errors.New("timeout")
}
}func main() {
err := func1()
fmt.Println("func1 Error:", err)
}
以上是常见做法,实际也可以结合 context 实现主动取消与超时控制。
主动取消
通过 context.WithCancel 结合 WaitGroup 实现取消通知:
示例:
func func1(ctx context.context, wg *sync.WaitGroup) error {
defer wg.Done()
resCh := make(chan int)
go func() {
time.Sleep(time.Second * 5) // 模拟处理
resCh <- 1
}()select {
case <-ctx.Done():
fmt.Println("cancel")
return errors.New("cancel")
case r := <-resCh:
fmt.Println(r)
return nil
}
}
主调代码示例:
func main() {
wg := &sync.WaitGroup{}
ctx, cancel := context.WithCancel(context.Background())
wg.Add(1)
go func() { _ = func1(ctx, wg) }()
time.Sleep(time.Second * 2)
cancel() // 主动取消
wg.Wait()
}
超时取消
通过 context.WithTimeout 实现超时控制:
示例:
func func1(ctx context.context) {
respCh := make(chan int)
go func() {
time.Sleep(time.Second * 5) // 模拟处理
respCh <- 1
}()select {
case <-ctx.Done():
fmt.Println("ctx timeout")
fmt.Println(ctx.Err())
case <-respCh:
fmt.Println("done")
}
}
主调代码:
func main() {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*2)
defer cancel()
func1(ctx)
}
延伸示例
引自相关资料中的示例:
func gen() chan int {
ch := make(chan int)
go func() {
n := 0
for {
ch <- n
n++
time.Sleep(time.Second)
}
}()
return ch
}
这是一个不断产生整数的协程,如果只需要前 5 个数,超出部分应停止以避免协程泄漏。
示例:循环读取前 5 个数并在达到退出条件时结束,阻止生成端继续运行。
func main() {
for n := range gen() {
fmt.Println(n)
if n == 5 {
break
}
}
}
为了避免协程泄漏,使用 context 改进:
示例:通过 context 的取消信号来优雅退出生成协程
func gen(ctx context.context) chan int {
ch := make(chan int)
go func() {
n := 0
for {
select {
case <-ctx.Done():
return
default:
ch <- n
n++
time.Sleep(time.Second)
}
}
}()
return ch
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel() // 避免忘记调用 cancel,重复调用也无妨
for n := range gen(ctx) {
fmt.Println(n)
if n == 5 {
cancel()
break
}
}
}
总结
上述示例覆盖了 context 的基本使用场景:传值、取消、超时与协程协作等。掌握这些基础用法,有助于在实际项目中进行稳定、可控的上下文管理,并且大多数框架和第三方库的实现都源于此类用法的扩展。
