从使用角度浅谈 Go context
从使用角度浅谈 Go context
〇、前言
这是《让我们一起Golang》专栏的第47篇文章,本文从使用角度浅谈Go的context包,不过由于笔者水平和工作经验限制,可能文章存在许多不足或错误,烦请指出斧正!
本专栏的其他文章:
一、context的作用是什么?
context译为“上下文”,主要在协程Goroutine之间传递上下文信息,包括取消信号、超时时间、截止时间、键值对等等。
比如我们用Go语言搭建一个HTTP server,如果有一个请求发送到server,它就会启动一个或多个Goroutine去工作,如果响应请求时调用的某个服务响应速度很慢,就会导致请求这个服务的Goroutine越来越多,导致内存占用暴涨,Go调度器和垃圾回收器压力很大,就会导致发送到server的请求得不到响应。
而context包的作用就是解决这个问题。
在Go里面,没有context包前,我们一般使用channel和select来控制协程的关闭,但是当多个协程之间互相关联,有共享的数据时,使用channel和select就会比较麻烦,此时我们就需要用到context包。
二、context的使用:传递共享数据
package main
import (
"context"
"fmt"
)
func main() {
//上下文默认值,所有其他的上下文都从他衍生。通常用于main函数、初始化、测试或者顶级上下文
ctx := context.Background()
s, ok := ctx.Value("name").(string)
if !ok {
fmt.Println("nil")
} else {
fmt.Println(s)
}
//基于某个 context 创建并存储对应的上下文信息。
ctx = context.WithValue(ctx, "name", "ReganYue")
s, _ = ctx.Value("name").(string)
fmt.Println(s)
}
context.Background()是上下文默认值,所有其他的上下文都从他衍生。通常用于main函数、初始化、测试或者顶级上下文。context.WithValue()是基于某个 context 创建并存储对应的上下文信息。
下面是运行结果:
第一次取上下文中的“name”时,因为ctx是一个空的context,因此取不出来,ok为false,因此我们输出的是nil,后面使用context.WithValue()创建并存储了上下文信息,因此第二次取时,能够取到“name”中的数据ReganYue
。
二、context的使用:定时取消
当某个服务因为业务负载过重、网络延迟高的情况导致请求阻塞时,需要用到context包中的WithTimeout(基于父级 context,创建一个具有超时时间(Timeout)的新 context)。
package main
import (
"context"
"fmt"
"time"
)
func main() {
timeout, cancelFunc := context.WithTimeout(context.Background(), 1*time.Second)
defer cancelFunc()
select {
case <-time.After(2 * time.Second):
fmt.Println("Hello")
case <-timeout.Done():
fmt.Println("finish finish")
}
}
如果是这样的话,会输出:
finish finish
因为已经context.WithTimeout设置的定时是1秒钟,1秒钟过后,timeout就会Done了,所以输出finish finish,然后再调用cancelFunc()。如果我们将context.WithTimeout设置的定时大于2秒钟,比如5秒钟,就会输出:
Hello
三、context的使用:避免Goroutine泄露
上述使用案例中,若不使用context,Goroutine仍然会执行完毕,但是某些场景下,若不用context取消,会导致goroutine泄露。
看下面这个例子:
package main
import (
"fmt"
"time"
)
func gen() <-chan int {
ch := make(chan int)
go func() {
var n int
for {
ch <- n
n++
time.Sleep(1 * time.Second)
}
}()
return ch
}
func main() {
for n := range gen() {
fmt.Println(n)
if n == 5 {
break
}
}
fmt.Println("....")
}
在该例子中,当取整数5后,我们直接break,这时候往管道ch发送数字的协程Goroutine就会被阻塞,我们常称之为Goroutine泄露。
如何用context解决这个问题呢?我们看看下面:
package main
import (
"context"
"fmt"
"time"
)
func gen(ctx context.Context) <-chan int {
ch := make(chan int)
go func() {
var n int
for {
select {
case ch <- n:
n++
time.Sleep(1 * time.Second)
case <-ctx.Done():
return
}
}
}()
return ch
}
func main() {
ctx, cancelFunc := context.WithCancel(context.Background())
defer cancelFunc()
for n := range gen(ctx) {
fmt.Println(n)
if n == 5 {
cancelFunc()
break
}
}
fmt.Println("....")
}
在主协程中调用break前,我们调用cancelFunc,让执行gen函数的协程执行return,让GC回收资源。
参考文献:
深度解密Go语言之context - 知乎 zhuanlan.zhihu.com/p/68792989
Context should go away for Go 2 — faiface blog faiface.github.io/post/contex…
Go程序员面试笔试宝典 - 机械工业出版社
转载自:https://juejin.cn/post/7173094636330680327