golang goroutine 通知_【Golang】Context基础篇

 一个接口,四种实现,六个函数。

01

一个接口

golang的context包定义了Context类型,根据官方文档的说法,该类型被设计用来在API边界之间以及过程之间传递截止时间、取消信号及其他与请求相关的数据。Context实际上是一个接口,提供了4个方法:   
 type Context interface {    Deadline() (deadline time.Time, ok bool)    Done() struct{}    Err() error    Value(key interface{}) interface{}}

-  Deadline 用来返回ctx的截止时间,ok为false表示没有设置。达到截止时间的ctx会被自动Cancel掉;- 如果当前ctx是可取消的, Done 返回一个chan用来监听,否则返回nil。当ctx被Cancel时,返回的chan会同时被close掉,也就变成“有信号”状态;- 如果ctx已经被canceled, Err 会返回执行Cancel时指定的error,否则返回nil;-  Value 用来从ctx中根据指定的key提取对应的value;

02

四种实现


golang对Context接口的具体实现基于如下几种类型:
type emptyCtx inttype cancelCtx struct {    Context    mu       sync.Mutex            // 保护其他字段    done     chan struct{}         // 延迟创建,被第一个cancel close    children map[canceler]struct{} // 被第一个cancel设为nil    err      error                 // 被第一个cancel赋值(非nil)}type timerCtx struct {    cancelCtx    timer *time.Timer // 被cancelCtx.mu保护(并发保护)    deadline time.Time}type valueCtx struct {    Context    key, val interface{}}
-  emptyCtx 类型只是实现了Context接口,几个方法都只是简单的返回nil、false等,实际上什么也不做。Background和TODO返回的Context,实际上都是emptyCtx;-  cancelCtx 类型可以说是最核心的一个类型,它实现了Cancel操作和信号机制,以及Context父子关系关联,从而支持在父Context Cancel时同步Cancel所有子Context;-  timerCtx 类型用来支持Deadline、Timeout,Cancel操作依赖于内置的cancelCtx;-  valueCtx 类型用来支持key、val打包,结合内置的Context字段,实际上构造了一个单链表节点;

03

六个函数


为了方便我们使用Context,context包还实现了一组函数:
func Background() Contextfunc TODO() Contextfunc WithCancel(parent Context) (ctx Context, cancel CancelFunc)func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)func WithValue(parent Context, key, val interface{}) Context

01 Background

var (  background = new(emptyCtx)  todo       = new(emptyCtx))func Background() Context {  return background}
Background 函数会创建一个没有Deadline、没有Value,也不能被Cancel的emptyCtx。通常在一个请求的初始化阶段用Background()创建最顶层的根Context。
ctx := context.Background()
`ctx`是Context类型,也就是一个非空接口,它的结构如下图所示:

aa5693dcfeac5eef8e9e6ed44d8133fd.png

02 TODO

func TODO() Context {  return todo}
TODO 函数和Background一样,也会创建一个emptyCtx,官方文档建议在本来应该使用外层传递的ctx,而外层却没有传递的地方使用,就像函数名的含义一样,留下一个TODO。

03 WithCancel

func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
WithCancel 函数会基于传入的 parent 创建一个可以Cancel的 ctx ,与 cancel 函数一起返回。调用 cancel 函数就会将这个新的`ctx` Cancel掉,所有基于此 ctx 创建的子孙Context也会一并被Cancel掉。
func main() {    var wg sync.WaitGroup    ctx := context.Background()    ctx1, cancel = context.WithCancel(ctx)    wg.Add(1)    go func() {        defer wg.Done()        tick := time.NewTicker(300 * time.Millisecond)        for {            select {            case                 fmt.Println(ctx1.Err())                return            case t :=                 fmt.Println(t.Nanosecond())            }        }    }()    time.Sleep(time.Second)    cancel()    wg.Wait()}
上面的示例把`Background()`获取的`ctx`包装为可Cancel的`ctx1`,它的结构如下图所示:(示例中拆分成ctx和ctx1单纯为了图好画-_-!)

bbb5ee654fdd9360d8191161fa2a709c.png


通过`ctx1.Done()`获取一个channel,监听这个channel可以获得`ctx1`的Cancel通知,还可以通过 `ctx.Err()` 获取 `ctx1` 被取消时写入的错误信息。

04 WithDeadline

func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)
WithDeadline 内部会创建一个可以Cancel的 ctx ,并且为它设置一个超时时间,然后与一个 cancel 函数一并返回。如果用户代码没有主动调用 cancel 函数,则 ctx 会在超时时间到达后自动Cancel。
如果需要给任务设定一个截止时间,就可以用WithDeadline创建一个timerCtx。用wg来等待goroutine返回,若没有达到截止时间就完成了任务,`defer cancel()`也会把`ctx2`取消掉。
func main() {    var wg sync.WaitGroup    ctx := context.Background()    ctx2, cancel := context.WithDeadline(ctx,time.Now().Add(time.Second))    defer cancel()    wg.Add(1)    go func() {        defer wg.Done()        tick := time.NewTicker(300 * time.Millisecond)        for {            select {            case                 fmt.Println(ctx2.Err())                return            case t :=                 fmt.Println(t.Nanosecond())            }        }    }()    wg.Wait()}
上面的示例中传递给WithDeadline函数的,是一个没有截止时间且不可取消的Context。WithDeadline内部会基于`ctx`构建一个`cancelCtx`,所以`ctx2`的动态类型是`*timerCtx`结构如下图所示:

0304979ced2df651b6339953b3b4528c.png


如果WithDeadline函数接收到的是一个有截止时间的Context,那就要比较一下这两个截止时间了。例如下面再基于 ctx2 创建一个Context:
ctx3, cancel := context.WithDeadline(ctx2, time.Now().Add(time.Second*2))
如果 ctx3 要设定的截止时间早于 ctx2 ,那么就会构造一个timerCtx,并且 timerCtx.cancelCtx 是基于 ctx2 构建的。

7c438e4a34a803dd388b102ce430fa02.png

如果 ctx3 要设定的截止时间比 ctx2 还晚,那么就没必要给它加定时器和截止时间了。只会用WithCancel函数基于 ctx2 构造一个cancelCtx,所以 ctx3 的动态类型是 *cancelCtx 。

7e7ded791e502401fd2a23292097ef66.png


上面的 ctx3 是基于 ctx2 创建的,而 ctx2 又是基于 ctx 创建的。这样层层包装的结果就是,这些Context会形成树状结构,子节点是基于父节点的包装,每个节点都可能有零个或多个子节点。

6aa2a46a8fbe7e166faa308e0e215b13.png


对于可取消的Context而言,也就是实现了 context.canceler 接口的类型,还有一个关键点,就是它们会被注册到距离当前Context节点最近的、可取消的祖先节点中。注册位置就在cancelCtx结构体的 children map中。而 *cancelCtx和 *timerCtx都实现了 canceler 接口,所以 ctx3 会被注册到 ctx2 中。

78a106156f300ba78a62ee956a239ddb.png


这样如果 ctx2 先取消,就可以根据 children map这里的记录,取消自己子节点中所有可Cancel的Context。

b21ccbc1af8abc490c8262f2d7cc7932.png

05 WithTimeout

func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {    return WithDeadline(parent, time.Now().Add(timeout))}
`WithTimeout`只是`WithDeadline`的一个wrapper,接受一个`time.Duration`类型的参数,使用当前时间加上这个时间段来设置Deadline。

06 WithValue

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

`WithValue`将传入的`parent` Context和`key`、`val`打包成一个新的`ctx`,再借助`valueCtx`的`Value`方法就可以通过Context传递数据了。使用Context传递数据要特别注意:

- Context是本着不可改变(immutable)的模式设计的,所以 不要试图修改Context里保存的数据 。
- 为了避免后续包装的键值对覆盖先前的值,最好不要直接使用 string 、 int 这类基础类型,而是用自定义类型包装一下。
下面我们来测试一下上述第二点:
package mainimport (    "context"    "fmt")var keyA string = "keyA"var keyC string = "keyA"func main() {    ctx := context.Background()      ctx1 := context.WithValue(ctx, keyA, "valueA")    fmt.Println("In ctx:")    fmt.Println("keyA => ", ctx1.Value(keyA))    ctx2 := context.WithValue(ctx1, keyC, "valueC")    fmt.Println("In ctx2:")    fmt.Println("keyA => ", ctx2.Value(keyA))    fmt.Println("keyC => ", ctx2.Value(keyC))        return}
运行结果如下, ctx2 中用相等的key覆盖了 ctx1 中的值。
In ctx:keyA =>  valueAIn ctx2:keyA =>  valueCkeyC =>  valueC

我们先来看一下 ctx1 和 ctx2 的结构,valueCtx中 key 和 val 都是interface{}类型,空接口类型的结构是一个动态类型元数据指针,外加一个指向动态值的unsafe.Pointer。

293701faf6ef89f3f1871d14019deab9.png

再结合valueCtx.Value方法看一下键值对查找过程:
func (c *valueCtx) Value(key interface{}) interface{} {    if c.key == key {        return c.val    }    return c.Context.Value(key)}
可以看到,通过`ctx2.Value("keyA")`查找val时,通过比较`key`,会直接锁定`ctx2`中保存的`val`,所以发生了Context间Value的覆盖。可以通过自定义key类型来避免这个问题。
type akeytype stringtype ckeytype stringvar keyA akeytype = "keyA"var keyC ckeytype = "keyA"......

执行结果如下:
In ctx:keyA =>  valueAIn ctx2:keyA =>  valueAkeyC =>  valueC
这一次通过 ctx2.Value(keyA) 查找val时,因为 ctx2 中存储的 key 与 `keyA` 类型不相符,所以会把查找请求委托给 ctx2 的parent,也就是 ctx1 。通过这样一级一级的向上查找,实际上形成了一个链表结构。

d7fcefc0f8d14e8437792d6b87056cbc.png

WithCancel 、 WithDeadline 和 WithValue 函数,内部就是分别构造了 cancelCtx 、 timerCtx 和 valueCtx 这3种结构,最终所有的 ctx 将构成类似一棵树的结构,在根节点执行cancel就可以Cancel整个棵树,所以非常适合用来控制请求处理。 Go语言的http和sql包中都有Context的应用,这些待到“应用篇”再接着聊~ 戳这里了解接口~

cf3cf80bd98e51ce63f8ead6081de54d.png

点个赞呗

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值