context
包的作用
context
包是在go1.7版本中引入到标准库中的
context
可以用来在goroutine
之间传递上下文信息,相同的context
可以传递给运行在不同goroutine
中的函数,上下文对于多个goroutine
同时使用是安全的,context
包定义了上下文类型,可以使用background
、TODO
创建一个上下文,在函数调用链之间传播context
,也可以使用WithDeadline
、WithTimeout
、WithCancel
或 WithValue
创建的修改副本替换它,听起来有点绕,其实总结起就是一句话:context的作用就是在不同的goroutine之间同步请求特定的数据、取消信号以及处理请求的截止日期。
context
包概述
创建context
context
包主要提供了两种方式创建context
:
context.Backgroud()
context.TODO()
这两种方式其实并没有什么差别,只是互为别名,官方给的定义是:
context.Background
是上下文的默认值,所有其他的上下文都应该从它衍生(Derived)出来。context.TODO
应该只在不确定应该使用哪种上下文时使用;
在大多数情况下,我们都使用context.Backgroud()
作为起始的上下文传递
上面的两种方式是创建根context
,不具备任何功能,具体实践还是要依靠context
包提供的With系列函数来进行派生:
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
func WithValue(parent Context, key, val interface{}) Context
context
包源码剖析
Context
抽象接口
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key any) any
}
deadline
:
Deadline 返回代表此上下文完成的工作应该被取消的时间。 当没有设置截止日期时,截止日期返回 ok==false。 对 Deadline 的连续调用返回相同的结果。
Done
:
Done 返回一个通道,当应该取消代表此上下文完成的工作时,该通道将关闭。 如果此上下文永远无法取消,则 Done 可能会返回 nil。 对 Done 的连续调用返回相同的值。
- WithCancel 安排在调用 cancel 时关闭 Done;
- WithDeadline 安排在截止日期到期时关闭 Done;
- WithTimeout 安排在超时结束时关闭 Done。
提供 Done 以供在 select 语句中使用:
Stream 使用 DoSomething 生成值并将它们发送到 out,直到 DoSomething 返回错误或 ctx.Done 关闭。
func Stream(ctx context.Context, out chan<- Value) error {
for {
v, err := DoSomething(ctx)
if err != nil {
return err
}
select {
case <-ctx.Done():
return ctx.Err()
case out <- v:
}
}
}
Err
如果 Done 尚未关闭,Err 返回 nil。
如果 Done 关闭,Err 返回一个非零错误来解释原因:
如果上下文被取消,则为取消; 如果上下文的最后期限已过,则为 DeadlineExceded。
在 Err 返回非零错误后,对 Err 的连续调用返回相同的错误。
这个接口主要是被是被三个类继承实现的,分别是emptyCtx
、ValueCtx
、cancelCtx
,采用匿名接口的写法,这样可以对任意实现了该接口的类型进行重写。
emptyCtx
类
emptyCtx
是一个空Context
,主要充当根context
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 any) any {
return nil
}
func (e *emptyCtx) String() string {
switch e {
case background:
return "context.Background"
case todo:
return "context.TODO"
}
return "unknown empty Context"
}
里面的各种接口,都是空实现
context.Backgroud()
,context.TODO()
,就是两个emotyCtx
var (
background = new(emptyCtx)
todo = new(emptyCtx)
)
func Background() Context {
return background
}
func TODO() Context {
return todo
}
这background
和todo
是两个单例对象,实现方法是一模一样的
valueCtx
类
valueCtx
目的就是为Context
携带数据,他会继承父Context
type valueCtx struct {
Context
key, val any
}
valueCtx
类的创建,下面带代码的实现看起来十分简单,就是返回一个valueCtx
实例
func WithValue(parent Context, key, val any) Context {
if parent == nil {
panic("cannot create context from nil parent")
}
if key == nil {
panic("nil key")
}
if !reflectlite.TypeOf(key).Comparable() {
panic("key is not comparable")
}
return &valueCtx{parent, key, val}
}
该类实现了String方法输出context和携带的键值对信息
func stringify(v any) string {
switch s := v.(type) {
case stringer:
return s.String()
case string:
return s
}
return "<not Stringer>"
}
func (c *valueCtx) String() string {
return contextName(c.Context) + ".WithValue(type " +
reflectlite.TypeOf(c.key).String() +
", val " + stringify(c.val) + ")"
}
该类还提供获取value的Value
方法
func (c *valueCtx) Value(key any) any {
if c.key == key {
return c.val
}
return value(c.Context, key)
}
func value(c Context, key any) any {
for {
switch ctx := c.(type) {
case *valueCtx:
if key == ctx.key {
return ctx.val
}
c = ctx.Context
case *cancelCtx:
if key == &cancelCtxKey {
return c
}
c = ctx.Context
case *timerCtx:
if key == &cancelCtxKey {
return &ctx.cancelCtx
}
c = ctx.Context
case *emptyCtx:
return nil
default:
return c.Value(key)
}
}
}
该方法的实现就是从树的最底层向上找,直到找到或者到达根Context为止,context树形结构如下
以ctx1.1为例,现在ctex1.1找,找不到,会往ctx1.0找,再找不到就会到达根context返回err
cancelCtx
类
cnacelCtx
也会继承父context
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
}
mu
:保护下面的成员done
:用来通知context的取消信号,采用的原子操作children
:存放当前接口的字节点,当根节点发生取消时,所有子节点也需要取消err
:用来存放取消context时的信息- 通过
newCancelCtx
方法创建cancelCtx实例对象
func newCancelCtx(parent Context) cancelCtx {
return cancelCtx{Context: parent}
}
该类也实现了Valie方法,与上面类似具体细节不过多赘述
实现了done方法,返回一个单向管道,当应该取消代表此上下文完成的工作时,该通道将关闭。 如果此上下文永远无法取消,则 Done 可能会返回 nil。 对 Done 的连续调用返回相同的值。
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{})
}
propagateCancel
方法用来构建父子节点之间的关系
func propagateCancel(parent Context, child canceler) {
//如果返回的是nil说明父节点不会被取消,就不用管,直接返回就可以
done := parent.Done()
if done == nil {
return // parent is never canceled
}
//判断要构建关联的节点是否被取消,被取消也就不需要构建关联了,该节点直接取消
//节点可能是timeCtx和cancelCtx
select {
case <-done:
// parent is already canceled
child.cancel(false, parent.Err())
return
default:
}
//这里的目的是去寻找可以构建关联的的context,可能是父节点,祖父节点,曾祖父节点.....
if p, ok := parentCancelCtx(parent); ok {
p.mu.Lock()
if p.err != nil {
// 构建关联的Context已经被取消,所以不需要再构建关联,子节点直接取消
child.cancel(false, p.err)
} else {
//初始化children节点的childrenMap
if p.children == nil {
p.children = make(map[canceler]struct{})
}
//将该节点添加到父节点children当中
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():
}
}()
}
}
cancel
方法来取消子节点
// 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
//关闭chan,通知其他协程
d, _ := c.done.Load().(chan struct{})
if d == nil {
c.done.Store(closedchan)
} else {
close(d)
}
//遍历每一个children,取消所有孩子节点
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()
//如果为true,则从父节点中将该节点移除
if removeFromParent {
removeChild(c.Context, c)
}
}
timerCtx
类
timerCtx
基于 cancelCtx
,继承了cancelCtx
只是多了一个 time.Timer
和一个 deadline
。Timer 会在 deadline
到来时,自动取消 context。
type timerCtx struct {
cancelCtx
timer *time.Timer // Under cancelCtx.mu.
deadline time.Time
}
先看WithTimeout
方法,它内部就是调用的WithDeadline
方法:
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
return WithDeadline(parent, time.Now().Add(timeout))
}
下面看看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) {
// The current deadline is already sooner than the new one.
return WithCancel(parent)
}
//创建timerCtx实例
c := &timerCtx{
cancelCtx: newCancelCtx(parent),
deadline: d,
}
//与父节点构建关联
propagateCancel(parent, c)
//判断截止时间是否合理(是否在当前时间之前)
dur := time.Until(d)
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 {
//增加定时器,定时去取消
c.timer = time.AfterFunc(dur, func() {
c.cancel(true, DeadlineExceeded)
})
}
return c, func() { c.cancel(true, Canceled) }
}
timerCtx
实现的cancel方法,内部也是调用了cancelCtx
的cancel
方法取消:
func (c *timerCtx) cancel(removeFromParent bool, err error) {
c.cancelCtx.cancel(false, err)
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()
}
以上就是Context包的大致内容
context
的优缺点
缺点 :
- 让代码变得十分丑陋,可读性变差,因为我们不得不将context作为一个参数,进行传递
- context取消和自动取消的错误返回不够友好,无法自定义错误,出现难以排查的问题时不好排查。
- 衍生的context就像一个个链表节点,当节点过多时,会影响代码效率
优点:
- 可以更好的,更方便的管理协程