0
点赞
收藏
分享

微信扫一扫

哇,14662 字解析 Go 语言 Context(附图)



你好,我是四哥。

之前​​几篇文章​​介绍过 Go 语言里面的 Context 包的使用方法和使用注意事项,今天我们就一起从源码的角度详细分析下 Context 包。

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

context 源码解析

我们分析的 Go 版本是 1.15.15。

整体结构图

哇,14662 字解析 Go 语言 Context(附图)_go

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

名称

类型

可否导出

说明

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 nil, false
}
// 递归向上查询第一个 *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 nil, false
}
p.mu.Lock()
ok = p.done == done
p.mu.Unlock()
if !ok {
return nil, false
}
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。

哇,14662 字解析 Go 语言 Context(附图)_python_02

第二点:

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

哇,14662 字解析 Go 语言 Context(附图)_编程语言_03

如图所示,对于 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 之前

哇,14662 字解析 Go 语言 Context(附图)_java_04

取消 ctx5 之后面

哇,14662 字解析 Go 语言 Context(附图)_go_05

分析完 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"))
}

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

哇,14662 字解析 Go 语言 Context(附图)_java_06

使用 *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​​


以上分享如果对你有所帮助,就给四哥点个赞,分享给身边学习 Go 语言的朋友叭,这样四哥也有更新下去的动力,跪谢各位父老乡亲嘞!

举报

相关推荐

0 条评论