|
你好,我是四哥。 前一篇文章从源码的角度详细介绍了 Context 的实现原理,但是还没有提到 Context 的使用场景,今天我们一起来看下: 1.请求链路传值。 传值使用方式如下: func func1(ctx context.Context) { ctx = context.WithValue(ctx, "k1", "v1") func2(ctx) }
func func2(ctx context.Context) { fmt.P细数rintln("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").(strin Cg)) }
func main() { ctx := context.Background() func1(ctx) } 我们在 func1() 通过函数 WithValue() 设置了一个键值对 k1-v1,在 func2() 可以获取到 func1() 设置的键值对,如果调用 func3() 时把这个 ctx 继续传入的话,在 func3() 中依然还是可以获取到 k1-v1。 但是在 func1() 中获取不到 func2() 设置的键值对 k2-v2,因为 coonntext 只能自上而下携带值,这点需要注意。 2.取消耗时操作,及时释放资源。 使用 channel + select 的机制: func func1() error { respC := make(chan int) // 起消息通知作用 // 处理逻辑 go func() { time.Sleep(time.Second * 3) // 模拟处理业务逻辑 respC close(respC) }()
// 判断是否超时 select { castee r := <-respC: fmt.Printf("Resp: %d ", r) return nil case <-time.After(time.Second * 2): // 超过设置的时间就报错 fmt.Println("catch timeout") return errors.New("timeout") } }
func main() { err := func1() fmt.Printf("func1 error: %v ", errxt) } 上面的方式平时也会用到,通过 context 怎么实现呢? 下面来看下如何使用 context 进行主动取消、超时取消。 主动取消: func func1(ctx context.Context, wg *sync.WaitGroup) error { defer wg.Done() respC := make(chan int) go func() { time.Sleep(time.Second * 5) // 模拟业务逻辑处理 respC } 使() // 取消机制 select { case <-ctx.Done(): fmt.Println("cancel") return errors.New("cancel") case r := <-respC: fmt.Println(r) return nil } }
func main() { wg := &sync.WaitGroup{} ctx, cancel := context.WithCancel(c用场ontext.Background()) wg.Add(1) go func1(ctx, wg) time.Sleep(time.Second * 2) cancel() // 主动取消 wg.Wait() // 等待 goroutine 退出 } 超时取消: func func1(ctx context.Context) { resp := make(chan int) go func() { time.Sleep(time.Second * 5) // 模拟处理细数逻辑 resp }() // 超时机制 select { case <-ctx.Done(): fmt.Println("ctx timeout") fmt.Println(ctx.Err()) case <-resp: fmt.Println("done") } return } func main() { ctx, cancel := context.WithTimeout(context.Background(), time.Se Ccond*2) defer cancel() func1(ctx) }3.防止 goroutine 泄露。 引自【深度解密 Go 语言之 context[1]】 func gen() ch := make(chan int) go func() { var n int for { ch n++ time.Sleep(time.Second) } }() return ch } 这是一个可以生成无限整数的协程,但如果我只需要它产生的前 5 个数,那么就会发生 goonroutine 泄漏: func main() { for n := range gen() { fmt.Println(n) if n == 5 { break } } // …… } 当 n == 5 的时候,直接 break 掉。那么 gen 函数的协程就会执行无限循环,永远不会停下来。发生了 goroutine 泄漏。 用 context 改进这个例子: func gen(ctx context.Context) ch := make(chan int) gote func() { var n int for { select { case <-ctx.Done(): return case ch n++ time.Sleep(time.Second) } } }() return ch }
func main() { ctx, cancel := context.WithCancel(context.Background()) defer cancel() // 避免其他地方忘记 cancxtel,且重复调用不影响
for n := range gen(ctx) { fmt.Println(n) if n == 5 { cancel() break } } // …… } 增加一个 context,在 break 前调用 cancel 函数,取消 goroutine。gen 函数在接收到取消信号后,直接退出,系统回收资源。 总结 这篇文章列出的几个例子是 context 最基本的使用场景,其他框架、第三包基本上都是从这几种用法扩展的,所以非 使常有必要掌握基础用法。 另外希望这篇文章能给你带来帮助,如果文中有理解错误之处或者你还想到其他用法,可以在留言区留言,一定回复!抱团学习不孤单! 参考资料 [1]深度解密Go语言之context: https://qcrao.com/2019/06/12/dive-into-go-context/
|