Go Context源码阅读

简介

context 主要用来在 goroutine 之间传递上下文信息,包括:取消信号、超时时间、截止时间、元数据传递等。 本文通过对Context源码阅读,对其有更深的理解,以便在工作中正确使用。

使用场景

contextGo开发常用的并发控制,常用在异步场景中用于实现并发协调以及对 goroutine 的生命周期控制,与WaitGroup最大的不同点是context对于父子之间goroutine有更强的控制力,它可以控制多级的goroutine

context 主要用来在 goroutine 之间传递上下文信息,包括:取消信号、超时时间、截止时间、元数据传递等。

Go 里,不能直接杀死协程,协程的关闭一般通过 channel+select 方式来控制。因此在一些在多级goroutine的情况下,例如处理一个请求衍生了很多协程,这些协程之间是相互关联的:需要共享一些全局变量、有共同的 deadline 等,而且可以同时被关闭。再用 channel+select 就会比较麻烦,这时就可以通过 context 来实现。

goroutine-imge.png

Context代码阅读

代码路径 /src/context/context.go

代码版本 GOVERSION='go1.21.1'

Context接口定义

type Context interface {
    // 返回 context 过期时间,只有定时取消才会用到该函数
    Deadline() (deadline time.Time, ok bool)
    // 返回 context 中的 channel;
    Done() <-chan struct{}
    // 返回错误
    Err() error
    // 返回 context 中的对应 key 的值
    Value(key any) any 
}
  1. Deadline():返回一个deadline和标识是否已设置deadlinebool值,如果没有设置deadline,则ok == false,此时deadline为一个初始值的time.Time值。

  2. Done():该方法返回一个channel,需要在select-case语句中使用,如case <-context.Done():

    • context关闭后,Done()返回一个被关闭的管道,关闭的管道仍然是可读的,据此 goroutine可以收到关闭请求;
    • context还未关闭时,Done()返回nil
  3. Err():该方法描述context关闭的原因。关闭原因由context实现控制,不需要用户设置。比如Deadline context,关闭原因可能是因为deadline,也可能提前被主动关闭,那么关闭原因就会不同:

    • context关闭后,Err()返回context的关闭原因;
      • deadline超时关闭:"context deadline exceeded"
      • cancel主动关闭: "context canceled"
      • context还未关闭时,Err()返回nil
  4. Value():类型是valueCtxcontext,它不是用于控制呈树状分布的goroutine,而是用于在树状分布的goroutine间传递信息。Value()方法就是用于此种类型的context,该方法根据key值查询map中的value

emptyCtx 结构体

主要用于context的根节点,空的context只是简单的实现了Context,本身不包含任何值,仅用于其他context的父节点。


// emptyCtx 是一个空的结构体, 不能被取消,没有deadline, 没有值。 是`backgroundCtx` 和 `todoCtx`的基类
type emptyCtx struct{}

// Deadline 方法会返回一个公元元年时间以及 false 的 flag,标识当前 context 不存在过期时间;
func (emptyCtx) Deadline() (deadline time.Time, ok bool) {
   return
}

//Done 方法返回一个类型只读的 nil 值,用户无论往 nil 中写入或者读取数据,均会陷入阻塞;
func (emptyCtx) Done() <-chan struct{} {
   return nil
}

// Err 方法返回的错误永远为 nil;
func (emptyCtx) Err() error {
   return nil
}
//Value 方法返回的 value 同样永远为 nil.
func (emptyCtx) Value(key any) any {
   return nil
}

// String() 方法来定制类型的字符串形式的输出,换句话说:一种可阅读性和打印性的输出。
// 继承于emptyCtx
type backgroundCtx struct{ emptyCtx }
func (backgroundCtx) String() string {
   return "context.Background"
}

type todoCtx struct{ emptyCtx }
func (todoCtx) String() string {
   return "context.TODO"
}

emptyCtx 是实现了 Context接口最简单的结构体,里面没有任何处理逻辑。
结构体目的只是为了构建Background()TODO()两个函数.(见下源码)

  • Background 函数主要用来作为创建其他Context的父Context

  • TODO 当调用函数需要Context但是暂时不知道传入什么类型的 Context的时候,使用该函数(可以理解为临时占位用)

// Background returns a non-nil, empty [Context]. It is never canceled, has no
// values, and has no deadline. It is typically used by the main function,
// initialization, and tests, and as the top-level Context for incoming
// requests.
func Background() Context {
   return backgroundCtx{}
}

// TODO returns a non-nil, empty [Context]. Code should use context.TODO when
// it's unclear which Context to use or it is not yet available (because the
// surrounding function has not yet been extended to accept a Context
// parameter).
func TODO() Context {
   return todoCtx{}
}

小结: context.Background()ctxTodo:=context.TODO() 方法的初始化部分已经完成。

cancelCtx 结构体

使用例子

讲解代码之前,首先了解一段如何使用context.WithCancel(context.Background())

func main() {
   // 创建扫描器
   ticker := time.NewTicker(1 * time.Second)
   ctx, cancel := context.WithCancel(context.Background())
   go func() {
      time.Sleep(2 * time.Second)
      // 经过2s强制终止
      cancel()
   }()
   for {
      select {
      case <-ctx.Done():
         fmt.Println(ctx.Err().Error())
         return
      case <-ticker.C: // 间隔1s发送数据
         fmt.Println("发送数据")
      }
   }
}

通过简单的编码,就可以轻松的监听取消事件的发生,简化了编程的复杂性。

cancelCtx结构体是实现Context接口较复杂也是最重要的一个结构体。

canceler 接口定义

源码如下:

// A canceler is a context type that can be canceled directly. The
// implementations are *cancelCtx and *timerCtx.
type canceler interface {
   cancel(removeFromParent bool, err, cause error)
   Done() <-chan struct{}
}

实现了上面定义的两个方法的 Context```,就表明该 ``Context 是可取消的。源码中有两个类型实现了 canceler 接口:cancelCtxtimerCtx

cancelCtx 数据结构

// &cancelCtxKey is the key that a cancelCtx returns itself for.
var cancelCtxKey int

// A cancelCtx can be canceled. When canceled, it also cancels any children
// that implement canceler.
type cancelCtx struct {
   Context                         //嵌入式接口类型,cancelCtx 必然为某个context的子context;

   mu       sync.Mutex            // 互斥锁,保护以下字段不受并发访问的影响
   done     atomic.Value          // 原子通道,第一次调用取消函数时被惰性创建,在该context及其后代context都被取消时关闭
   children map[canceler]struct{} // 值为struct{}其实是一个set,保存当前上下文的所有子上下文,第一次取消调用时设为nil
   err      error                 // 第一次取消操作时设置为一个错误值,对此上下文及后代上下文进行取消操作返回该错误
   cause    error                 // go高版本中支持设置取消原因
}

Done() 方法

func (c *cancelCtx) Done() <-chan struct{} {
   d := c.done.Load() //基于 atomic 包,读取 cancelCtx 中的 chan;倘若已存在,则直接返回;
   if d != nil {
      return d.(chan struct{})
   }
   c.mu.Lock() //加锁后,再次检查 chan 是否存在,若存在则返回;(double check)
   defer c.mu.Unlock()
   d = c.done.Load()
   if d == nil { // 如果不存在,创建strct{} 类型channel
      d = make(chan struct{})
      c.done.Store(d) // 初始化 chan 存储到 aotmic.Value 当中,并返回.(懒加载机制)
   }
   return d.(chan struct{})
}

c.done 使用了惰性加载(lazy loading)的机制,只有一次调用 Done() 方法的时候才会被创建,且通过加锁二次检查,确保在多个goroutine同时调用 Done() 方法时,只有第一个goroutine创建通道,其他goroutine均复用已创建的通道。

函数返回的是一个只读的 channel,一般通过搭配 select 来使用,当channel关闭后,就会立即读出零值,据此可以判断cancelCtx是否被取消。

image.png

这块看源代码, 大白话讲一下:
Done() 只有在被执行的时候才会去创建生成一个 只读的channel,外部使用到 case <-ctx.Done(): 时候会被阻塞起来。 如果上下文中执行了 cancel() 后那么直接 close()channel

如果 receive 值 从一个非 nil 但是已关闭的channel 永不阻塞。

Err() 方法

func (c *cancelCtx) Err() error {
   c.mu.Lock()
   err := c.err
   c.mu.Unlock()
   return err
}

cancelCtx.err默认是nil,在context被cancel时指定一个error变量: "context canceled"

Valuel()方法

func (c *cancelCtx) Value(key any) any {
   if key == &cancelCtxKey { //如果 key 特定值 &cancelCtxKey,则返回 cancelCtx 自身的指针;
      return c
   }
   return value(c.Context, key) //否则 遵循 valueCtx 的思路取值返回
}

如果 key 特定值 &cancelCtxKey,则返回 cancelCtx 自身的指针(基于 cancelCtxKey 为 key 取值时返回 cancelCtx 自身,是 cancelCtx 特有的协议)

简单讲就是:既然你要创建一个可取消的Context,那么什么时候取消这个Context呢?

  • 要么外部主动调用 cancel方法取消

  • 要么就是被动的取消。创建c := newCancelCtx(parent)的时候有个父Context,如果父Context被取消了,那么自己作为儿子也就被取消吧

context.WithCancel()方法

context.WithCancel()方法 是Go语言中的context包提供的函数之一,用于创建一个可取消的上下文对象。

func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
   c := withCancel(parent)
   return c, func() { c.cancel(true, Canceled, nil) }
}

func withCancel(parent Context) *cancelCtx {
   if parent == nil { //校验参数parent——父context 非空;
      panic("cannot create context from nil parent")
   }
   c := &cancelCtx{}    // 创建cancelCtx对象 
   c.propagateCancel(parent, c) //propagateCancel方法内启动一个守护协程,当父context终止时,该cancelCtx 也被终止;
   return c
}


// propagateCancel arranges for child to be canceled when parent is.
// It sets the parent context of cancelCtx.
func (c *cancelCtx) propagateCancel(parent Context, child canceler) {
   // parent 父协程,child 当前协程。 
   c.Context = parent // 注入parent——父context构造cancelCtx;
    
   done := parent.Done()
   if done == nil { // parent 是不会被 cancel 的类型(如 emptyCtx),则直接返回。 对于cannel类型一定是不为空的
      return // parent is never canceled
   }

   select {
   case <-done:
       //  parent 已经被 cancel,则直接终止子 context,并以 parent 的 err 作为子 context 的 err
      child.cancel(false, parent.Err(), Cause(parent))
      return
   default:
   }
    
    
   //parentCancelCtx通过 parent.Value(&cancelCtxKey) 判断 父节点是否是cancelCtx 类型        
   //若以特定的 cancelCtxKey 从 parent 中通过parent.Value()取值,取得的 value 是 parent 本身,则返回 true.         
   //若parent 的 channel 已关闭或者是不会被 cancel 的类型,则返回 false;
   if p, ok := parentCancelCtx(parent); ok { // 父亲节点可以被关闭,则向上关联
      // parent is a *cancelCtx, or derives from one.
      p.mu.Lock()
      if p.err != nil { // 父节点已经被取消了, 那自己取消掉自己就行了。
         // parent has already been canceled
         child.cancel(false, p.err, p.cause)
      } else { // 父节点还没有被取消
         if p.children == nil { // 没有子节点,就给他创建一个
            p.children = make(map[canceler]struct{})
         }
         p.children[child] = struct{}{} // 把自己放在父节点的上面
      }
      p.mu.Unlock()
      return
   }
   
   // 判断是否为 afterFuncer 类型的context, 后边会介绍。 go 21 中新特性
   if a, ok := parent.(afterFuncer); ok {
      // parent implements an AfterFunc method.
      c.mu.Lock()
      stop := a.AfterFunc(func() {
         child.cancel(false, parent.Err(), Cause(parent))
      })
      c.Context = stopCtx{
         Context: parent,
         stop:    stop,
      }
      c.mu.Unlock()
      return
   }

   goroutines.Add(1) // 实际生产中没有什么用,测试用的
   go func() { // 创建新的监听
      select {
      case <-parent.Done(): // 父节点退出
         child.cancel(false, parent.Err(), Cause(parent))
      case <-child.Done(): // 自己写退出
      }
   }()
}

propagateCancel 用以传递父子 context 之间的 cancel 事件,向上寻找可以“挂靠”的“可取消”的 context,并且“挂靠”上去。这样,调用上层 cancel 方法的时候,就可以层层传递,将那些挂靠的子 context 同时“取消”。

当通过WithCancel(parent Context)创建一个新的cancelContext时propagateCancel 被调用,用来确保当parentcontext终止时,该cancelCtx 也被终止:

image.png

  1. 若 parent 是不会被 cancel 的类型(如 emptyCtx),不需要传递,则直接返回;

  2. 若 parent 已经被 cancel,则直接终止子 context,并以 parent 的 err 作为子 context 的 err;

  3. 判断父节点为 afterFuncer类型,则修改自己的Ctx,将以父节点 和 stop(将自己的cannel注入进去)形成新的上下文。这样父节点在调用前置逻辑后就可以调用自己的cannel(函数)

  4. 若 parent 是 cancelCtx 的类型,则加锁,并将子 context 添加到 parent 的 children map 当中;

  5. 若 parent 不是 cancelCtx 类型,但又存在 cancel 的能力(比如用户自定义实现的 context),则启动一个协程,通过多路复用的方式监控 parent 状态,倘若其终止,则同时终止子 context,并传递 parent 的 err

cancelCtx.cancel()

// removeFromParent  bool,表示当前 context 是否需要从父 context 的 children set 中删除;
// 若当前的cancel是由父节点取消引起的,由于父节点已取消,则removeFromParent可以为false
// 第二个 err 则是 cancel 后需要展示的错误;
func (c *cancelCtx) cancel(removeFromParent bool, err, cause error) {
   //校验传入的 err 是否为空,若为空则 panic
   if err == nil {
      panic("context: internal error: missing cancel error")
   }
   // 如果case为空,则将error赋值给case
   if cause == nil {
      cause = err
   }
   c.mu.Lock()
   //校验 cancelCtx 自带的 err 是否已经非空,若非空说明已被 cancel,则解锁返回
   if c.err != nil { // 这里就是为什么能频繁的 cannel的原因
      c.mu.Unlock()
      return // already canceled
   }
   c.err = err
   c.cause = cause
   d, _ := c.done.Load().(chan struct{})
   if d == nil {
      //若 channel 此前未初始化,则直接注入一个 closedChan,否则关闭该 channel;
      c.done.Store(closedchan)
   } else {
      close(d)
   }
   //遍历当前 cancelCtx 的 children set,依次将 children context 都进行 cancel;
   for child := range c.children {
       // 此时child.cancel第一个参数为false,因为父context已经关闭,会将child从set中删除
       // 即若当前的cancel是由父节点取消引起的,则removeFromParent 则可以为false
      child.cancel(false, err, cause)
   }
   c.children = nil
   c.mu.Unlock()
    
   // 根据传入的 removeFromParent flag 判断是否需要手动把 cancelCtx 从 parent 的 children set 中移除.
   if removeFromParent {
       //如果 parent 不是 cancelCtx,直接返回(因为只有 cancelCtx 才有 children set) 
       //加锁;从 parent 的 children set 中delete删除对应 child解锁返回.
      removeChild(c.Context, c)
   }
}

image.png

context.WithCancel() 函数返回一个新的上下文(context)以及一个可用于取消该上下文的取消函数c.cancel(true, Canceled)

当调用cancel()取消函数时,将通过 context 的取消信号来通知这个上下文相关联的所有操作停止执行并释放资源。步骤如下:

  1. 判断err参数是否为空,若为空则引发panic("context: internal error: missing cancel error"),否则首先加锁

  2. 判断cancelCtx 自带的 err 是否已经非空,若非空说明cancelCtx 已被 cance,则解锁返回。

  3. 判断channel是否初始化,若未初始化,则直接注入一个 closedChan关闭的通道,否则关闭已有的channel

  4. 将所有的子contex一起取消,并解锁

  5. 加锁,从 parentchildren setdelete删除所有 child,解锁返回.

context.WithCancelCause()方法

func WithCancelCause(parent Context) (ctx Context, cancel CancelCauseFunc) {
   c := withCancel(parent)
   return c, func(cause error) { c.cancel(true, Canceled, cause) }
}

// 使用方法
ctx, cancelFunc := context.WithCancelCause(parentCtx)
defer cancelFunc(errors.New("定义原因"))

可以设置额外的取消原因,也就是 error 信息

timerCtx

timerCtx结构

type timerCtx struct {
   cancelCtx
   timer *time.Timer // Under cancelCtx.mu.

   deadline time.Time
}

timerCtx 是继承于 cancelCtx ,除了继承 cancelCtx 的能力之外,新增了一个 time.Timer 用于定时终止 context;另外新增了一个 deadline 字段用于字段 timerCtx 的过期时间.

timerCtx.Deadline()方法

func (c *timerCtx) Deadline() (deadline time.Time, ok bool) {
    return c.deadline, true
}

由于继承了 cancelCtx 结构体,timerCtx 可以从其父亲结构体获得取消能力,同时也可以使用它的成员变量 timer 来设置超时。

当计时器触发时,会使用与该上下文相关联的取消函数来取消该上下文中运行的所有操作。

func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
   return WithDeadline(parent, time.Now().Add(timeout))
}

context.WithTimeout 方法用于构造一个 timerCtx,本质上会调用 context.WithDeadline 方法,截止时间是time.Now().Add(timeout).

context.WithDeadlineCause()方法

// not set the cause.
func WithDeadlineCause(parent Context, d time.Time, cause error) (Context, CancelFunc) {
   //校验 parent context 非空;
   if parent == nil {
      panic("cannot create context from nil parent")
   }
   //校验 parent是否可过期, 且过期时间是否早于自己,若是,则构造一个 cancelCtx 返回即可;
   if cur, ok := parent.Deadline(); ok && cur.Before(d) {
      // The current deadline is already sooner than the new one.
      return WithCancel(parent)
   }
    //构造出一个新的 timerCtx结构体;
   c := &timerCtx{
      deadline: d,
   }
    //调用propagateCancel启动守护方法,同步 parent 的 cancel 事件到子 context
   c.cancelCtx.propagateCancel(parent, c)
   dur := time.Until(d)
   //判断过期时间是否已到,若是,直接 cancel timerCtx,并返回 DeadlineExceeded 的错误;
   if dur <= 0 {
      c.cancel(true, DeadlineExceeded, cause) // deadline has already passed
      return c, func() { c.cancel(false, Canceled, nil) }
   }
   //加锁; c.cancel() 和 time.AfterFunc() 两个操作的原子性和同步性确保在计时器触发前或在取消方法执行后不再触发计时器。
   c.mu.Lock()
   defer c.mu.Unlock()
   if c.err == nil {
      c.timer = time.AfterFunc(dur, func() {
          //启动time.Timer,达到过期时间后会调用cancel终止该 timerCtx,并返回 DeadlineExceeded 的错误;            
          //time.AfterFunc(dur, func()) 函数的工作方式是异步的,它创建一个新的 Goroutine 来运行计时器并等待计时器触发。
          c.cancel(true, DeadlineExceeded, cause)
      })
   }
   // 返回 timerCtx,以及一个封装了 cancel 逻辑的闭包 cancel 函数.
   return c, func() { c.cancel(true, Canceled, nil) }
}

image.png

WithDeadline 函数会创建一个新的到期时间戳的上下文。如果在到期时间之前完成操作,则操作正常完成;否则,如果到期时间已经过了,上下文将被标记为超时,并将其传播给与此上下文相关联的所有操作,从而达到错误处理和资源释放的目的。 返回值是一个设置好到期时间戳的子上下文对象,以及一个取消函数(CancelFunc),可以随时用于删除此上下文及其所有子级上下文。创建过程如下:

  1. 判断parent是否为空,若为空,则引发panic("cannot create context from nil parent")

  2. 判断parent是否可过期, 且过期时间是否早于自己,若是,则构造一个cancelCtx返回即可;

  3. 构造出一个新的 timerCtx结构体,并调用propagateCancel启动守护方法,同步 parent 的 cancel 事件到子 context

  4. 判断过期时间是否已到,若是,直接 cancel timerCtx,并返回 DeadlineExceeded 的错误;

  5. 加锁;并启动time.Timer,这会开启一个新的协程,当Timer达到过期时间后会调用cancel终止该 timerCtx,并返回 DeadlineExceeded 的err;

  6. 返回 timerCtx,以及一个封装了 cancel 逻辑的闭包 cancel 函数.

timerCtx.cancel()方法

func (c *timerCtx) cancel(removeFromParent bool, err, cause error) {
   //复用继承的 cancelCtx 的 cancel 能力,进行 cancel 处理;
   c.cancelCtx.cancel(false, err, cause)
   if removeFromParent {
      //判断是否需要手动从 parent 的 children set 中移除,若是则进行处理
      removeChild(c.cancelCtx.Context, c)
   }
   c.mu.Lock()
   if c.timer != nil {
      //停止 time.Timer, 避免timer泄漏
      c.timer.Stop()
      c.timer = nil
   }
   c.mu.Unlock()
}

由于继承了 cancelCtx 结构体,timerCtx 可以从其父亲结构体获得取消能力,调用cancel()时过程如下

  1. 首先调用父亲结构体的cancel()函数,不过此时父亲结构体的cancel()第一个参数removeFromParentfalse,因为不需要删除父亲结点的所有子结点

  2. 判断是否需要将该timerCtx手动从 parent 的 children set 中移除,若是则进行处理

  3. 之后加锁,暂停timer计数器并置空,之后解锁返回

valueCtx

valueCtx结构

type valueCtx struct {
   Context
   key, val any
}
  • valueCtx 同样继承了一个 parent context;
  • 一个 valueCtx 中仅有一组 KV值。

WithValue() 方法

func WithValue(parent Context, key, val any) Context {
   if parent == nil {
      panic("cannot create context from nil parent")
   }
   if key == nil {
      panic("nil key")
   }
   // 判断Key是不是可以比较的类型
   if !reflectlite.TypeOf(key).Comparable() {
      panic("key is not comparable")
   }
   // 保存父节点的上下文
   return &valueCtx{parent, key, val}
}

valueCtx.Value()方法

func (c *valueCtx) Value(key any) any {
    if c.key == key {
        return c.val
    }
    return value(c.Context, key)
}


func value(c Context, key any) any {
  //启动一个 for 循环,由下而上,由子及父,依次对 key 进行匹配
   for {
      switch ctx := c.(type) {
      case *valueCtx:
         if key == ctx.key {
            return ctx.val
         }
         c = ctx.Context
      case *cancelCtx:
         if key == &cancelCtxKey {
            return c
         }
         c = ctx.Context
      case withoutCancelCtx:
         if key == &cancelCtxKey {
            // This implements Cause(ctx) == nil
            // when ctx is created using WithoutCancel.
            return nil
         }
         c = ctx.c
      case *timerCtx:
         if key == &cancelCtxKey {
            return &ctx.cancelCtx
         }
         c = ctx.Context
      case backgroundCtx, todoCtx:
         return nil
      default:
         return c.Value(key)
      }
   }
}
  1. 假如当前 valueCtx 的 key 等于用户传入的 key,则直接返回其 value;

  2. 假如不等,则调用value方法从 parent context 中依次向上寻找,直到找到根节点emptyctx,若找不到则返回空。

  3. 其中 cancelCtxtimerCtxemptyCtx 类型会有特殊的处理方式,如当前key== &cancelCtxKey ,则会返回cancelCtx自身,而emptyCtx 则会返回空

小结

阅读源码可以看出valueCtx 不适合视为存储介质,存放大量的 kv 数据,原因如下:

  • 一个 valueCtx 实例只能存一个 kv 对,因此 n 个 kv 对会嵌套 n 个 valueCtx,造成空间浪费;

  • 基于 k 寻找 v 的过程是线性的,时间复杂度 O(N);

  • 不支持基于 k 的去重,相同 k 可能重复存在,并基于起点的不同,返回不同的 v.

由此得知,valueContext 的定位类似于请求头,context 存储的应该是一些共同的数据。例如:登陆的 session、cookie 等。

afterFuncCtx(go1.21版本后)

afterFuncCtx结构

type afterFuncCtx struct {
   cancelCtx
   once sync.Once // either starts running f or stops f from running
   f    func()
}

afterFuncCtx 同样继承了一个 cancelCtx

AfterFunc()方法

// AfterFunc will use it to schedule the call.
func AfterFunc(ctx Context, f func()) (stop func() bool) {
   a := &afterFuncCtx{
      f: f,
   }
   a.cancelCtx.propagateCancel(ctx, a)
   return func() bool { // 返回stop 调用函数
      stopped := false
      a.once.Do(func() {
         stopped = true
      })
      if stopped {
         a.cancel(true, Canceled, nil)
      }
      return stopped
   }
}

cancel() 方法

func (a *afterFuncCtx) cancel(removeFromParent bool, err, cause error) {
   a.cancelCtx.cancel(false, err, cause)
   if removeFromParent {
      removeChild(a.Context, a)
   }
   // 开了goroutine去执行函数
   a.once.Do(func() {
      go a.f()
   })
}

总结

总体类图

image.png

context使用建议

  1. 不要在结构体中加入 Context 参数,而是将它显式地传递给需要它的每个函数,并且它应该是第一个参数,通常命名为 ctx

  2. Context 是线程安全的,可以放心地在多个 goroutine 中使用。

  3. Context 传递给多个 goroutine 使用时,只要执行一次 cancel 操作,所有的 goroutine 就可以收到 取消的信号

  4. 不要把原本可以由函数参数来传递的变量,交给 ContextValue 来传递。

  5. 当一个函数需要接收一个 Context 时,但是此时你还不知道要传递什么 Context 时,可以先用 context.TODO 来代替,而不要选择传递一个 nil

  6. 当一个 Contextcancel 时,继承自该 Context 的所有 子 Context 都会被 cancel。并且可以多次执行cancel

关于我
loading