Golang:context基于go1.21版本
Context 是什么
- 译为上下文
- 用于进程之间信息和信号的传递
- 用于服务之间的信号和信息从传递
Context 的功能
- Context 可以用于
不同的api或者进程之间传递(
携带键值对传递),不要传递 nil Context
- 传递取消信号(主动取消,超时/时限取消),因为Context是树结构,所以传递是
单向传递的
,只有父节点取消的时候,才会把取消的信号传递给父节点的衍生子节点 - Context需要显示传递给他所需要的函数,并且需要他的函数作为第一个参数,通常命名为
ctx
应用场景
- 用于父子协程间取消信号传递
- 用于客服端与服务器之间的信息传递
- 用于设置请求超时时间等
源码分析
1.10 Context核心结构
本文都是围绕一下进行讨论的,无非就是实现这四个方法的逻辑不同。方法的具体实现,将会在下面具体讲解,现在先让大家对Context有个宏观的认知
- 一个接口:
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{
}
Err() error
Value(key any) any
}
Deadline()
: 返回一个完成工作的截止时间,这个截止时间代表上下文被取消的时间,这个时间之后所有的衍生协程都将会取消。如果没有截止时间,将会返回falseDone()
:返回一个Context中的channel,这个channel会在当前任务完成工作的时候关闭,如果关闭的时候无法取消上下文,则Done可能返回nil。多次调用Done方法会返回同一个channel。Err()
:返回Context结束的原因,他只会在Done
方法对应的Channel关闭的时候关闭返回非空值,如果context被取消,会返回context.Canceled
如果Context超时,会返回Context.DeadlineExceeded
错误Value()
:从Context从获取对应的键值对,如果未设置键值对则返回nil
。相同的key取value会返回相同的值。因为这四个方法都是幂等
的
- 四个实现:
emptyCtx ()
: 实现了一个空的context用作根节点,TODO(),Background(),都是基于他实现的cancelCtx ()
: 实现一个能够自己取消的ContexttimerCtx ()
: 实现一个通过定时器timer和截止时间deadline定时取消的contextvalueCtx ()
: 实现一个可以通过 key、val 两个字段来存数据的context
- 十个方法:
Background ()
: Background 返回一个emptyCtx作为根节点TODO ()
: TODO 返回一个emptyCtx作为未知节点,如果你不知道使用哪个Context可以使用此方法WithCancel()
: WithCancel 入参是一个父Context,return一个子Context和cancelCtx(主动取消Context),如果想取消Context可以调用cancelCtx()WithDeadline ()
: WithDeadline 入参是一个父Context和一个过期时间, 返回一个子Context和timerCtx,如果时间超时,那么他会自动的调用timerCtx()方法WithTimeout ()
: WithTimeout 入参是一个父Context和一段时间, 返回一个子Context和timerCtx,如果传入的是3秒,那么3秒后,他会自动的调用timerCtx()方法WithCancelCause()
: WithCancelCause,和WithCancel不同点就是他可以自定义error而WithCancel不能WithDeadlineCause()
: 同上WithTimeoutCause()
: 同上AfterFunc()
: AfterFunc入参是一个Context和一个func(),在该Context取消的时候,会执行该func。可以解决以往的一些合并取消上下文和串联处理的复杂场景。AfterFunc 安排在 ctx 完成后在自己的 goroutine 中调用 f(取消或超时)。如果 ctx 已经完成,AfterFunc 会立即在自己的 goroutine 中调用 f。WithoutCancel()
: WithoutCance返回一个父级Context的副本,即使父Context关闭,生成的父Contest的副本也不会关闭。
1.1Err错误
从下面的err可以看出,有两个错误方法,Canceled
:Context取消时返回的err,DeadlineExceeded
:Context截止时间超时的err
// Canceled is the error returned by [Context.Err] when the context is canceled.
var Canceled = errors.New("context canceled")
// DeadlineExceeded is the error returned by [Context.Err] when the context's
// deadline passes.
var DeadlineExceeded error = deadlineExceededError{
}
type deadlineExceededError struct{
}
func (deadlineExceededError) Error() string {
return "context deadline exceeded" }
func (deadlineExceededError) Timeout() bool {
return true }
func (deadlineExceededError) Temporary() bool {
return true }
2 .1 emptyCtx
字如其名就是一个空的Context,无法被取消,没有值,也没有超时时间。
// An emptyCtx is never canceled, has no values, and has no deadline.
// It is the common base of backgroundCtx and todoCtx.
type emptyCtx struct{
}
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 any) any {
return nil
}
Deadline()
: 方法会返回一个公元元年的时间以及false的flag。表示这个Context没有超时时间
Done()
: 方法返回一个nil,如果用户读取或者写入
这个Channel,会造成阻塞Err(
: Err的错误返回一个nil,证明这个方法不会有err
Value
: Value值返回nil,证明这个方法不会有value
3.1 Background() 和 TODO()
从源码可以看出这两个方法其实都是返回的emptyCtx 的一个实例,只是他们的表达方式不一样,他们两个继承
了emptyCtx 所有的特点,,无法被取消,没有值,也没有超时时间。
Background():上下文的默认值,相当于root
,所有的Context都应该从这里衍生出来,一般会在main中作为最顶层的Context
TODO():Todo就是表示这个地方还没有完成,代表目前还不知道用哪个Context传递,或者根本没有Context,但是业务又需要传递Context,这时TODO就派上用场了
type backgroundCtx struct{
emptyCtx }
func (backgroundCtx) String() string {
return "context.Background"
}
type todoCtx struct{
emptyCtx }
func (todoCtx) String() string {
return "context.TODO"
}
func Background() Context {
return backgroundCtx{
}
}
func TODO() Context {
return todoCtx{
}
}
4.1.1 cancelCtx
cancelCtx 是一个能够被取消的Context,其中他实现了Context接口,他是一个匿名字段,可以看成Context.
type canceler interface {
cancel(removeFromParent bool, err, cause error)
Done() <-chan struct{
}
}
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
cause error // set to non-nil by the first cancel call
}
Context
:嵌入了一个Context作为父Context,表示cancelCtx 肯定是某一个Context的子节点mu
:互斥锁,用来保证并发安全done
:实际的类型是chan struct{}
,用来反应cancelCtx 生命周期的通道,如果是cancelCtx 关闭,那么就会向done发送信号,用来表示传递关闭信号。其中内部是使用atomic.Value
实现的,会读取上次最近存储的chan struct{}children
:指向 cancelCtx 的所有子 context,在第一次调用的时候为nilerr
:记录了当前 cancelCtx 的错误.cause
:自定义err,返回自定义的cancelCtx 错误
4.1.2 Deadline 方法
cancelCtx 未实现该方法,仅是 嵌入了一个带有 Deadline 方法的 Context interface,因此倘若直接调用会报错.
4.1.2 Done 方法
Done返回一个只读的通道,当Context取消的时候,该通道就会关闭,你可以监听这个通道检测Context是否被关闭,将done初始化。
- 检查atomic.Value
是否存储了chan struct{}
,如果存储的有就返回原来的实例化的chan struct{} - 如果atomic.Value没有存储,就加锁,然后在判断一下是否有没有存储(双重保险),没有了在进行实例化,最后返回实例化后的chan struct{}
func (c *cancelCtx) Done() <-chan struct{
} {
d := c.done.Load()
if d != nil {
return d.(chan struct{
})
}
c.mu.Lock()
defer c.mu.Unlock()
d = c.done.Load()
if d == nil {
d = make(chan struct{
})
c.done.Store(d)
}
return d.(chan struct{
})
}
4.1.2 Err 方法
这个就是读取cancelCtx的err,然后进行返回
func (c *cancelCtx) Err() error {
c.mu.Lock()
err := c.err
c.mu.Unlock()
return err
}
4.1.2 Value 方法
倘若 key 特定值 &cancelCtxKey,则返回 cancelCtx 自身的指针,否则就会取值return
func (c *cancelCtx) Value(key any) any {
if key == &cancelCtxKey {
return c
}
return value(c.Context, key)
}
了解了cancelCtx 结构之后进入真正的主题
4.2 WithCancel() 和 WithCancelCause()
WithCancel():返回的是一个继承了父Context
的新Context(cancelCtx),和一个CancelFunc方法,如果调用CancelFunc,这个context 将会取消。
WithCancelCause():返回的内容一样,就是CancelFunc方法多了一个自定义的err。下面是使用示例,myError是自定义的err。
ctx, cancel := context.WithCancelCause(parent)
cancel(myError)
//返回myError
context.Cause(ctx)
type CancelFunc func()
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
c := withCancel(parent)
return c, func() {
c.cancel(true, Canceled, nil) }
}
type CancelCauseFunc func(cause error)
func WithCancelCause(parent Context) (ctx Context, cancel CancelCauseFunc) {
c := withCancel(parent)
return c, func(cause error) {
c.cancel(true, Canceled, cause) }
}
func withCancel(parent Context) *cancelCtx {
if parent == nil {
panic("cannot create context from nil parent")
}
c := &cancelCtx{
}
c.propagateCancel(parent, c)
return c
}
- 先判断父Context是否为nil,如果为nil直接panic,然后在重新实例化一个子context
- 在 propagateCancel 方法内启动一个守护协程,以保证父 context 终止时,该 cancelCtx 也会被终止
4.2.2 propagateCancel
如果父context取消了,那么他的所有衍生context都应该被取消
func (c *cancelCtx) propagateCancel(parent Context, child canceler) {
c.Context = parent
done := parent.Done()
if done == nil {
return // parent is never canceled
}
select {
case <-done:
// parent is already canceled
child.cancel(false, parent.Err(), Cause(parent))
return
default:
}
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
}
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():
}
}()
}
c.Context = parent,把该context指向父context,这样就知道父context有哪些子context了
判断父context是否为不可取消类型,如果是,就跳过,如果不是就监听父context是否已经取消了,如果是取消了就把该子context取消。并把父err,传递给子err
如果parent 是 cancelCtx 的类型,则加锁,并将子 context 添加到 parent 的 children map 当中,假如以后父context取消,可以直接用children map 取消所有的子context
如果parent 实现了afterFuncer接口,把cancel函数封装成一个stop函数,进行延迟取消。赋值给子context的结构体中。后续会调用这个afterFuncer
如果 parent 没有实现afterFuncer接口,则启动一个协程,通过多路复用的方式监控 parent 状态,如果终止,则同时终止子 context,并把parent 的 err传递给子context,如果孩子终止则直接跳过,因为不影响parent 。这里启动协程是主要监听parent,如果parent取消则立即取消子context。
4.2.3 cancelCtx.cancel()
在propagateCancel守护协程中,知道了怎么终止协程cancel(false, parent.Err(), Cause(parent))。但是你知道怎么实现的吗?来看源代码
func (c *cancelCtx) cancel(removeFromParent bool, err, cause error) {
if err == nil {
panic("context: internal error: missing cancel error")
}
if cause == nil {
cause = err
}
c.mu.Lock()
if c.err != nil {
c.mu.Unlock()
return // already canceled
}
c.err = err
c.cause = cause
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, cause)
}
c.children = nil
c.mu.Unlock()
if removeFromParent {
removeChild(c.Context, c)
}
}
- 可以看到cancel需要三个入参,一个是是否需要从父map[canceler]struct{}剔除,如果为true就剔除。第二个是取消context的错误是必传的,第三个是自定义err。
- 如果err为nil直接panic,检查cancelCtx的err是否已经为空,如果不为空则解锁return,为空就把入参赋值给err和cause
- 加载cancelCtx 的 channel,如果没有初始化,则初始化一个closedchan,反之就关闭Channel。遍历当前cancelCtx的children 把他的衍生孩子都进行取消。然后把children 清空赋值为nil。
- 下面是removeChild的方法,先判断parent是否实现了stopCtx结构体,如果实现了就证明它实现了afterFunction这个接口,需要在移除的时候,运行在propagateCancel中添加的stopCtx.stop方法。
- 如果parent不是cancelCtx,那么就返回,只有cancelCtx才有children ,如果是cancelCtx类型,就把children map中的child 删除,最后解锁返回
func removeChild(parent Context, child canceler) {
if s, ok := parent.(stopCtx); ok {
s.stop()
return
}
p, ok := parentCancelCtx(parent)
if !ok {
return
}
p.mu.Lock()
if p.children != nil {
delete(p.children, child)
}
p.mu.Unlock()
}
type timerCtx struct {
cancelCtx
timer *time.Timer // Under cancelCtx.mu.
deadline time.Time
}
4.3 WithTimeout (),WithDeadline(),WithDeadlineCause(),WithTimeoutCause()
因为这四个方法实现的逻辑都基本是一样的,下面将会讲解怎么实现具体逻辑
4.3.1 timerCtx 结构
timerCtx 在 cancelCtx 基础上又做了一层封装,他继承了cancelCtx结构体,实现了cancelCtx的所有能力,然后外加了一个定时取消的Context,和到截止时间取消的Context
type timerCtx struct {
cancelCtx
timer *time.Timer // Under cancelCtx.mu.
deadline time.Time
}
4.3.2 Deadline结构
func (c *timerCtx) Deadline() (deadline time.Time, ok bool) {
return c.deadline, true
}
在之前讲解的 WithCancel() 和 WithCancelCause()中,都没有实现Deadline()这个方法,这个方法用于展示过期时间
4.3.3 cancel结构
因为他继承了cancelCtx,所以可以之间调用cancelCtx.cancel,进行取消操作,然后判断是否需要把孩子context从map中删除,然后停止timer
func (c *timerCtx) cancel(removeFromParent bool, err, cause error) {
c.cancelCtx.cancel(false, err, cause)
if removeFromParent {
// Remove this timerCtx from its parent cancelCtx's children.
removeChild(c.cancelCtx.Context, c)
}
c.mu.Lock()
if c.timer != nil {
c.timer.Stop()
c.timer = nil
}
c.mu.Unlock()
}
4.3.4 String结构
这个是把context进行name的拼接,调用这个方法会把context的名字return
func (c *timerCtx) String() string {
return contextName(c.cancelCtx.Context) + ".WithDeadline(" +
c.deadline.String() + " [" +
time.Until(c.deadline).String() + "])"
}
4.4.1 WithDeadline 结构
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
return WithDeadlineCause(parent, d, nil)
}
// WithDeadlineCause behaves like [WithDeadline] but also sets the cause of the
// returned Context when the deadline is exceeded. The returned [CancelFunc] does
// not set the cause.
func WithDeadlineCause(parent Context, d time.Time, cause error) (Context, CancelFunc) {
if parent == nil {
panic("cannot create context from nil parent")
}
if cur, ok := parent.Deadline(); ok && cur.Before(d) {
// The current deadline is already sooner than the new one.
return WithCancel(parent)
}
c := &timerCtx{
deadline: d,
}
c.cancelCtx.propagateCancel(parent, c)
dur := time.Until(d)
if dur <= 0 {
c.cancel(true, DeadlineExceeded, cause) // deadline has already passed
return c, func() {
c.cancel(false, Canceled, nil) }
}
c.mu.Lock()
defer c.mu.Unlock()
if c.err == nil {
c.timer = time.AfterFunc(dur, func() {
c.cancel(true, DeadlineExceeded, cause)
})
}
return c, func() {
c.cancel(true, Canceled, nil) }
}
- 从代码可以看到WithDeadline其实内部就是调用的WithDeadlineCause,只不过他的自定义err为nil。
- 检查parent 是否为nil,校验 parent 的过期时间是否早于自己,如果是就返回 WithCancel(parent),然后构造一个timerCtx,启用守护进程,判断过期的时间是否已经到了,如果到则取消context,判断context的err是否为nil,如果为nil则
设定一个延时时间
,到过期时间的时候会终止该 timerCtx,并返回 的错误。最后返回一个timerCtx和cancel函数