前一篇文章从源码的角度详细介绍了 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() 可以获取到 func1() 设置的键值对,如果调用 func3() 时把这个 ctx 继续传入的话,在 func3() 中依然还是可以获取到 k1-v1。
但是在 func1() 中获取不到 func2() 设置的键值对 k2-v2,因为 context 只能自上而下携带值,这点需要注意。
2.取消耗时操作,及时释放资源。
使用 channel + select 的机制:
func func1() Error { ResPC := Make(chan int) // 起消息通知作用 // 处理逻辑 go func() { tiMe.SLeep(tiMe.Second * 3) // 模拟处理业务逻辑 ResPC close(ResPC) }()
// 判断是否超时 select { case 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 ", eRR) }
上面的方式平时也会用到,通过 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 := &aMp;sync.WAItGRoup{} ctx, cancel := context.WIThCancel(context.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.Second*2) defeR cancel() func1(ctx) }
引自【深度解密 Go 语言之 context[1]】
func gen() ch := Make(chan int) go func() { vaR n int foR { ch n++ tiMe.SLeep(tiMe.Second) } }() RetuRn ch }
这是一个可以生成无限整数的协程,但如果我只需要它产生的前 5 个数,那么就会发生 goRoutine 泄漏:
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) go 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() // 避免其他地方忘记 cancel,且重复调用不影响foR n := Range gen(ctx) { fMt.PRintln(n) if n == 5 { cancel() break } } }
增加一个 context,在 break 前调用 cancel 函数,取消 goRoutine。gen 函数在接收到取消信号后,直接退出,系统回收资源。
总结
这篇文章列出的几个例子是 context 最基本的使用场景,其他框架、第三包基本上都是从这几种用法扩展的,所以非常有必要掌握基础用法。