哇,超详细解析 Go 语言 Context(附图)
大家好,今天分享一篇从源码的角度详细分析 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 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。
第二点:
上面的代码中,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]
参考资料
全部的代码: 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