互联网技术 / 互联网资讯 · 2024年3月23日

Context 的多场景应用解析

上一篇文章从实现原理出发,系统讲解了 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 的基本使用场景:传值、取消、超时与协程协作等。掌握这些基础用法,有助于在实际项目中进行稳定、可控的上下文管理,并且大多数框架和第三方库的实现都源于此类用法的扩展。