3//如果Context实现了超时控制,该方法返回 超时时间,true。否则ok为false

4Done() <- chanstruct{}

5//依旧使用<-chan struct{}来通知退出,供被调用的goroutine监听。

6Err() error

7//当Done()返回的chan收到通知后,防卫Err()获知被取消的原因

8Value(key interface{}) interface

9}

canceler接口:拓展接口,规定了取消通知的Context具体类型需要实现的接口:

1typecanceler interface{

2cancel(removeFromParent bool, err error)

3//通知后续创建的goroutine退出

4Done() <- chanstruct{}

5//作者对这个Done()方法的理解是多余的

6}

struct

emptyCtx:实现了一个不具备任何功能的Context接口,其存在的目的就是作为Context对象树的root节点:

1typeemptyCtx int

2func(*emptyCtx)Deadline()(deadline time.Time, ok bool){

3return

4}

5func(*emptyCtx)Done()<-chanstruct{} {

6returnnil

7}

8func(*emptyCtx)Err()error{

9returnnil

10}

11func(*emptyCtx)Value(key interface{})interface{} {

12returnnil

13}

14//......

15var(

16background = new(emptyCtx)

17todo = new(emptyCtx)

18)

19funcBackground()Context{

20returnbackground

21}

22funcTODO()Context{

23returntodo

24}

25//这两者返回值是一样的,文档上建议main函数可以使用Background()创建root context

cancelCtx:可以认为他与emptyCtx最大的区别在于,具体实现了cancel函数。即他可以向子goroutine传递cancel消息。

timerCtx:另一个实现Context接口的具体类型,内部封装了cancelCtx类型实例,同时拥有deadline变量,用于实现定时退出通知。

valueCtx:实现了Context接口的具体类型,内部分装cancelCtx类型实例,同时封装了一个kv存储变量,用于传递通知消息。

API

除了root context可以使用Background()创建以外,其余的context都应该从cancelCtx,timerCtx,valueCtx中选取一个来构建具体对象:

func WithCancel(parent Context) (Context, CancelFunc):创建cancelCtx实例。

func WithDeadline(parent Context, deadline time.Time)(Context, CancelFunc)与func WithTimeout(parent Context, timeout time.Duration)(Context, CancelFunc):两种方法都可以创建一个带有超时通知的Context具体对象timerCtx,具体差别在于传递绝对或相对时间。

func WithValue(parent Context, key, val interface{}) Context:创建valueCtx实例。

1、创建root context并构建一个WithCancel类型的上下文,使用该上下文注册一个goroutine模拟运行:

1funcmain(){

2ctxa, cancel := context.WithCancel(context.Background())

3gowork(ctxa, "work1")

4}

5funcwork(ctx context.Context, name string){

6for{

7select{

8case<-ctx.Done():

9println(name, " get message to quit")

10return

11default:

12println(name, " is running")

13time.Sleep(time.Second)

14}

15}

16}

2、使用WithDeadline包装ctxa,并使用新的上下文注册另一个goroutine:

1funcmain(){

2ctxb, _ := context.WithTimeout(ctxa, time.Second * 3)

3gowork(ctxb, "work2")

4}

3、使用WithValue包装ctxb,并注册新的goroutine:

1funcmain(){

2ctxc := context.WithValue(ctxb, "key", "custom value")

3goworkWithValue(ctxc, "work3")

4}

5funcworkWithValue(ctx context.Context, name string){

6for{

7select{

8case<-ctx.Done():

9println(name, " get message to quit")

10return

11default:

12value:=ctx.Value( "key").( string)

13println(name, " is running with value", value)

14time.Sleep(time.Second)

15}

16}

17}

4、最后在main函数中手动关闭ctxa,并等待输出结果:

1func main(){

2time.Sleep( 5*time.Second)

3cancel()

4time.Sleep(time.Second)

5}

6

7至此我们运行程序并查看输出结果:

8work1 isrunning

9work3 isrunning with valuecustom value

10work2 isrunning

11work1 isrunning

12work2 isrunning

13work3 isrunning with valuecustom value

14work2 isrunning

15work3 isrunning with valuecustom value

16work1 isrunning

17//work2超时并通知work3退出

18work2 getmessage to quit

19work3 getmessage to quit

20work1 isrunning

21work1 isrunning

22work1 getmessage to quit

可以看到,当ctxb因超时而退出之后,会通知由他包装的所有子goroutine(ctxc),并通知退出。各context的关系结构如下:

Background() -> ctxa -> ctxb -> ctxc

Background() -> ctxa -> ctxb -> ctxc

我们主要研究两个问题,即各Context如何保存父类和子类上下文;以及cancel方法如何实现通知子类context实现退出功能。

context的数据结构

1、emptyCtx只是一个uint类型的变量,其目的只是为了作为第一个goroutine ctx的parent,因此他不需要,也没法保存子类上下文结构。

2、cancelCtx的数据结构:

1type cancelCtx struct{

2Context

3

4mu sync.Mutex // protects following fields

5done chan struct{} // created lazily, closed by first cancel call

6children map[canceler] struct{} // set to nil by the first cancel call

7err error // set to non-nil by the first cancel call

8}

Context接口保存的就是父类的context。children map[canceler]struct{}保存的是所有直属与这个context的子类context。done chan struct{}用于发送退出信号。

我们查看创建cancelCtx的APIfunc WithCancel(…)…:

1funcWithCancel(parent Context)(ctx Context, cancel CancelFunc){

2c := newCancelCtx(parent)

3propagateCancel(parent, &c)

4return&c, func(){ c.cancel( true, Canceled) }

5}

6funcnewCancelCtx(parent Context)cancelCtx{

7returncancelCtx{Context: parent}

8}

propagateCancel函数的作用是将自己注册至parent context。我们稍后会讲解这个函数。

3、timerCtx的数据结构:

1type timerCtx struct{

2cancelCtx

3timer *time.Timer // Under cancelCtx.mu.

4

5deadline time.Time

6}

timerCtx继承于cancelCtx,并为定时退出功能新增自己的数据结构。

1funcWithDeadline(parent Context, d time.Time)(Context, CancelFunc){

2ifcur, ok := parent.Deadline(); ok && cur.Before(d) {

3// The current deadline is already sooner than the new one.

4returnWithCancel(parent)

5}

6c := &timerCtx{

7cancelCtx: newCancelCtx(parent),

8deadline: d,

9}

10propagateCancel(parent, c)

11//以下内容与定时退出机制有关,在本文不作过多分析和解释

12dur := time.Until(d)

13ifdur <= 0{

14c.cancel( true, DeadlineExceeded) // deadline has already passed

15returnc, func(){ c.cancel( true, Canceled) }

16}

17c.mu.Lock()

18deferc.mu.Unlock()

19ifc.err == nil{

20c.timer = time.AfterFunc(dur, func(){

21c.cancel( true, DeadlineExceeded)

22})

23}

24returnc, func(){ c.cancel( true, Canceled) }

25}

26funcnewCancelCtx(parent Context)cancelCtx{

27returncancelCtx{Context: parent}

28}

timerCtx查看parent context的方法是timerCtx.cancelCtx.Context。

4、valueCtx的数据结构:

1type valueCtx struct{

2Context

3key, val interface{}

4}

相较于timerCtx而言非常简单,没有继承于cancelCtx struct,而是直接继承于Context接口。

1funcWithValue(parent Context, key, val interface{})Context{

2ifkey == nil{

3panic( "nil key")

4}

5if!reflect.TypeOf(key).Comparable() {

6panic( "key is not comparable")

7}

8return&valueCtx{parent, key, val}

9}

辅助函数

这里我们会有两个疑问,第一,valueCtx为什么没有propagateCancel函数向parent context注册自己。既然没有注册,为何ctxb超时后能通知ctxc一起退出。第二,valueCtx是如何存储children和parent context结构的。相较于同样绑定Context接口的cancelCtx,valueCtx并没有children数据。

第二个问题能解决一半第一个问题,即为何不向parent context注册。先说结论:valueCtx的children context注册在valueCtx的parent context上。函数func propagateCancel(…)负责注册信息,我们先看一下他的构造:

func propagateCancel1funcpropagateCancel(parent Context, child canceler){

2ifparent.Done() == nil{

3return// parent is never canceled

4}

5ifp, ok := parentCancelCtx(parent); ok {

6p.mu.Lock()

7ifp.err != nil{

8// parent has already been canceled

9child.cancel( false, p.err)

10} else{

11ifp.children == nil{

12p.children = make( map[canceler] struct{})

13}

14p.children[child] = struct{}{}

15}

16p.mu.Unlock()

17} else{

18gofunc(){

19select{

20case<-parent.Done():

21child.cancel( false, parent.Err())

22case<-child.Done():

23}

24}()

25}

26}

这个函数的主要逻辑如下:接收parent context 和 child canceler方法,若parent为emptyCtx,则不注册;否则通过funcparentCancelCtx寻找最近的一个*cancelCtx;若该cancelCtx已经结束,则调用child的cancel方法,否则向该cancelCtx注册child。

func parentCancelCtx1funcparentCancelCtx(parent Context)(*cancelCtx, bool){

2for{

3switchc := parent.( type) {

4case*cancelCtx:

5returnc, true

6case*timerCtx:

7return&c.cancelCtx, true

8case*valueCtx:

9parent = c.Context

10default:

11returnnil, false

12}

13}

14}

func parentCancelCtx从parentCtx中向上迭代寻找第一个cancelCtx并返回。从函数逻辑中可以看到,只有当parent.(type)为*valueCtx的时候,parent才会向上迭代而不是立即返回。否则该函数都是直接返回或返回经过包装的*cancelCtx。因此我们可以发现,valueCtx是依赖于parentCtx的*cancelCtx结构的。

至于第二个问题,事实上,parentCtx根本无需,也没有办法通过Done()方法通知valueCtx,valueCtx也没有额外实现Done()方法。可以理解为:valueCtx与parentCtx公用一个done channel,当parentCtx调用了cancel方法并关闭了done channel时,监听valueCtx的done channel的goroutine同样会收到退出信号。另外,当parentCtx没有实现cancel方法(如emptyCtx)时,可以认为valueCtx也是无法cancel的。

func (c *cancelCtx) cancel1func(c *cancelCtx)cancel(removeFromParent bool, err error){

2iferr == nil{

3panic( "context: internal error: missing cancel error")

4}

5c.mu.Lock()

6ifc.err != nil{

7c.mu.Unlock()

8return// already canceled

9}

10c.err = err

11ifc.done == nil{

12c.done = closedchan

13} else{

14close(c.done)

15}

16forchild := rangec.children {

17child.cancel( false, err)

18}

19c.children = nil

20c.mu.Unlock()

21

22ifremoveFromParent {

23removeChild(c.Context, c)

24}

25}

该方法的主要逻辑如下:若外部err为空,则代表这是一个非法的cancel操作,抛出panic;若cancelCtx内部err不为空,说明该Ctx已经执行过cancel操作,直接返回;关闭done channel,关联该Ctx的goroutine收到退出通知;遍历children,若有的话,执行child.cancel操作;调用removeChild将自己从parent context中移除。

func (c *timerCtx) cancel

与cancelCtx十分类似,不作过多阐述。

ID:Golangweb