查看原文
其他

哇,超详细解析 Go 语言 Context(附图)

polarisxu 2022-11-21

The following article is from Golang来啦 Author Seekload


大家好,今天分享一篇从源码的角度详细分析 Context 包的文章。

长文预警,点击文末「阅读原文」查看网页版,方便阅读。

context 源码解析

我们分析的 Go 版本是 1.15.15。

整体结构图

主要函数、结构体和变量说明:

名称类型可否导出说明
Context接口可以Context 基本接口,定义了 4 个方法
canceler接口不可以Context 取消接口,定义了 2 个方法
CancelFunc函数可以取消函数签名
Background函数可以返回一个空的 Context,常用来作为根 Context
Todo函数可以返回一个空的 context,常用于初期写的时候,没有合适的 context 可用
emptyCtx结构体不可以实现了 Context 接口,默认都是空实现,emptyCtx 是 int 类型别名
cancelCtx结构体不可以可以被取消
valueCtx结构体不可以可以存储 k-v 信息
timerCtx结构体不可以可被取消,也可超时取消
WithCancel函数可以基于父 context,创建可取消 Context
WithDeadline函数可以创建一个有 deadline 的 context
WithTimeout函数可以创建一个有 timeout 的 context
WithValue函数可以创建一个存储 k-v 的 context
newCancelCtx函数不可以创建一个可取消的 context
propagateCancel函数不可以向下传递 context 节点间的取消关系
parentCancelCtx函数不可以找到最先出现的一个可取消 Context
removeChild函数不可以将当前的 canceler 从父 Context 中的 children map 中移除
background变量不可以包级 Context,默认的 Context,常作为顶级 Context
todo变量不可以包级 Context,默认的 Context 实现,也作为顶级 Context,与 background 同类型
closedchan变量不可以channel struct{}类型,用于信息通知
Canceled变量可以取消 error
DeadlineExceeded变量可以超时 error
cancelCtxKey变量不可以int 类型别名,做标记用的

接口

context 定义了两个接口,Context 和 canceler。如文章开头的整体结构图所示,*emptyCtx 和 *valueCtx 实现了 Context, *cancelCtx 同时实现了 Context 和 canceler, *timerCtx 因为内嵌了 cancelCtx,也间接实现了 Context 和 canceler。

Context 接口

Context 接口包括四个方法,源码如下:

type Context interface {

 Deadline() (deadline time.Time, ok bool)

 Done() <-chan struct{}

 Err() error

 Value(key interface{}) interface{}
}

Context 接口的这四个方法都是幂等的,连续多次调用同一个方法,返回的结果都是相同的。

Deadline() 返回 context 被取消的时间,如果没有设置截止时间,ok 返回 false。

Done() 返回一个只读的 channel,当 Context 被主动取消或者超时自动取消时,该 Context 及其派生的 Context 的 done channel 将会被关闭,我们知道,读取一个关闭的 channel 会读出相应类型的零值,正好利用这点,与 select 配合使用,实现协程控制或者超时退出等。

Err() 返回一个 error 对象,当 channel 没有被 close 的时候,返回 nil,如果 channel 被 close, 返回 channel 被 close 的原因。

Value() 获取设置的 key 对应的 value,如果不存在则返回 nil。

canceler 接口

接口定义如下:

type canceler interface {
 cancel(removeFromParent bool, err error)
 Done() <-chan struct{}
}

如果一个 Context 类型实现了上面定义的两个方法,该 Context 就是一个可取消的 Context。Context 包中 *cancelCtx 和 *timerCtx 实现了 canceler 接口,注意这里是指针类型。

第一次看到这两个接口时,我就在想为什么不把 canneler 和 Context 合并呢?况且他们定义的方法中都有 Done 方法,可以解释得通的说法是,源码作者认为 cancel 方法并不是 Context 必须的,根据最小接口设计原则,将两者分开。像 emptyCtx 和 valueCtx 不是可取消的,所以他们只要实现 Context 接口即可。cancelCtx 和 timerCtx 是可取消的 Context,他们要实现 2 个接口中的所有方法。

Context 的四种实现

emptyCtx

从源码可以看出,emptyCtx 实际上就是个 int,其对 Context 接口的实现不是直接返回,就是返回 nil,是一个空实现。它通常用于创建 root Context,标准库中 context.Background() 和 context.TODO() 返回的就是这个 emptyCtx。emptyCtx 不能取消、不能传值且没有 deadline。

// An emptyCtx is never canceled, has no values, and has no deadline. It is not
// struct{}, since vars of this type must have distinct addresses.
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 被包装成 background 和 todo,通过包提供的 Background() 和 TODO() 导出供外部使用,两者都是不可取消的 Context,通常都是放在 main 函数或者最顶层使用。


var (
 background = new(emptyCtx)
 todo       = new(emptyCtx)
)


func Background() Context {
 return background
}


func TODO() Context {
 return todo
}

cancelCtx

Context 包的核心实现就是 cancelCtx,包括里面的构造树形结构、级联取消等。

type cancelCtx struct {
 Context

    // 互斥锁字段,保护下面字段
 mu       sync.Mutex
 done     chan struct{}
 // 记录可取消的孩子节点
 children map[canceler]struct{}
 err      error
}

这是一个可取消的 Context,实现了 canceler 接口;同时,接口 Context 是 cancelCtx 结构体的一个匿名字段,所以 cancelCtx 也可以看成是一个 Context,只不过 *cancelCtx 重写了 Value()、Err() 和 Done() 方法。

mu 字段用于保护结构体中的字段,在访问修改的时候进行加锁处理,防止并发 data race 冲突。

done 是一个 channel,配合 close(done) 实现信息通知,当一个 channel 被关闭之后,它返回的是该类型零值,此处是 struct{}。

children 保存可取消的子节点,cancelCtx 可以级联成一个树形结构。

err 当 done 没有关闭时,err 返回 nil,当 done 被关闭时,err 返回非空值,内容是被关闭的原因,是主动 cancel 还是 timeout 取消,这些错误信息都是 context 包内部定义的,比如下面这些:

// 主动取消
var Canceled = errors.New("context canceled")

// 超时取消
var DeadlineExceeded error = deadlineExceededError{}
type deadlineExceededError struct{}
func (deadlineExceededError) Error() string   { return "context deadline exceeded" }

Done() 和 Value() 方法比较简单,我们先分析,比较重要的 cancel() 方法放在后面展开分析。

Dono()方法

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
}

c.done 是“懒汉式”初始化,只有调用了 Done() 方法的时候才会被创建。Done() 方法用于通知该 Context 是否被取消,通过监听 channel 关闭达到被取消通知目的,c.done 没有被关闭的时候,调用 Done() 方法会 block,被关闭之后,调用 Done() 方法返回 struct{},一般通过搭配 select 使用。

Value()方法

*cancelCtx 的 Value() 方法实现如下:

func (c *cancelCtx) Value(key interface{}) interface{} {
 if key == &cancelCtxKey {
  return c
 }
 return c.Context.Value(key)
}

这个方法的实现比较有意思,cancelCtxKey 是一个 Context 包内部变量,将 key 与 &cancelCtxKey 比较,相等的话就返回 *cancelCtx,即 cancelCtx 的自身地址;否则继续递归。

问题 1:调用 context.WithCancel() 时发生了什么???

通过 WithCancel() 可以创建可取消的 Context 方法,它有两个返回值,分别是 Context 类型和 func() 类型,第一个返回值在使用时一般会传给其他协程,第二个返回值放在 main 协程或顶级协程中处理,这样便可实现调用方 caller 和被调方 callee 隔离。callee 只管负责收到 caller 发送的取消信息时执行退出操作。

创建方法如下:

func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
 if parent == nil {
  panic("cannot create context from nil parent")
 }
 c := newCancelCtx(parent)
 propagateCancel(parent, &c)
 return &c, func() { c.cancel(true, Canceled) }
}

func newCancelCtx(parent Context) cancelCtx {
 return cancelCtx{Context: parent}
}

可以看出创建的是一个 cancelCtx,可取消的 Context。newCancelCtx() 函数将 parent Context 设置到内部变量,这是实现从 child 向 parent 查找的基础条件,在后面我们将看到使用到它的地方。

这里我们需要重点分析下 propagateCancel() 和这个函数内部调用的 parentCancelCtx()。

parentCancelCtx() 解析如下:

func parentCancelCtx(parent Context) (*cancelCtx, bool) {
 // 从 parent 开始向上寻找第一个可取消的 *cancelCtx
 // 如果 parent done 为 nil 表示是不可取消的 Context;
 // 如果 parent done 为 closedchan 表示 Context 已经被取消了,这两种情况都直接返回。
 done := parent.Done()
 if done == closedchan || done == nil {
  return nilfalse
 }
 // 递归向上查询第一个 *cancelCtx
 // parent.Value(&cancelCtxKey) 递归向上查找节点是不是 cancelCtx。
 // 注意这里 p.done==done 的判断,是防止下面的情况,parent.Done() 找到的可取消 Context 是我们自定义的可取消Context,
 // 这样 parent.Done() 返回的 done 和 cancelCtx 肯定不在一个同级,它们的 done 肯定是不同的。这种情况也返回 nil。
 p, ok := parent.Value(&cancelCtxKey).(*cancelCtx)    // 类型断言
 if !ok {
  return nilfalse
 }
 p.mu.Lock()
 ok = p.done == done
 p.mu.Unlock()
 if !ok {
  return nilfalse
 }
 return p, true
}

上面的代码有两个需要关注的点:

第一点

p, ok := parent.Value(&cancelCtxKey).(*cancelCtx)

这里我们提前讲下 *valueCtx 类型,它也有自己的 Value() 方法,parent 有可能是 cancenCtx 或 valueCtx,所有会走不通的 Value() 方法。

可以看到传入的 key 是 cancelCtxKey 的地址,那 key==&cancelCtxKey 肯定是成立的,所以直接返回 *cancelCtx。换句话说, *cancelCtx 调用 Value() 返回它本身,非 *cancelCtx(比如 *valueCtx) 调用 Value() 是它自己的实现,肯定跟 *cancelCtx 是不一样的,对非 *cancelCtx 调用 c.Context.Value(&cancelCtxKey) 会一直递归查询到最后的 root context,返回的会是 nil。

结合下面的图更好理解,ctx3.Value(&cancelCtxKey) 会返回它本身的地址 &ctx3。对于 ctx2.Value(&cancelCtxKey),因为它是 valueCtx,结合 valueCtx.Value(key) 源码可以看到,它的 key 不可能是 &cancelCtxKey,因为它是不可导出的,在包外是不能获取到 cancelCtxKey 地址的,接着会走到 ctx2.Context.Value(&cancelCtxKey),就是在执行 ctx1.Value(&cancelCtxKey),ctx1 是 cancelCtx,所以会返回 ctx1 的地址 &ctx1。

第二点:

上面的代码中,p.done == done 的判断是为了防止下图这种情况,parent context 是个自定义的 cancelCtx 且重写了 Done() 方法,这种情况需要单独处理,返回 nil、false。

如图所示,对于 ctx3,parent.Done() 返回的是 ctx2.done ,而 p.done 返回的是 ctx1.done。

propagateCancel() 解析如下:

propagateCancel() 主要是向上寻找可取消的 context,并且“挂靠”上去。这是级联取消的前提,调用父级的 cancel() 时就可以层层传递,将那些挂靠的子 context 同时“取消”。

func propagateCancel(parent Context, child canceler) {
 // done channel 为 nil 时说明 parent context 必然永远不会被取消,所以就无需建立级联关系
 done := parent.Done()
 if done == nil {
  return // parent is never canceled
 }

 // 如果 done channel 不是 nil,说明 parent Context 是一个可以取消的 Context
 // 这里立即判断一下 done channel 是否可读取
 // 如果可以读取的话说明 parent Context 已经被取消了,那么应该立即取消 child Context
 select {
 case <-done:
  // parent is already canceled
  child.cancel(false, parent.Err())
  return
 default:
 }

 // 找到可以取消的父节点 *cancelCtx
 if p, ok := parentCancelCtx(parent); ok {
  p.mu.Lock()   // 加锁保护
  if p.err != nil {    // 再次判断父节点是否已经取消;如果父节点已经取消,子节点也要取消
   child.cancel(false, p.err)
  } else {
   if p.children == nil {
    p.children = make(map[canceler]struct{})
   }
   // 将子节点挂靠到父节点上,形成级联关系
   p.children[child] = struct{}{}
  }
  p.mu.Unlock()
 } else {
  atomic.AddInt32(&goroutines, +1)
  // 代码走到这里,说明向上无法找到可取消的 *cancelCtx,这种情况可能是自定义实现的 Context 类型
  // 这种情况下无法通过 parent Context 的 children map 建立关联,只能通过创建一个 goroutine 来完成及联取消的操作
  go func() {
   select {
   // 这里的 parent.Done() 不能省略,当 parent context 取消时,需要取消下面的 child cotext
   // 如果省略了就不能级联取消 child context
   case <-parent.Done():
    // 取消 child context
    child.cancel(false, parent.Err())
   // 当 child 取消时,groutine 退出,防止泄露
   case <-child.Done():
   }
  }()
 }
}

代码的分析都在注释里,其实我非常好奇自定义的 Context 类型触发上面 else 的逻辑,所以我把 Context 包的内容全部拷贝出来,做了个 case,全部的代码[1]在这里,下面列出关键代码:

// 自定义的 Context 类型
type MyContext struct {
 Context
 done chan struct{}
}

// MyContext定义自己的Done()方法,返回只读的channel
func (c *MyContext) Done() <-chan struct{} {
 if c.done == nil {
  c.done = make(chan struct{})
 }
 d := c.done
 return d
}

// 关闭自定义的 Context,模拟 cancel 操作
func (c *MyContext) Close() {
 ch := c.done
 close(ch)
}

func main() {

 rootCtx := Background()
 ctx1, cancel1 := WithCancel(rootCtx)

 fmt.Println("goroutine num: ", runtime.NumGoroutine())

 myCon := &MyContext{Context: ctx1}
 myCon.done = make(chan struct{})
 ctx2, cancel2 := WithCancel(myCon)
 ctx3, _ := WithCancel(ctx2)

 fmt.Println("goroutine num: ", runtime.NumGoroutine())

 // 关闭自定义context的done,模拟 parent context 取消
 myCon.Close()

 // 调用cancel2(),模拟 child context 取消
 //cancel2()

 time.Sleep(2 * time.Second) // 延时,给协程退出留出时间
 fmt.Println("goroutine num: ", runtime.NumGoroutine())

 fmt.Println(reflect.TypeOf(ctx1), reflect.ValueOf(ctx1))
 fmt.Println(reflect.TypeOf(myCon), reflect.ValueOf(myCon))
 fmt.Println(ctx1, ctx2, ctx3, cancel1, cancel2)
}


func propagateCancel(parent Context, child canceler) {

    // ...
 if p, ok := parentCancelCtx(parent); ok {
 // ...
 } else {
  atomic.AddInt32(&goroutines, +1)
  go func() {
   fmt.Println("监控中.......")
   select {
   case <-parent.Done():
    fmt.Println("parent cancel.........")            // 取消自定义的context,调试代码
    child.cancel(false, errors.New("parent cancel")) // 模拟错误 errors.New("parent cancel")
   case <-child.Done():
    fmt.Println("son cancel........."// 调试代码打印
   }
   fmt.Println("退出监控!")
  }()
 }
}

MyContext 是自定义的 Context 类型,并且重写了 Done() 方法,使用 MyContext 类型作为父节点创建了 ctx2,接着使用 ctx2 创建了 ctx3,我们模拟取消父节点 myCon.Close(),输出如下:

goroutine num:  1
goroutine num:  2
监控中.......
parent cancel.........
退出监控!
goroutine num:  1

// 其他输出省略

从输出可以得出,当父节点去掉时,会走 <-parent.Done() 的 case,接着会级联取消子节点 ctx2、ctx3。

如果调用 cancel2(),模拟取消子节点的操作,输出如下:

goroutine num:  1
goroutine num:  2
监控中.......
son cancel.........
退出监控!
goroutine num:  1

// 其他输出省略

由输出可得,当执行子节点的取消函数 cancel2() 时,走了 <-child.Done() 的 case,单独开启的协程退出,防止协程泄露。

问题 2:cancel() 的取消机制是怎么样的?

调用 context.WithCancel() 会返回一个取消函数 cancel(),当调用 cancel() 时,实际执行的是 *cancelCtx.cancel 方法,将 *cancelCtx.done 关闭,所有的 <-c.Done() 便会停止阻塞,达到通知 callee 的目的,然后对挂在下面的 child context 执行递归取消操作,将所有的 children 自底向上取消。

func (c *cancelCtx) cancel(removeFromParent bool, err error) {
 if err == nil {
  panic("context: internal error: missing cancel error")
 }
 c.mu.Lock()    // 加锁
 if c.err != nil {   // 再次判断,防止重复取消
  c.mu.Unlock()
  return // already canceled
 }
 c.err = err         // 取消的原因

 // 如果 c.done 还未初始化,说明 Done() 方法还未被调用,这时候直接将 c.done 赋值一个已关闭的 channel
 // Done() 方法被调用的时候不会阻塞直接返回 struct{}
 if c.done == nil {
  c.done = closedchan
 } else {
  close(c.done)    // 关闭c.done
 }
 // 如果有子节点,递归对子节点进行 cancel 操作
 for child := range c.children {
  // NOTE: acquiring the child's lock while holding parent's lock.
  child.cancel(false, err)
 }
 c.children = nil   // 清除 c.children
 c.mu.Unlock()

 if removeFromParent {
  // 将本节点从它的父节点中删除
  removeChild(c.Context, c)
 }
}

// 删除子节点
func removeChild(parent Context, child canceler) {
 // 向上寻找可取消的*cancelCtx
 p, ok := parentCancelCtx(parent)
 if !ok {
  return
 }
 p.mu.Lock()
 if p.children != nil {
  // 删除子节点
  delete(p.children, child)
 }
 p.mu.Unlock()
}

注意 removeFromParent 参数,对子节点执行 cancel() 时,即下面的 child.cancle(false,err) 传递的是 false,都会执行清空操作 c.children=nil,所以没有必要传 true;但是在最外层调用 cancel() 函数执行取消操作时,removeFromParent 要传 true,这里需要将 cancelCtx 从它的父节点 children map 中移除掉,因为父级节点并没有取消。

下面两幅图帮助大家理解这个取消过程:

取消 ctx5 之前

取消 ctx5 之后面

分析完 cancelCtx,接下来的 timerCtx 和 valueCtx 就比较简单了,我们继续!

timerCtx

timerCtx 基于 cancelCtx,所以它是一个可取消的 Context,此外它有超时定时器和超时截止时间字段,对 timer 和 deadline 的访问需要加锁,有了这两个配置就可以在特定时间进行自动取消。

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

 deadline time.Time
}

*timerCtx 重写了 cancel() 方法 和 Deadline() 方法,cancel() 后面再分析。

Deadline() 方法返回第一个参数是取消截止时间,第二个参数是是否设置了截止时间,如果没有设置的话 ok 返回 false。这里 ok 为啥直接返回 true 呢?因为通过创建 *timeCtx 时肯定会设置 *timeCtx.deadline 值。

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

问题三:*timerCtx 是如何创建的??

创建 *timerCtx 有两个方法,一个是 WithTimeout(),另一个是 WithDeadline()。

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

我们可以看到,WithTimeout() 基于 WithDeadline() ,将 timeout 转换成了 deadline。

我们重点看下 WithDeadline()

代码解析如下:

func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
 if parent == nil {
  panic("cannot create context from nil parent")
 }
 // 注意点!!!
 if cur, ok := parent.Deadline(); ok && cur.Before(d) {
  // 父节点 context 的超时时间比 d 时间早,直接创建一个可取消的 context,
  // 因为父 context 比子context先超时,当父节点超时时,会自动调用 cancel 函数,子context也会被取消。
  // 所以不用单独处理子context的定时器。
  return WithCancel(parent)
 }
 // 构建timerCtx
 c := &timerCtx{
  cancelCtx: newCancelCtx(parent),
  deadline:  d,
 }
 // 同 cancelCtx 的操作相同 ,将当前节点挂到父节点上
 propagateCancel(parent, c)
 dur := time.Until(d)    // 计算当前距离 deadline 的时间
 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 {
  // 重点!!! 启动一个定时器,在 dur 时间之后,自动进行取消操作
  c.timer = time.AfterFunc(dur, func() {
   c.cancel(true, DeadlineExceeded)
  })
 }
 return c, func() { c.cancel(true, Canceled) }
}

问题 4: *timerCtx 是如何取消的?

不管是手动取消 timerCtx 还是自动取消,取消操作都调用了 *timerCtx.cancel() 方法,如下:

func (c *timerCtx) cancel(removeFromParent bool, err error) {
 // 调用cancelCtx的取消方法,取消子节点
 c.cancelCtx.cancel(false, err)
 if removeFromParent {
  // 将当前的 *timerCtx 从父节点移除掉
  removeChild(c.cancelCtx.Context, c)
 }
 c.mu.Lock()
 if c.timer != nil {
  // 停止定时器
  c.timer.Stop()
  c.timer = nil
 }
 c.mu.Unlock()
}

valueCtx

type valueCtx struct {
 Context
 key, val interface{}
}

valueCtx 是一个 k-v Context,只能使用 WithValue() 函数创建,返回 *valueCtx,如下:

func WithValue(parent Context, key, val interface{}) Context {
 if parent == nil {
  panic("cannot create context from nil parent")
 }
 // key不能为空且是可以比较的,因为之后需要通过 key 取出 context 中的值,可比较是必须的
 if key == nil {
  panic("nil key")
 }
 if !reflectlite.TypeOf(key).Comparable() {
  panic("key is not comparable")
 }
 return &valueCtx{parent, key, val}
}

按照源码分析,感觉有点干巴巴,我们结合下例子分析:

func main() {

 rootCtx := context.Background()
 ctxVal11 := context.WithValue(rootCtx, "key11""name11")
 ctxVal12 := context.WithValue(ctxVal11, "key12""name12")
 ctxVal13 := context.WithValue(ctxVal12, "key13""name13")
 ctxVal14 := context.WithValue(ctxVal13, "key14""name14")

 ctxVal21 := context.WithValue(rootCtx, "key21""name21")

 fmt.Println("key14: ", ctxVal14.Value("key14"))
 fmt.Println("key14: ", ctxVal13.Value("key14"))
 fmt.Println("key21: ", ctxVal21.Value("key21"))
}

上面的例子,最终会形成像下面这样的一棵树:

使用 *valueCtx.Value 方法查询 key 对应的值:

func (c *valueCtx) Value(key interface{}) interface{} {
    // 要查询的 key 与当前的 valueCtx(c) 中的 key 相同,直接返回
 if c.key == key {
  return c.val
 }
 // 递归查询父节点
 return c.Context.Value(key)
}

查询时按照自底向上查询,如果当前节点 key 不存在,就继续查询父节点,如果都不存在,一直查询到根节点,根节点通常都是 Background() 或者 TODO(),返回 nil。

为什么可以向上查询,因为 c.Context 指向父节点。也正是因为只能向上查询,父节点没法获取子节点存储的值,子节点却可以获取父节点的值。比如上面的 case,父节点 ctxVal13 获取不到子节点 ctxVal14 存储的 key14 对应的值。

另外,递归向上只能查找 “直系” Context,也就是说可以无限递归查找 parent Context 是否包含这个 key,但是无法查找兄弟 Context 是否包含。比如上面的 case,通过分支 1 任何一个节点都无法获取分支 2 任何一个 key 对应的值。

总结

context 包的代码非常短,去掉注释的话也就 200+ 行,但却是并发控制的标准做法,比如实现 goroutine 之间传递取消信号、截止时间及传递一些 k-v 值等。如此短小精悍非常值得我们细读。如果有一直关注 context 包代码的同学就会发现,随着 go 版本的迭代,包里面的一些方法采用了更为优雅的实现方式,比如 parentCancelCtx() 函数,这个应该是在 1.14 某个小版本优化的。

最后,这篇文章只是从源码的角度分析了 context 的功能,关于 context 的一些最佳实践大家可以参考文末的推荐文章。欢迎一起讨论交流!

扩展阅读

1.go 官方关于 context 的 blog[2]
2.墙裂推荐!!饶大的深度解密 Go 语言之 context[3]
3.Go 语言实战笔记(二十)| Go Context[4]
4.Context 源码剖析[5]
5.https://www.kevinwu0904.top/blogs/golang-context/[6]

参考资料

[1]

全部的代码: https://go.dev/play/p/rYhHgg0UdE-

[2]

go官方关于context的blog: https://go.dev/blog/context

[3]

深度解密Go语言之context: https://qcrao.com/2019/06/12/dive-into-go-context/#cancelCtx

[4]

Go语言实战笔记(二十)| Go Context: https://www.flysnow.org/2017/05/12/go-in-action-go-context.html#context%E6%8E%A7%E5%88%B6%E5%A4%9A%E4%B8%AAgoroutine

[5]

Context 源码剖析: https://www.qtmuniao.com/2020/07/12/go-context/

[6]

https://www.kevinwu0904.top/blogs/golang-context/: https://www.kevinwu0904.top/blogs/golang-context/#%E6%80%BB%E7%BB%93




往期推荐


我是 polarisxu,北大硕士毕业,曾在 360 等知名互联网公司工作,10多年技术研发与架构经验!2012 年接触 Go 语言并创建了 Go 语言中文网!著有《Go语言编程之旅》、开源图书《Go语言标准库》等。


坚持输出技术(包括 Go、Rust 等技术)、职场心得和创业感悟!欢迎关注「polarisxu」一起成长!也欢迎加我微信好友交流:gopherstudio


您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存