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 之间同步请求特定的数据、取消信号以及处理请求的截止日期。
每个 Context
都会从最顶层的 Goroutine 一层一层传递到最下层,这也是 Golang 中上下文最多见的使用方式,若是没有 Context
,当上层执行的操做出现错误时,下层其实不会收到错误而是会继续执行下去。
当最上层的 Goroutine 由于某些缘由执行失败时,下两层的 Goroutine 因为没有接收到这个信号因此会继续工做;可是当咱们正确地使用 Context
时,就能够在下层及时停掉无用的工做减小额外资源的消耗:
这其实就是 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.Background
和 context.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,固然咱们也能够不等到这个时候,能够提早经过取消函数进行取消。
WithTimeout
和WithDeadline
基本上同样,这个表示是超时自动取消,是多少时间后自动取消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
链上由尾部上前搜寻:
context.valueCtx
结构体会将除了 Value
以外的 Err
、Deadline
等方法代理到父上下文中,它只会响应 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
类型的context
。WithCancel
返回一个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
即用来建立当前节点与祖先节点这个取消关联逻辑。
- 如果
parent.Done()
返回nil
,表明父节点以上的路径上没有可取消的context
,不需要处理; - 如果在
context
链上找到到cancelCtx
类型的祖先节点,则判断这个祖先节点是否已经取消,如果已经取消就取消当前节点;否则将当前节点加入到祖先节点的children
列表。 - 否则开启一个协程,监听
parent.Done()
和child.Done()
,一旦parent.Done()
返回的channel
关闭,即context
链中某个祖先节点context
被取消,则将当前context
也取消。
context.propagateCancel
的做用是在 parent
和 child
之间同步取消和结束的信号,保证在 parent
被取消时,child
也会收到对应的信号,不会发生状态不一致的问题。
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
是一种基于cancelCtx
的context
类型,从字面上就能看出,这是一种可以定时取消的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) }
}
- 如果父节点
parent
有过期时间并且过期时间早于给定时间d
,那么新建的子节点context
无需设置过期时间,使用WithCancel
创建一个可取消的context
即可; - 否则,就要利用
parent
和过期时间d
创建一个定时取消的timerCtx
,并建立新建context
与可取消context
祖先节点的取消关联关系,接下来判断当前时间距离过期时间d
的时长dur
: - 如果
dur
小于0,即当前已经过了过期时间,则直接取消新建的timerCtx
,原因为DeadlineExceeded
; - 否则,为新建的
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。