Go Contex

Go Contex

Contex

context包能够提供一个请求从API请求边界到各goroutine的请求域数据传递、取消信号及截至时间等能力。详细原理请看下文。

在 Go 语言中 context 包容许您传递一个 “context” 到您的程序。Context 如超时或截止日期(deadline)或通道,来指示中止运行和返回。例如,若是您正在执行一个 web 请求或运行一个系统命令,定义一个超时对生产级系统一般是个好主意。由于,若是您依赖的API运行缓慢,你不但愿在系统上备份(back up)请求,由于它可能最终会增长负载并下降全部请求的执行效率。致使级联效应。这是超时或截止日期 context 派上用场的地方。

设计原理

Go 语言中的每个请求的都是经过一个单独的 Goroutine 进行处理的,HTTP/RPC 请求的处理器每每都会启动新的 Goroutine 访问数据库和 RPC 服务,咱们可能会建立多个 Goroutine 来处理一次请求,而 Context 的主要做用就是在不一样的 Goroutine 之间同步请求特定的数据、取消信号以及处理请求的截止日期。

image-20211031173219424

每个 Context 都会从最顶层的 Goroutine 一层一层传递到最下层,这也是 Golang 中上下文最多见的使用方式,若是没有 Context,当上层执行的操做出现错误时,下层其实不会收到错误而是会继续执行下去。

image-20211031173256610

当最上层的 Goroutine 由于某些缘由执行失败时,下两层的 Goroutine 因为没有接收到这个信号因此会继续工做;可是当咱们正确地使用 Context 时,就能够在下层及时停掉无用的工做减小额外资源的消耗:

image-20211031173507293

这其实就是 Golang 中上下文的最大做用,在不一样 Goroutine 之间对信号进行同步避免对计算资源的浪费,与此同时 Context 还能携带以请求为做用域的键值对信息。

接口

type Context interface {
 Deadline() (deadline time.Time, ok bool)
 Done() <-chan struct{}
 Err() error
 Value(key interface{}) interface{}
}

context.Context 是 Go 语言在 1.7 版本中引入标准库的接口1,该接口定义了四个须要实现的方法,其中包括:

  • Deadline — 返回 context.Context 被取消的时间,也就是完成工做的截止日期;

  • Done — 返回一个 Channel,这个 Channel 会在当前工做完成或者上下文被取消以后关闭,屡次调用 Done 方法会返回同一个 Channel;

  • Err — 返回 context.Context 结束的缘由,它只会在 Done 返回的 Channel 被关闭时才会返回非空的值;

    • 若是 context.Context 被取消,会返回 Canceled 错误;
    • 若是 context.Context 超时,会返回 DeadlineExceeded 错误;
  • Value — 从 context.Context 中获取键对应的值,对于同一个上下文来讲,屡次调用 Value 并传入相同的 Key 会返回相同的结果,该方法能够用来传递请求特定的数据;

使用

建立context

context包容许如下方式建立和得到context:

  • context.Background():这个函数返回一个空 context。这只能用于高等级(在 main 或顶级请求处理中)。
  • context.TODO():这个函数也是建立一个空 context。也只能用于高等级或当您不肯定使用什么 context,或函数之后会更新以便接收一个 context 。这意味您(或维护者)计划未来要添加 context 到函数。

其实咱们查看源代码。发现他俩都是经过 new(emptyCtx) 语句初始化的,它们是指向私有结构体 context.emptyCtx 的指针,这是最简单、最经常使用的上下文类型:

var (
 background = new(emptyCtx)
 todo       = new(emptyCtx)
)
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
}

func (e *emptyCtx) String() string {
 switch e {
 case background:
  return "context.Background"
 case todo:
  return "context.TODO"
 }
 return "unknown empty Context"
}

从上述代码,咱们不难发现 context.emptyCtx 经过返回 nil 实现了 context.Context 接口,它没有任何特殊的功能。

从源代码来看,context.Backgroundcontext.TODO 函数其实也只是互为别名,没有太大的差异。它们只是在使用和语义上稍有不一样:

  • context.Background 是上下文的默认值,全部其余的上下文都应该从它衍生(Derived)出来。
  • context.TODO 应该只在不肯定应该使用哪一种上下文时使用;

在多数状况下,若是当前函数没有上下文做为入参,咱们都会使用 context.Background 做为起始的上下文向下传递。

context的继承衍生

有了如上的根Context,那么是如何衍生更多的子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

这四个With函数,接收的都有一个partent参数,就是父Context,咱们要基于这个父Context建立出子Context的意思,这种方式能够理解为子Context对父Context的继承,也能够理解为基于父Context的衍生。

经过这些函数,就建立了一颗Context树,树的每一个节点均可以有任意多个子节点,节点层级能够有任意多个。

WithCancel函数,传递一个父Context做为参数,返回子Context,以及一个取消函数用来取消Context。WithDeadline函数,和WithCancel差很少,它会多传递一个截止时间参数,意味着到了这个时间点,会自动取消Context,固然咱们也能够不等到这个时候,能够提早经过取消函数进行取消。

WithTimeoutWithDeadline基本上同样,这个表示是超时自动取消,是多少时间后自动取消Context的意思。

WithValue函数和取消Context无关,它是为了生成一个绑定了一个键值对数据的Context,这个绑定的数据能够经过Context.Value方法访问到

你们可能留意到,前三个函数都返回一个取消函数CancelFunc,这是一个函数类型,它的定义很是简单。

type CancelFunc func()

这就是取消函数的类型,该函数能够取消一个Context,以及这个节点Context下全部的全部的Context,无论有多少层级。

WithValue

context 包中的 context.WithValue 函数能从父上下文中建立一个子上下文,传值的子上下文使用 context.valueCtx 类型,咱们看一下源码:

func WithValue(parent Context, key, val interface{}) Context {
 if key == nil {
  panic("nil key")
 }
 if !reflectlite.TypeOf(key).Comparable() {
  panic("key is not comparable")
 }
 return &valueCtx{parent, key, val}
}
type valueCtx struct {
    Context
    key, val interface{}
}

func (c *valueCtx) Value(key interface{}) interface{} {
    if c.key == key {
        return c.val
    }
    return c.Context.Value(key)
}

函数接收 context 并返回派生 context,其中值 val 与 key 关联,并经过 context 树与 context 一块儿传递。这意味着一旦得到带有值的 context,从中派生的任何 context 都会得到此值。

里添加键值对不是在原context结构体上直接添加,而是以此context作为父节点,重新创建一个新的valueCtx子节点,将键值对添加在子节点上,由此形成一条context链。获取value的过程就是在这条context链上由尾部上前搜寻:

image-20211031175223944

context.valueCtx 结构体会将除了 Value 以外的 ErrDeadline 等方法代理到父上下文中,它只会响应 context.valueCtx.Value 方法。若是 context.valueCtx 中存储的键值对与 context.valueCtx.Value 方法中传入的参数不匹配,就会从父上下文中查找该键对应的值直到在某个父上下文中返回 nil 或者查找到对应的值。

举例

type key string

func main()  {
 ctx := context.WithValue(context.Background(),key("asong"),"Golang梦工厂")
 Get(ctx,key("asong"))
 Get(ctx,key("song"))
}

func Get(ctx context.Context,k key)  {
 if v, ok := ctx.Value(k).(string); ok {
  fmt.Println(v)
 }
}

WithCancel

cancelCtx

cancelCtx结构体

type cancelCtx struct {
    Context

    mu       sync.Mutex            // protects following fields
    done     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
}
type canceler interface {
    cancel(removeFromParent bool, err error)
    Done() <-chan struct{}
}

valueCtx类似,cancelCtx中也有一个context变量作为父节点;变量done表示一个channel,用来表示传递关闭信号;children表示一个map,存储了当前context节点下的子节点;err用于存储错误信息表示任务结束的原因。

cancelCtx实现的方法:

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
    //设置一个关闭的channel或者将done channel关闭,用以发送关闭信号
    if c.done == nil {
        c.done = closedchan
    } else {
        close(c.done)
    }
    // 将子节点context依次取消
    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 {
        // 将当前context节点从父节点上移除
        removeChild(c.Context, c)
    }
}

WithCancel

WithCancel函数用来创建一个可取消的context,即cancelCtx类型的contextWithCancel返回一个context和一个CancelFunc,调用CancelFunc即可触发cancel操作。

type CancelFunc func()

func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
    c := newCancelCtx(parent)
    propagateCancel(parent, &c)
    return &c, func() { c.cancel(true, Canceled) }
}

// newCancelCtx returns an initialized cancelCtx.
func newCancelCtx(parent Context) cancelCtx {
    // 将parent作为父节点context生成一个新的子节点
    return cancelCtx{Context: parent}
}

func propagateCancel(parent Context, child canceler) {
    if parent.Done() == nil {
        // parent.Done()返回nil表明父节点以上的路径上没有可取消的context
        return // parent is never canceled
    }
    // 获取最近的类型为cancelCtx的祖先节点
    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{})
            }
            // 将当前子节点加入最近cancelCtx祖先节点的children中
            p.children[child] = struct{}{}
        }
        p.mu.Unlock()
    } else {
        go func() {
            select {
            case <-parent.Done():
                child.cancel(false, parent.Err())
            case <-child.Done():
            }
        }()
    }
}

func parentCancelCtx(parent Context) (*cancelCtx, bool) {
    for {
        switch c := parent.(type) {
        case *cancelCtx:
            return c, true
        case *timerCtx:
            return &c.cancelCtx, true
        case *valueCtx:
            parent = c.Context
        default:
            return nil, false
        }
    }
}

之前说到cancelCtx取消时,会将后代节点中所有的cancelCtx都取消,propagateCancel即用来建立当前节点与祖先节点这个取消关联逻辑。

  1. 如果parent.Done()返回nil,表明父节点以上的路径上没有可取消的context,不需要处理;
  2. 如果在context链上找到到cancelCtx类型的祖先节点,则判断这个祖先节点是否已经取消,如果已经取消就取消当前节点;否则将当前节点加入到祖先节点的children列表。
  3. 否则开启一个协程,监听parent.Done()child.Done(),一旦parent.Done()返回的channel关闭,即context链中某个祖先节点context被取消,则将当前context也取消。

context.propagateCancel 的做用是在 parentchild 之间同步取消和结束的信号,保证在 parent 被取消时,child 也会收到对应的信号,不会发生状态不一致的问题。

image-20211031200351280

func main()  {
 ctx,cancel := context.WithCancel(context.Background())
 defer cancel()
 go Speak(ctx)
 time.Sleep(10*time.Second)
}

func Speak(ctx context.Context)  {
 for range time.Tick(time.Second){
  select {
  case <- ctx.Done():
   return
  default:
   fmt.Println("balabalabalabala")
  }
 }
}

咱们使用withCancel建立一个基于Background的ctx,而后启动一个讲话程序,每隔1s说一话,main函数在10s后执行cancel,那么speak检测到取消信号就会退出。

timerCtx

timerCtx是一种基于cancelCtxcontext类型,从字面上就能看出,这是一种可以定时取消的context

type timerCtx struct {
    cancelCtx
    timer *time.Timer // Under cancelCtx.mu.

    deadline time.Time
}

func (c *timerCtx) Deadline() (deadline time.Time, ok bool) {
    return c.deadline, true
}

func (c *timerCtx) cancel(removeFromParent bool, err error) {
    将内部的cancelCtx取消
    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()
}

timerCtx内部使用cancelCtx实现取消,另外使用定时器timer和过期时间deadline实现定时取消的功能。timerCtx在调用cancel方法,会先将内部的cancelCtx取消,如果需要则将自己从cancelCtx祖先节点上移除,最后取消计时器。

WithDeadline

WithDeadline返回一个基于parent的可取消的context,并且其过期时间deadline不晚于所设置时间d

func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
    if cur, ok := parent.Deadline(); ok && cur.Before(d) {
        // The current deadline is already sooner than the new one.
        return WithCancel(parent)
    }
    c := &timerCtx{
        cancelCtx: newCancelCtx(parent),
        deadline:  d,
    }
    // 建立新建context与可取消context祖先节点的取消关联关系
    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) }
}
  1. 如果父节点parent有过期时间并且过期时间早于给定时间d,那么新建的子节点context无需设置过期时间,使用WithCancel创建一个可取消的context即可;
  2. 否则,就要利用parent和过期时间d创建一个定时取消的timerCtx,并建立新建context与可取消context祖先节点的取消关联关系,接下来判断当前时间距离过期时间d的时长dur
  3. 如果dur小于0,即当前已经过了过期时间,则直接取消新建的timerCtx,原因为DeadlineExceeded
  4. 否则,为新建的timerCtx设置定时器,一旦到达过期时间即取消当前timerCtx

WithTimeout

WithDeadline类似,WithTimeout也是创建一个定时取消的context,只不过WithDeadline是接收一个过期时间点,而WithTimeout接收一个相对当前时间的过期时长timeout:

func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
    return WithDeadline(parent, time.Now().Add(timeout))
}

接下来咱们来看一个例子:

func main()  {
 now := time.Now()
 later,_:=time.ParseDuration("10s")

 ctx,cancel := context.WithDeadline(context.Background(),now.Add(later))
 defer cancel()
 go Monitor(ctx)

 time.Sleep(20 * time.Second)

}

func Monitor(ctx context.Context)  {
 select {
 case <- ctx.Done():
  fmt.Println(ctx.Err())
 case <-time.After(20*time.Second):
  fmt.Println("stop monitor")
 }
}go

设置一个监控goroutine,使用WithTimeout建立一个基于Background的ctx,其会当前时间的10s后取消。验证结果以下:

context deadline exceeded

Context使用原则

  • context.Background 只应用在最高等级,做为全部派生 context 的根。
  • context 取消是建议性的,这些函数可能须要一些时间来清理和退出。
  • 不要把Context放在结构体中,要以参数的方式传递。
  • Context做为参数的函数方法,应该把Context做为第一个参数,放在第一位。
  • 给一个函数方法传递Context的时候,不要传递nil,若是不知道传递什么,就使用context.TODO
  • Context的Value相关方法应该传递必须的数据,不要什么数据都使用这个传递。context.Value 应该不多使用,它不该该被用来传递可选参数。这使得 API 隐式的而且能够引发错误。取而代之的是,这些值应该做为参数传递。
  • Context是线程安全的,能够放心的在多个goroutine中传递。同一个Context能够传给使用其的多个goroutine,且Context可被多个goroutine同时安全访问。
  • Context 结构没有取消方法,由于只有派生 context 的函数才应该取消 context。

Go 语言中的 context.Context 的主要做用仍是在多个 Goroutine 组成的树中同步取消信号以减小对资源的消耗和占用,虽然它也有传值的功能,可是这个功能咱们仍是不多用到。在真正使用传值的功能时咱们也应该很是谨慎,使用 context.Context 进行传递参数请求的全部参数一种很是差的设计,比较常见的使用场景是传递请求对应用户的认证令牌以及用于进行分布式追踪的请求 ID。


   转载规则


《Go Contex》 朱林刚 采用 知识共享署名 4.0 国际许可协议 进行许可。
 上一篇
Go GC Go GC
Go GCGo GC“GC Roots” 的对象选择JAVA的GC Root对象选择 虚拟机栈(栈帧中的本地变量表)中引用的对象; 本地方法栈(Native 方法)中引用的对象; 方法区中类静态属性引用的对象; 方法区中常量引用的对象;
2021-11-18
下一篇 
Go指针 Go指针
指针指针unsafe.Pointerunsafe.Pointer的定义 从unsate.Pointer的定义如下,从定义中我们可以看出,Pointer的本质是一个int的指针: type ArbitraryType int type Poi
2021-11-18
  目录