context.Context类型
context.Context类型(以下简称Context类型)是在Go 1.7发布时才被加入到标准库的。而后,标准库中的很多其他代码包都为了支持它而进行了扩展,包括:os/exec包、net包、database/sql包,以及runtime/pprof包和runtime/trace包,等等。
Context类型之所以受到了标准库中众多代码包的积极支持,主要是因为它是一种非常通用的同步工具。它的值不但可以被任意地扩散,而且还可以被用来传递额外的信息和信号。
更具体地说,Context类型可以提供一类代表上下文的值。此类值是并发安全的,也就是说它可以被传播给多个goroutine。
由于Context类型实际上是一个接口类型,而context包中实现该接口的所有私有类型,都是基于某个数据类型的指针类型,所以,如此传播并不会影响该类型值的功能和安全。
context 的作用
1.通过context,我们可以方便地对同一个请求所产生地goroutine进行约束管理,可以设定超时、deadline,甚至是取消这个请求相关的所有goroutine。形象地说,假如一个请求过来,需要A去做事情,而A让B去做一些事情,B让C去做一些事情,A、B、C是三个有关联的goroutine,那么问题来了:假如在A、B、C还在处理事情的时候请求被取消了,那么该如何优雅地同时关闭goroutine A、B、C呢?这个时候就轮到context包上场了。
2.在golang中的创建一个新的线程并不会返回像c语言类似的pid所有我们不能从外部杀死某个线程,所有我就得让它自己结束之前我们用channel+select的方式,来解决这个问题但是有些场景实现起来比较麻烦,例如由一个请求衍生出多个线程并且之间需要满足一定的约束关系,以实现一些诸如:
有效期,中止线程树,传递请求全局变量之类的功能
context 接口定义
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key interface{}) interface{}
}
1.Deadline方法是获取设置的截止时间的意思,第一个返回式是截止时间,到了这个时间点,Context会自动发起取消请求;第二个返回值ok==false时表示没有设置截止时间,如果需要取消的话,需要调用取消函数进行取消。
2.Done方法返回一个只读的chan,类型为struct{},我们在goroutine中,如果该方法返回的chan可以读取,则意味着parent context已经发起了取消请求,我们通过Done方法收到这个信号后,就应该做清理操作,然后退出goroutine,释放资源。之后,Err 方法会返回一个错误,告知为什么 Context 被取消。
3.Err方法返回取消的错误原因,因为什么Context被取消。
4.Value方法获取该Context上绑定的值,是一个键值对,所以要通过一个Key才可以获取对应的值,这个值一般是线程安全的。
创建 context
一个是Background,主要用于main函数、初始化以及测试代码中,作为Context这个树结构的最顶层的Context,也就是根Context,它不能被取消。
一个是TODO,如果我们不知道该使用什么Context的时候,可以使用这个。
两个实现方式代码是一样的,不同的是,静态分析工具可以使用它来验证 context 是否正确传递
var (
background = new(emptyCtx)
todo = new(emptyCtx)
)
func Background() Context {
return background
}
func TODO() Context {
return todo
}
四个重要的函数
1.context.WithCancel(parent Context) (ctx Context, cancel CancelFunc)
返回派生 context 和取消函数。只有创建它的函数才能调用取消函数来取消此 context。如果您愿意,可以传递取消函数,但是,强烈建议不要这样做。这可能导致取消函数的调用者没有意识到取消 context 的下游影响。
ctx, cancel := context.WithCancel(context.Background())
2.context.WithDeadline(parent Context, d time.Time) (ctx Context, cancel CancelFunc)
此函数返回其父项的派生 context,当截止日期超过或取消函数被调用时,该 context 将被取消。例如,您可以创建一个将在以后的某个时间自动取消的 context,并在子函数中传递它。当因为截止日期耗尽而取消该 context 时,获此 context 的所有函数都会收到通知去停止运行并返回。
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(2 * time.Second))
3.context.WithTimeout(parent Context, timeout time.Duration) (ctx Context, cancel CancelFunc)
此函数类似于 context.WithDeadline。不同之处在于它将持续时间作为参数输入而不是时间对象。此函数返回派生 context,如果调用取消函数或超出超时持续时间,则会取消该派生 context。
ctx, cancel := context.WithTimeout(context.Background(), 2 * time.Second)
4.context.WithValue(parent Context, key, val interface{}) (ctx Context, cancel CancelFunc)
此函数接收 context 并返回派生 context,其中值 val 与 key 关联,并通过 context 树与 context 一起传递。这意味着一旦获得带有值的 context,从中派生的任何 context 都会获得此值。
示例:
func ContextTest01() {
logger.Info("start")
ctx, cancelFunc := context.WithCancel(context.Background())
go func() {
logger.Info("go 1")
cancelFunc()
}()
<-ctx.Done()
logger.Info("end")
logger.Info("end", ctx.Err())
}
func ContextTest02() {
fmt.Println("start")
gen := func(ctx context.Context) <-chan int {
dst := make(chan int)
n := 1
go func() {
for {
select {
case <-ctx.Done():
logger.Info("end")
return // returning not to leak the goroutine
case dst <- n:
n++
}
}
}()
return dst
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel() // main 方法执行完后,结束 ctx 相关的 goroutine
for n := range gen(ctx) {
logger.Info(n)
if n == 5 {
break
}
}
}
func ContextTest03() {
// 自动取消(定时取消)
timeout := 3 * time.Second
ctx, _ := context.WithTimeout(context.Background(), timeout)
logger.Info(Add(ctx), ctx.Err())
}
func ContextTest04() {
// 手动取消
ctx, cancel := context.WithCancel(context.Background())
go func() {
time.Sleep(2 * time.Second)
cancel() // 在调用处主动取消
}()
logger.Info(Add(ctx), ctx.Err())
}
func Cdd(ctx context.Context) int {
logger.Info(ctx.Value("K_C"))
<-ctx.Done()
return -3
}
func Bdd(ctx context.Context) int {
logger.Info(ctx.Value("K_A"))
logger.Info(ctx.Value("K_B"))
ctxc := context.WithValue(ctx, "K_C", "I am GO")
go logger.Info(Cdd(ctxc), ctxc.Err())
<-ctxc.Done()
return -2
}
func Add(ctx context.Context) int {
ctxa := context.WithValue(ctx, "K_A", "HELLO")
ctxb := context.WithValue(ctxa, "K_B", "WORLD")
go logger.Info(Bdd(ctxb), ctxb.Err())
<-ctxb.Done()
return -1
}
知识扩展
问题1:“可撤销的”在context包中代表着什么?“撤销”一个Context值又意味着什么?
我相信很多初识context包的Go程序开发者,都会有这样的疑问。确实,“可撤销的”(cancelable)这个词在这里是比较抽象的,很容易让人迷惑。我这里再来解释一下。
这需要从Context类型的声明讲起。这个接口中有两个方法与“撤销”息息相关。Done方法会返回一个元素类型为struct{}的接收通道。不过,这个接收通道的用途并不是传递元素值,而是让调用方去感知“撤销”当前Context值的那个信号。
一旦当前的Context值被撤销,这里的接收通道就会被立即关闭。我们都知道,对于一个未包含任何元素值的通道来说,它的关闭会使任何针对它的接收操作立即结束。
正因为如此,在coordinateWithContext函数中,基于调用表达式cxt.Done()的接收操作,才能够起到感知撤销信号的作用。
除了让Context值的使用方感知到撤销信号,让它们得到“撤销”的具体原因,有时也是很有必要的。后者即是Context类型的Err方法的作用。该方法的结果是error类型的,并且其值只可能等于context.Canceled变量的值,或者context.DeadlineExceeded变量的值。
前者用于表示手动撤销,而后者则代表:由于我们给定的过期时间已到,而导致的撤销。
func ContextTest01() {
logger.Info("start")
ctx, cancelFunc := context.WithCancel(context.Background())
go func() {
logger.Info("go 1")
cancelFunc()
}()
<-ctx.Done()
logger.Info("end")
logger.Info("end", ctx.Err())
// 最后输出 ‘end context canceled’
}
你可能已经感觉到了,对于Context值来说,“撤销”这个词如果当名词讲,指的其实就是被用来表达“撤销”状态的信号;如果当动词讲,指的就是对撤销信号的传达;而“可撤销的”指的则是具有传达这种撤销信号的能力。
我在前面讲过,当我们通过调用context.WithCancel函数产生一个可撤销的Context值时,还会获得一个用于触发撤销信号的函数。
通过调用这个函数,我们就可以触发针对这个Context值的撤销信号。一旦触发,撤销信号就会立即被传达给这个Context值,并由它的Done方法的结果值(一个接收通道)表达出来。
撤销函数只负责触发信号,而对应的可撤销的Context值也只负责传达信号,它们都不会去管后边具体的“撤销”操作。实际上,我们的代码可以在感知到撤销信号之后,进行任意的操作,Context值对此并没有任何的约束。
最后,若再深究的话,这里的“撤销”最原始的含义其实就是,终止程序针对某种请求(比如HTTP请求)的响应,或者取消对某种指令(比如SQL指令)的处理。这也是Go语言团队在创建context代码包,和Context类型时的初衷。
如果我们去查看net包和database/sql包的API和源码的话,就可以了解它们在这方面的典型应用。
问题2:撤销信号是如何在上下文树中传播的?
我在前面讲了,context包中包含了四个用于繁衍Context值的函数。其中的WithCancel、WithDeadline和WithTimeout都是被用来基于给定的Context值产生可撤销的子值的。
context包的WithCancel函数在被调用后会产生两个结果值。第一个结果值就是那个可撤销的Context值,而第二个结果值则是用于触发撤销信号的函数。
在撤销函数被调用之后,对应的Context值会先关闭它内部的接收通道,也就是它的Done方法会返回的那个通道。
然后,它会向它的所有子值(或者说子节点)传达撤销信号。这些子值会如法炮制,把撤销信号继续传播下去。最后,这个Context值会断开它与其父值之间的关联。
我们通过调用context包的WithDeadline函数或者WithTimeout函数生成的Context值也是可撤销的。它们不但可以被手动撤销,还会依据在生成时被给定的过期时间,自动地进行定时撤销。这里定时撤销的功能是借助它们内部的计时器来实现的。
当过期时间到达时,这两种Context值的行为与Context值被手动撤销时的行为是几乎一致的,只不过前者会在最后停止并释放掉其内部的计时器。
最后要注意,通过调用context.WithValue函数得到的Context值是不可撤销的。撤销信号在被传播时,若遇到它们则会直接跨过,并试图将信号直接传给它们的子值。
问题 3:怎样通过Context值携带数据?怎样从中获取数据?
既然谈到了context包的WithValue函数,我们就来说说Context值携带数据的方式。
WithValue函数在产生新的Context值(以下简称含数据的Context值)的时候需要三个参数,即:父值、键和值。与“字典对于键的约束”类似,这里键的类型必须是可判等的。
原因很简单,当我们从中获取数据的时候,它需要根据给定的键来查找对应的值。不过,这种Context值并不是用字典来存储键和值的,后两者只是被简单地存储在前者的相应字段中而已。
Context类型的Value方法就是被用来获取数据的。在我们调用含数据的Context值的Value方法时,它会先判断给定的键,是否与当前值中存储的键相等,如果相等就把该值中存储的值直接返回,否则就到其父值中继续查找。
如果其父值中仍然未存储相等的键,那么该方法就会沿着上下文根节点的方向一路查找下去。
注意,除了含数据的Context值以外,其他几种Context值都是无法携带数据的。因此,Context值的Value方法在沿路查找的时候,会直接跨过那几种值。
如果我们调用的Value方法的所属值本身就是不含数据的,那么实际调用的就将会是其父辈或祖辈的Value方法。这是由于这几种Context值的实际类型,都属于结构体类型,并且它们都是通过“将其父值嵌入到自身”,来表达父子关系的。
最后,提醒一下,Context接口并没有提供改变数据的方法。因此,在通常情况下,我们只能通过在上下文树中添加含数据的Context值来存储新的数据,或者通过撤销此种值的父值丢弃掉相应的数据。如果你存储在这里的数据可以从外部改变,那么必须自行保证安全。