go实战学习——context包学习理解笔记
Posted Demonwuwen
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了go实战学习——context包学习理解笔记相关的知识,希望对你有一定的参考价值。
context包学习理解笔记
学习golang时,在看一下项目的源码过程中,总会发现context包被频繁的调用,为啥调用,一脸懵逼。通过对context使用进行详细的学习后整理笔记出来。
context包
context也就是说的上下文。
context 包我们就用来做两件事:
- 安全传递数据 :是指在请求执行上下文中线 程安全地传递数据,依赖于 WithValue 方法
- 控制链路
使用场景比较丰富: 链路追踪的 trace id 、 AB测试的标记位 、 压力测试标记位 、分库分表中间件中传递 sharding hint 、ORM 中间件传递 SQL hint 、 Web 框架传递上下文等
打开context包源码,大致内容讲解如下:
context包定义了context类型,它跨越API边界和进程之间携带截止日期、取消信号和其他请求范围的值。
向服务器的传入请求应该创建一个Context,而向服务器发出的调用应该接受一个Context。它们之间的函数调用链必须传播上下文,可以选择用使用WithCancel、WithDeadline、WithTimeout或WithValue创建的派生上下文替换它。当一个上下文被取消时,从它派生的所有上下文也被取消。
WithCancel, WithDeadline和WithTimeout函数接受一个Context(父函数),并返回一个派生的Context(子函数)和一个CancelFunc。调用CancelFunc会取消子进程及其子进程,移除父进程对子进程的引用,并停止任何相关的计时器。未能调用CancelFunc会泄漏子进程及其子进程,直到父进程被取消或计时器触发。go vet工具检查CancelFuncs是否在所有控制流路径上使用。
使用上下文的程序应该遵循这些规则,以保持跨包的接口一致,并启用静态分析工具来检查上下文传播:
- 不要将context存储在struct类型中;相反,显式地将Context传递给每个需要它的函数。Context应该是第一个参数,通常命名为ctx:
func DoSomething(ctx context.Context, arg Arg) error
// ... use ctx ...
- 不要传递空上下文,即使函数允许这样做。通过上下文。如果您不确定要使用哪个上下文,则使用TODO。
- 上下文值只用于传递进程和api的请求范围内的数据,而不是传递可选参数给函数。
- 相同的上下文可以传递给运行在不同goroutine中的函数;上下文对于多个goroutines同时使用是安全的。
context 包的核心 API 有四个:
context.WithValue
:设置键值对,并且 返回一个新的 context 实例context.WithCancel
context.WithDeadline
context.WithTimeout
:三者都返回一个 可取消的 context 实例,和取消函数
注意:context 实例是不可变的,每一次都是 新创建的。
context.WithValue 用于安全传递数据
另外三个,WithCancel
,WithDeadline
,WithTimeout
用于控制链路
context接口
Context 接口核心 API 有四个:
- Deadline :返回过期时间,如果 ok 为 false,说明没有 设置过期时间。不常用
- Done:返回一个 channel,一般用于监听 Context 实例 的信号,比如说过期,或者正常关闭。常用
- Err:返回一个错误用于表达 Context 发生了什么。 Canceled => 正常关闭,DeadlineExceeded => 过期超 时。比较常用
- context.Value:取值。非常常用
context实例
context实例之间存在父子关系。
- 控制从上至下:当父亲取消或者超时,所有派生的子 context 都被取消或者超时
- 查找从下至上:当找 key 的时候,子 context 先看自己有 没有,没有则去祖先里面找
其中,父无法访问子内容,即父context无法拿到子context设置的值。
valueCtx
valueCtx定义如下:
type valueCtx struct
Context
key, val any
valueCtx 用于存储 key-value 数据,特点:
- 典型的装饰器模式:在已有 Context 的基础上附加一个存 储 key-value 的功能
- 只能存储一个 key, val:为什么不用 map?
- map 要求 key 是 comparable 的,而我们可能用不是 comparable 的 key
- context 包的设计理念就是将 Context
在查找值的时候,先从自己查找,不行再找父亲的
控制
context父亲可以控制儿子,但是儿子控制不了父亲
context 包提供了三个控制方法, WithCancel
、WithDeadline
和 WithTimeout
。
三者用法大同小异:
WithCancel
没有过期时间,但是又需要在必要的时候取 消,使用 WithCancelWithDeadline
在固定时间点过期,使用 WithDeadlineWithTimeout
在一段时间后过期,使用 WithTimeout
而后便是监听 Done() 返回的 channel,不管是 主动调用 cancel() 还是超时,都能从这个 channel 里面取出来数据。后面可以用 Err() 方 法来判断究竟是哪种情况。
context最经典的用法:控制超时,相当于我们同时监听两个 channel,一个是正常业务结束的 channel,一个Done() 返回的。
超时控制至少两个分支:
- 超时分支
- 正常业务分支
所以普遍来说 context.Context 会和 select- case 一起使用。
// WithCancel returns a copy of parent with a new Done channel. The returned
// context's Done channel is closed when the returned cancel function is called
// or when the parent context's Done channel is closed, whichever happens first.
//
// Canceling this context releases resources associated with it, so code should
// call cancel as soon as the operations running in this Context complete.
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)
// newCancelCtx returns an initialized cancelCtx.
func newCancelCtx(parent Context) cancelCtx
return cancelCtxContext: parent
// goroutines counts the number of goroutines ever created; for testing.
var goroutines int32
// propagateCancel arranges for child to be canceled when parent is.
func propagateCancel(parent Context, child canceler)
done := parent.Done()
if done == nil
return // parent is never canceled
select
case <-done:
// parent is already canceled
child.cancel(false, parent.Err())
return
default:
if p, ok := parentCancelCtx(parent); ok
p.mu.Lock()
if p.err != nil
// parent has already been canceled
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)
go func()
select
case <-parent.Done():
child.cancel(false, parent.Err())
case <-child.Done():
()
cancelCtx
其定义如下
type cancelCtx struct
Context
mu sync.Mutex // protects following fields
done atomic.Value // of chan struct, created lazily, closed by first cancel call
children map[canceler]struct // set to nil by the first cancel call
err error // set to non-nil by the first cancel call
cancelCtx 也是典型的装饰器模式:在已有 Context 的基础上,加上取消的功能。
核心实现:
- Done 方法是通过类似于 double-check 的 机制写的。这种原子操作和锁结合的用法比较罕见。
- 利用 children 来维护了所有的衍生节点,难点就在于它是如何维护这个衍生节点。
children:核心是儿子把自己加进去父亲的 children 字段里面。 但是因为 Context 里面存在非常多的层级, 所以父亲不一定是 cancelCtx,因此本质上是找最近属于 cancelCtx 类型的祖先,然后儿 子把自己加进去。 cancel 就是遍历 children,挨个调用 cancel。然后儿子调用孙子的 cancel。
核心cancel方法
做两件事:
• 遍历所有的 children
• 关闭 done 这个 channel:这个符合谁创 建谁关闭的原则
源码如下
// cancel closes c.done, cancels each of c's children, and, if
// removeFromParent is true, removes c from its parent's 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
d, _ := c.done.Load().(chan struct)
if d == nil
c.done.Store(closedchan)
else
close(d)
for child := range c.children
// NOTE: acquiring the child's lock while holding parent's lock.
child.cancel(false, err)
c.children = nil
c.mu.Unlock()
if removeFromParent
removeChild(c.Context, c)
timerCtx
是装饰器模式:在已有 cancelCtx 的 基础上增加了超时的功能。
实现要点:
- WithTimeout 和 WithDeadline 本质一样
- WithDeadline 里面,在创建 timerCtx 的时候利用 time.AfterFunc 来实现超时
context包使用注意事项
• 一般只用做方法参数,而且是作为第一个参数;
• 所有公共方法,除非是 util,helper 之类的方法,否则都加上 context 参数;
• 不要用作结构体字段,除非你的结构体本身也是表达一个上下文的概念。
以上是关于go实战学习——context包学习理解笔记的主要内容,如果未能解决你的问题,请参考以下文章