context的中文翻译是
上下文 ,我们可以理解为 context 管理了一组呈现树状结构的 Goroutine ,让每个Goroutine 都拥有相同的上下文,并且可以在这个上下文中传递数据
我们理解为:当一个goroutine有很多子goroutine是,如果某事goroutine失败了,如何通知全部g取消呢,就是通过context
接下来看几个案例,再看原理
context 实际上只是定义的4个方法的接口,凡是实现了该接口的都称为一种 context
// A Context carries a deadline, a cancelation signal, and other values across
// API boundaries.
//
// Context's methods may be called by multiple goroutines simultaneously.
type Context interface {
// 返回绑定当前context的任务被取消的截止时间;如果没有设定期限,将返回ok == false。
Deadline() (deadline time.Time, ok bool)
// 绑定当前context的任务被取消时,将返回一个关闭的channel;如果当前context不会被取消,将返回nil。
Done() <-chan struct{}
// 如果Done返回的channel没有关闭,将返回nil;如果Done返回的channel已经关闭,将返回非空的值表示任务结束的原因。如果是context被取消,Err将返回Canceled;如果是context超时,Err将返回DeadlineExceeded。
Err() error
// 获取上游Goroutine 传递给下游Goroutine的某些数据
Value(key interface{}) interface{}
}
context.go 包中提供了4个以 With 开头的函数, 这几个函数的主要功能是实例化不同类型的context
1、通过 Background() 和 TODO() 创建最 emptyCtx 实例 ,通常是作为根节点
2、通过 WithCancel() 创建 cancelCtx 实例
3、通过 WithValue() 创建 valueCtx 实例
4、通过 WithDeadline 和 WithTimeout 创建 timerCtx 实例
WithCancel
package main
import (
"context"
"fmt"
"time"
)
func MyDo2(ctx context.Context) {
for {
select {
default:
fmt.Println("MyDo2 doing.... ")
time.Sleep(2 * time.Second)
case <-ctx.Done():
fmt.Println("MyDo2 Done")
fmt.Println(ctx.Err())
return
}
}
}
func MyDo1(ctx context.Context) {
go MyDo2(ctx)
for {
select {
case <-ctx.Done():
fmt.Println("MyDo1 Done")
fmt.Println(ctx.Err())
return
default:
fmt.Println("MyDo1 doing.... ")
time.Sleep(1 * time.Second)
}
}
}
func main() {
// 创建 cancelCtx 实例
// 传入context.Background() 作为根节点
ctx, cancel := context.WithCancel(context.Background())
// 向协程中传递ctx
go MyDo1(ctx)
time.Sleep(7 * time.Second)
fmt.Println("stop all goroutines")
// 执行cancel操作
cancel()
time.Sleep(2 * time.Second)
}
WithDeadline
package main
import (
"context"
"fmt"
"time"
)
func dl2(ctx context.Context) {
n := 1
for {
select {
case <-ctx.Done():
fmt.Println(ctx.Err())
return
default:
fmt.Println("dl2 doing.... ", n)
n++
time.Sleep(time.Second)
}
}
}
func dl1(ctx context.Context) {
n := 1
for {
select {
case <-ctx.Done():
fmt.Println(ctx.Err())
return
default:
fmt.Println("dl1 doing.... ", n)
n++
time.Sleep(2 * time.Second)
}
}
}
func main() {
// 设置deadline为当前时间之后的5秒那个时刻
d := time.Now().Add(5 * time.Second)
ctx, cancel := context.WithDeadline(context.Background(), d)
defer cancel()
go dl1(ctx)
go dl2(ctx)
time.Sleep(10 * time.Second)
}
WithTimeout
实际就是调用了WithDeadline()
WithValue
package main
import (
"context"
"fmt"
"time"
)
func v2(ctx context.Context) {
fmt.Println(ctx.Value("key"))
fmt.Println(ctx.Value("v1"))
// 相同键,值覆盖
ctx = context.WithValue(ctx, "key", "modify from v2")
}
func v1(ctx context.Context) {
if v := ctx.Value("key"); v != nil {
fmt.Println("key = ", v)
}
ctx = context.WithValue(ctx, "v1", "value of v1 func")
go v2(ctx)
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
// 向context中传递值
ctx = context.WithValue(ctx, "key", "main**")**
go v1(ctx)
time.Sleep(10 * time.Second)
cancel()
time.Sleep(3 * time.Second)
}
原理解析
1、通过 Background() 和 TODO() 创建最 emptyCtx 实例 ,通常是作为根节点
2、通过 WithCancel() 创建 cancelCtx 实例
3、通过 WithValue() 创建 valueCtx 实例
4、通过 WithDeadline 和 WithTimeout 创建 timerCtx 实例
1,Background()
Background(), Empty() 均会返回一个空的 Context emptyCtx。emptyCtx 对象在方法 Deadline(), Done(), Err(), Value(interface{}) 中均会返回nil,String() 方法会返回对应的字符串。这个实现比较简单
2,WithCancel 构造的context
type cancelCtx struct {
Context
// 互斥锁,保证context协程安全
mu sync.Mutex
// cancel 的时候,close 这个chan
done chan struct{}
// 派生的context
children map[canceler]struct{}
err error
}
WithCancel 方法首先会基于 parent 构建一个新的 Context
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
c := newCancelCtx(parent) // 新的上下文
propagateCancel(parent, &c) // 挂到parent 上
return &c, func() { c.cancel(true, Canceled) }
}
// 把child 挂在到parent 下
func propagateCancel(parent Context, child canceler) {
// 如果parent 为空,则直接返回
if parent.Done() == nil {
return // parent is never canceled
}
// 获取parent类型
if p, ok := parentCancelCtx(parent); ok {
p.mu.Lock()
if p.err != nil {
// parent has already been canceled
child.cancel(false, p.err)
} else {
if p.children == nil {
p.children = make(map[canceler]struct{})
}
p.children[child] = struct{}{}
}
p.mu.Unlock()
} else {
// 启动goroutine,等待parent/child Done
go func() {
select {
case <-parent.Done():
child.cancel(false, parent.Err())
case <-child.Done():
}
}()
}
}
其中,propagateCancel 方法会判断 parent 是否已经取消,如果取消,则直接调用方法取消;如果没有取消,会在parent的children 追加一个child。这里就可以看出,context 树状结构的实现。
Done()
func (c *cancelCtx) Done() <-chan struct{} {
c.mu.Lock()
if c.done == nil {
c.done = make(chan struct{})
}
d := c.done
c.mu.Unlock()
return d
}
实现比较简单,就是返回一个channel,等待chan 关闭。可以看出 Done 操作是在调用时才会构造 chan done,done 变量是延时初始化的。
在手动取消 Context 时
func (c *cancelCtx) cancel(removeFromParent bool, err error) {
// 一些判断,关闭 ctx.done chan
// ...
if c.done == nil {
c.done = closedchan
} else {
close(c.done)
}
// 广播到所有的child,需要cancel goroutine 了
for child := range c.children {
// NOTE: acquiring the child's lock while holding parent's lock.
child.cancel(false, err)
}
c.children = nil
c.mu.Unlock()
// 然后从父context 中,删除当前的context
if removeFromParent {
removeChild(c.Context, c)
}
}
会调用 cancelCtx 的 cancel 方法, 这里可以看到,当执行cancel时,除了会关闭当前的cancel外,还做了两件事,
① 所有的child 都调用cancel方法,
② 由于该上下文已经关闭,需要从父上下文中移除当前的上下文。
3,4,定时取消功能的上下文
WithDeadline, WithTimeout 提供了实现定时功能的 Context 方法,返回一个timerCtx结构体。WithDeadline 是给定了执行截至时间,WithTimeout 是倒计时时间
WithTImeout 是基于WithDeadline实现的,因此我们仅看其中的WithDeadline即可
WithDeadline 内部实现是基于cancelCtx 的。相对于 cancelCtx 增加了一个计时器,并记录了 Deadline 时间点。下面是timerCtx 结构体:
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
// 若父上下文结束时间早于child,
// 则child直接挂载在parent上下文下即可
if cur, ok := parent.Deadline(); ok && cur.Before(d) {
return WithCancel(parent)
}
// 创建个timerCtx, 设置deadline
c := &timerCtx{
cancelCtx: newCancelCtx(parent),
deadline: d,
}
// 将context挂在parent 之下
propagateCancel(parent, c)
// 计算倒计时时间
dur := time.Until(d)
if dur <= 0 {
c.cancel(true, DeadlineExceeded) // deadline has already passed
return c, func() { c.cancel(false, Canceled) }
}
c.mu.Lock()
defer c.mu.Unlock()
if c.err == nil {
// 设定一个计时器,到时调用cancel
c.timer = time.AfterFunc(dur, func() {
c.cancel(true, DeadlineExceeded)
})
}
return c, func() { c.cancel(true, Canceled) }
}
WithValue 构造的上下文与上面几种有区别,其构造的context 原型如下:
type valueCtx struct { // 保留了父节点的context
Context
key, val interface{}
}
每个context
包含了一个Key-Value组合。
valueCtx 保留了父节点的Context,
但没有像cancelCtx 一样保留子节点的Context
func (c *valueCtx) Value(key interface{}) interface{} {
if c.key == key {
return c.val
}
// 从父context 中获取
return c.Context.Value(key)
}
Value 的获取是采用链式获取的方法。如果当前 Context 中找不到,则从父Context中获取。如果我们希望一个context 多放几条数据时,可以保存一个map 数据到 context 中。这里不建议多次构造context来存放数据。毕竟取数据的成本也是比较高的。