什么是context
从go1.7开始,golang.org/x/net/context包正式作为context包进入了标准库。那么,这个包到底是做什么的呢?根据官方的文档说明:
Package context defines the Context type, which carries deadlines, cancelation signals, and other request-scoped values across API boundaries and between processes.
也就是说,通过context,我们可以方便地对同一个请求所产生地goroutine进行约束管理,可以设定超时、deadline,甚至是取消这个请求相关的所有goroutine。形象地说,假如一个请求过来,需要A去做事情,而A让B去做一些事情,B让C去做一些事情,A、B、C是三个有关联的goroutine,那么问题来了:假如在A、B、C还在处理事情的时候请求被取消了,那么该如何优雅地同时关闭goroutine A、B、C呢?这个时候就轮到context包上场了。
称之为上下文非常贴切,它就是goroutine的上下文。
Context控制多个goroutine
使用Context控制一个goroutine的例子如上,非常简单,下面我们看看控制多个goroutine的例子,其实也比较简单。
funcmain()
{
ctx
,cancel
:=context
.WithCancel(context
.Background())
go
watch(
ctx
,"【监控1】")
go
watch(
ctx
,"【监控2】")
go
watch(
ctx
,"【监控3】")
time
.Sleep(10*
time
.Second
)
fmt
.Println("可以了,通知监控停止")
cancel()
//为了检测监控过是否停止,如果没有监控输出,就表示停止了
time
.Sleep(5*
time
.Second
)
}
funcwatch(
ctx context
.Context
,name
string){
for
{
select
{
case
<-
ctx
.Done():
fmt
.Println(name
,"监控退出,停止了...")
return
default:
fmt
.Println(name
,"goroutine监控中...")
time
.Sleep(2*
time
.Second
)
}
}
}
示例中启动了3个监控goroutine进行不断的监控,每一个都使用了Context进行跟踪,当我们使用cancel
函数通知取消时,这3个goroutine都会被结束。这就是Context的控制能力,它就像一个控制器一样,按下开关后,所有基于这个Context或者衍生的子Context都会收到通知,这时就可以进行清理操作了,最终释放goroutine,这就优雅的解决了goroutine启动后不可控的问题。
Context接口
Context的接口定义的比较简洁,我们看下这个接口的方法。
type
Contextinterface
{
Deadline()
(
deadline time
.Time
,ok
bool)
Done()
<-
chan
struct{}
Err()
error
Value(
key
interface{})interface{}
}
这个接口共有4个方法,了解这些方法的意思非常重要,这样我们才可以更好的使用他们。
Deadline
方法是获取设置的截止时间的意思,第一个返回式是截止时间,到了这个时间点,Context会自动发起取消请求;第二个返回值ok==false时表示没有设置截止时间,如果需要取消的话,需要调用取消函数进行取消。
Done
方法返回一个只读的chan,类型为struct{}
,我们在goroutine中,如果该方法返回的chan可以读取,则意味着parent context已经发起了取消请求,我们通过Done
方法收到这个信号后,就应该做清理操作,然后退出goroutine,释放资源。
Err
方法返回取消的错误原因,因为什么Context被取消。
Value
方法获取该Context上绑定的值,是一个键值对,所以要通过一个Key才可以获取对应的值,这个值一般是线程安全的。
以上四个方法中常用的就是Done
了,如果Context取消的时候,我们就可以得到一个关闭的chan,关闭的chan是可以读取的,所以只要可以读取的时候,就意味着收到Context取消的信号了,以下是这个方法的经典用法。
func
Stream(
ctx context
.Context
,out
chan<-Value
)error
{
for
{
v
,err
:=DoSomething(
ctx
)
if
err
!=nil
{
return
err
}
select
{
case
<-
ctx
.Done():
return
ctx
.Err()
case
out
<-v
:
}
}
}
Context接口并不需要我们实现,Go内置已经帮我们实现了2个,我们代码中最开始都是以这两个内置的作为最顶层的partent context,衍生出更多的子Context。
var
(
background
=new(
emptyCtx
)
todo
=new(
emptyCtx
)
)
funcBackground()
Context
{
return
background
}
funcTODO()
Context
{
return
todo
}
一个是Background
,主要用于main函数、初始化以及测试代码中,作为Context这个树结构的最顶层的Context,也就是根Context。
一个是TODO
,它目前还不知道具体的使用场景,如果我们不知道该使用什么Context的时候,可以使用这个。
他们两个本质上都是emptyCtx
结构体类型,是一个不可取消,没有设置截止时间,没有携带任何值的Context。
type emptyCtx
int
func(*
emptyCtx
)Deadline()
(
deadline time
.Time
,ok
bool){
return
}
func(*
emptyCtx
)Done()
<-chan
struct{}
{
return
nil
}
func(*
emptyCtx
)Err()
error
{
return
nil
}
func(*
emptyCtx
)Value(
key
interface{})interface{}
{
return
nil
}
这就是emptyCtx
实现Context接口的方法,可以看到,这些方法什么都没做,返回的都是nil或者零值。
Context的继承衍生
有了如上的根Context,那么是如何衍生更多的子Context的呢?这就要靠context包为我们提供的With
系列的函数了。
funcWithCancel(
parent Context
)(
ctx Context
,cancel CancelFunc
)
funcWithDeadline(
parent Context
,deadline time
.Time
)(
Context
,CancelFunc
)
funcWithTimeout(
parent Context
,timeout time
.Duration
)(
Context
,CancelFunc
)
funcWithValue(
parent Context
,key
,val
interface{})Context
这四个With
函数,接收的都有一个partent参数,就是父Context,我们要基于这个父Context创建出子Context的意思,这种方式可以理解为子Context对父Context的继承,也可以理解为基于父Context的衍生。
通过这些函数,就创建了一颗Context树,树的每个节点都可以有任意多个子节点,节点层级可以有任意多个。
WithCancel
函数,传递一个父Context作为参数,返回子Context,以及一个取消函数用来取消Context。WithDeadline
函数,和WithCancel
差不多,它会多传递一个截止时间参数,意味着到了这个时间点,会自动取消Context,当然我们也可以不等到这个时候,可以提前通过取消函数进行取消。
WithTimeout
和WithDeadline
基本上一样,这个表示是超时自动取消,是多少时间后自动取消Context的意思。
WithValue
函数和取消Context无关,它是为了生成一个绑定了一个键值对数据的Context,这个绑定的数据可以通过Context.Value
方法访问到,后面我们会专门讲。
大家可能留意到,前三个函数都返回一个取消函数CancelFunc
,这是一个函数类型,它的定义非常简单。
type CancelFunc
func()
这就是取消函数的类型,该函数可以取消一个Context,以及这个节点Context下所有的所有的Context,不管有多少层级。
WithValue传递元数据
通过Context我们也可以传递一些必须的元数据,这些数据会附加在Context上以供使用。
var key
string="name"
funcmain()
{
ctx
,cancel
:=context
.WithCancel(context
.Background())
//附加值
valueCtx
:=context
.WithValue(ctx
,key
,"【监控1】")
go
watch(
valueCtx
)
time
.Sleep(10*
time
.Second
)
fmt
.Println("可以了,通知监控停止")
cancel()
//为了检测监控过是否停止,如果没有监控输出,就表示停止了
time
.Sleep(5*
time
.Second
)
}
funcwatch(
ctx context
.Context
){
for
{
select
{
case
<-
ctx
.Done():
//取出值
fmt
.Println(ctx
.Value(key
),"监控退出,停止了...")
return
default:
//取出值
fmt
.Println(ctx
.Value(key
),"goroutine监控中...")
time
.Sleep(2*
time
.Second
)
}
}
}
在前面的例子,我们通过传递参数的方式,把name
的值传递给监控函数。在这个例子里,我们实现一样的效果,但是通过的是Context的Value的方式。
我们可以使用context.WithValue
方法附加一对K-V的键值对,这里Key必须是等价性的,也就是具有可比性;Value值要是线程安全的。
这样我们就生成了一个新的Context,这个新的Context带有这个键值对,在使用的时候,可以通过Value
方法读取ctx.Value(key)
。
记住,使用WithValue传值,一般是必须的值,不要什么值都传递。
Context 使用原则
- 不要把Context放在结构体中,要以参数的方式传递
- 以Context作为参数的函数方法,应该把Context作为第一个参数,放在第一位。
- 给一个函数方法传递Context的时候,不要传递nil,如果不知道传递什么,就使用context.TODO
- Context的Value相关方法应该传递必须的数据,不要什么数据都使用这个传递
- Context是县城安全的,可以放心的在多个goroutine中传递
作者:豆瓣奶茶
链接:https://www.jianshu.com/p/dcbd87eb1a3f
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。