GO 语言context.Context类型

context.Context类型

context.Context类型(以下简称Context类型)是在Go 1.7发布时才被加入到标准库的。而后,标准库中的很多其他代码包都为了支持它而进行了扩展,包括:os/exec包、net包、database/sql包,以及runtime/pprof包和runtime/trace包,等等。
Context类型之所以受到了标准库中众多代码包的积极支持,主要是因为它是一种非常通用的同步工具。它的值不但可以被任意地扩散,而且还可以被用来传递额外的信息和信号。
更具体地说,Context类型可以提供一类代表上下文的值。此类值是并发安全的,也就是说它可以被传播给多个goroutine。
由于Context类型实际上是一个接口类型,而context包中实现该接口的所有私有类型,都是基于某个数据类型的指针类型,所以,如此传播并不会影响该类型值的功能和安全。

context 的作用

1.通过context,我们可以方便地对同一个请求所产生地goroutine进行约束管理,可以设定超时、deadline,甚至是取消这个请求相关的所有goroutine。形象地说,假如一个请求过来,需要A去做事情,而A让B去做一些事情,B让C去做一些事情,A、B、C是三个有关联的goroutine,那么问题来了:假如在A、B、C还在处理事情的时候请求被取消了,那么该如何优雅地同时关闭goroutine A、B、C呢?这个时候就轮到context包上场了。

2.在golang中的创建一个新的线程并不会返回像c语言类似的pid所有我们不能从外部杀死某个线程,所有我就得让它自己结束之前我们用channel+select的方式,来解决这个问题但是有些场景实现起来比较麻烦,例如由一个请求衍生出多个线程并且之间需要满足一定的约束关系,以实现一些诸如:

有效期,中止线程树,传递请求全局变量之类的功能
context 接口定义
type Context interface {

Deadline() (deadline time.Time, ok bool)

Done() <-chan struct{}

Err() error

Value(key interface{}) interface{}

}

1.Deadline方法是获取设置的截止时间的意思,第一个返回式是截止时间,到了这个时间点,Context会自动发起取消请求;第二个返回值ok==false时表示没有设置截止时间,如果需要取消的话,需要调用取消函数进行取消。

2.Done方法返回一个只读的chan,类型为struct{},我们在goroutine中,如果该方法返回的chan可以读取,则意味着parent context已经发起了取消请求,我们通过Done方法收到这个信号后,就应该做清理操作,然后退出goroutine,释放资源。之后,Err 方法会返回一个错误,告知为什么 Context 被取消。

3.Err方法返回取消的错误原因,因为什么Context被取消。

4.Value方法获取该Context上绑定的值,是一个键值对,所以要通过一个Key才可以获取对应的值,这个值一般是线程安全的。

创建 context

一个是Background,主要用于main函数、初始化以及测试代码中,作为Context这个树结构的最顶层的Context,也就是根Context,它不能被取消。

一个是TODO,如果我们不知道该使用什么Context的时候,可以使用这个。

两个实现方式代码是一样的,不同的是,静态分析工具可以使用它来验证 context 是否正确传递

var (
    background = new(emptyCtx)
    todo       = new(emptyCtx)
)
func Background() Context {
    return background
}

func TODO() Context {
    return todo
}

四个重要的函数

1.context.WithCancel(parent Context) (ctx Context, cancel CancelFunc)

返回派生 context 和取消函数。只有创建它的函数才能调用取消函数来取消此 context。如果您愿意,可以传递取消函数,但是,强烈建议不要这样做。这可能导致取消函数的调用者没有意识到取消 context 的下游影响。

ctx, cancel := context.WithCancel(context.Background())
2.context.WithDeadline(parent Context, d time.Time) (ctx Context, cancel CancelFunc)

此函数返回其父项的派生 context,当截止日期超过或取消函数被调用时,该 context 将被取消。例如,您可以创建一个将在以后的某个时间自动取消的 context,并在子函数中传递它。当因为截止日期耗尽而取消该 context 时,获此 context 的所有函数都会收到通知去停止运行并返回。

ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(2 * time.Second))
3.context.WithTimeout(parent Context, timeout time.Duration) (ctx Context, cancel CancelFunc)

此函数类似于 context.WithDeadline。不同之处在于它将持续时间作为参数输入而不是时间对象。此函数返回派生 context,如果调用取消函数或超出超时持续时间,则会取消该派生 context。

ctx, cancel := context.WithTimeout(context.Background(), 2 * time.Second)
4.context.WithValue(parent Context, key, val interface{}) (ctx Context, cancel CancelFunc)

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

示例:


func ContextTest01() {
    logger.Info("start")
    ctx, cancelFunc := context.WithCancel(context.Background())
    go func() {
        logger.Info("go 1")
        cancelFunc()
    }()
    <-ctx.Done()
    logger.Info("end")
    logger.Info("end", ctx.Err())
}

func ContextTest02() {
    fmt.Println("start")
    gen := func(ctx context.Context) <-chan int {
        dst := make(chan int)
        n := 1
        go func() {
            for {
                select {
                case <-ctx.Done():
                    logger.Info("end")
                    return // returning not to leak the goroutine
                case dst <- n:
                    n++
                }
            }

        }()
        return dst
    }

    ctx, cancel := context.WithCancel(context.Background())
    defer cancel() // main 方法执行完后,结束 ctx 相关的 goroutine

    for n := range gen(ctx) {
        logger.Info(n)
        if n == 5 {
            break
        }
    }
}

func ContextTest03() {
    // 自动取消(定时取消)
    timeout := 3 * time.Second
    ctx, _ := context.WithTimeout(context.Background(), timeout)
    logger.Info(Add(ctx), ctx.Err())

}

func ContextTest04() {
    // 手动取消
    ctx, cancel := context.WithCancel(context.Background())
    go func() {
        time.Sleep(2 * time.Second)
        cancel() // 在调用处主动取消
    }()
    logger.Info(Add(ctx), ctx.Err())
}

func Cdd(ctx context.Context) int {
    logger.Info(ctx.Value("K_C"))
    <-ctx.Done()
    return -3
}
func Bdd(ctx context.Context) int {
    logger.Info(ctx.Value("K_A"))
    logger.Info(ctx.Value("K_B"))
    ctxc := context.WithValue(ctx, "K_C", "I am GO")
    go logger.Info(Cdd(ctxc), ctxc.Err())
    <-ctxc.Done()
    return -2
}
func Add(ctx context.Context) int {
    ctxa := context.WithValue(ctx, "K_A", "HELLO")
    ctxb := context.WithValue(ctxa, "K_B", "WORLD")
    go logger.Info(Bdd(ctxb), ctxb.Err())
    <-ctxb.Done()
    return -1

}

知识扩展

问题1:“可撤销的”在context包中代表着什么?“撤销”一个Context值又意味着什么?

我相信很多初识context包的Go程序开发者,都会有这样的疑问。确实,“可撤销的”(cancelable)这个词在这里是比较抽象的,很容易让人迷惑。我这里再来解释一下。

这需要从Context类型的声明讲起。这个接口中有两个方法与“撤销”息息相关。Done方法会返回一个元素类型为struct{}的接收通道。不过,这个接收通道的用途并不是传递元素值,而是让调用方去感知“撤销”当前Context值的那个信号。

一旦当前的Context值被撤销,这里的接收通道就会被立即关闭。我们都知道,对于一个未包含任何元素值的通道来说,它的关闭会使任何针对它的接收操作立即结束。

正因为如此,在coordinateWithContext函数中,基于调用表达式cxt.Done()的接收操作,才能够起到感知撤销信号的作用。

除了让Context值的使用方感知到撤销信号,让它们得到“撤销”的具体原因,有时也是很有必要的。后者即是Context类型的Err方法的作用。该方法的结果是error类型的,并且其值只可能等于context.Canceled变量的值,或者context.DeadlineExceeded变量的值。

前者用于表示手动撤销,而后者则代表:由于我们给定的过期时间已到,而导致的撤销。

func ContextTest01() {
    logger.Info("start")
    ctx, cancelFunc := context.WithCancel(context.Background())
    go func() {
        logger.Info("go 1")
        cancelFunc()
    }()
    <-ctx.Done()
    logger.Info("end")
    logger.Info("end", ctx.Err())
//  最后输出 ‘end context canceled’
}

你可能已经感觉到了,对于Context值来说,“撤销”这个词如果当名词讲,指的其实就是被用来表达“撤销”状态的信号;如果当动词讲,指的就是对撤销信号的传达;而“可撤销的”指的则是具有传达这种撤销信号的能力。

我在前面讲过,当我们通过调用context.WithCancel函数产生一个可撤销的Context值时,还会获得一个用于触发撤销信号的函数。

通过调用这个函数,我们就可以触发针对这个Context值的撤销信号。一旦触发,撤销信号就会立即被传达给这个Context值,并由它的Done方法的结果值(一个接收通道)表达出来。

撤销函数只负责触发信号,而对应的可撤销的Context值也只负责传达信号,它们都不会去管后边具体的“撤销”操作。实际上,我们的代码可以在感知到撤销信号之后,进行任意的操作,Context值对此并没有任何的约束。

最后,若再深究的话,这里的“撤销”最原始的含义其实就是,终止程序针对某种请求(比如HTTP请求)的响应,或者取消对某种指令(比如SQL指令)的处理。这也是Go语言团队在创建context代码包,和Context类型时的初衷。

如果我们去查看net包和database/sql包的API和源码的话,就可以了解它们在这方面的典型应用。

问题2:撤销信号是如何在上下文树中传播的?

我在前面讲了,context包中包含了四个用于繁衍Context值的函数。其中的WithCancel、WithDeadline和WithTimeout都是被用来基于给定的Context值产生可撤销的子值的。

context包的WithCancel函数在被调用后会产生两个结果值。第一个结果值就是那个可撤销的Context值,而第二个结果值则是用于触发撤销信号的函数。

在撤销函数被调用之后,对应的Context值会先关闭它内部的接收通道,也就是它的Done方法会返回的那个通道。

然后,它会向它的所有子值(或者说子节点)传达撤销信号。这些子值会如法炮制,把撤销信号继续传播下去。最后,这个Context值会断开它与其父值之间的关联。

我们通过调用context包的WithDeadline函数或者WithTimeout函数生成的Context值也是可撤销的。它们不但可以被手动撤销,还会依据在生成时被给定的过期时间,自动地进行定时撤销。这里定时撤销的功能是借助它们内部的计时器来实现的。

当过期时间到达时,这两种Context值的行为与Context值被手动撤销时的行为是几乎一致的,只不过前者会在最后停止并释放掉其内部的计时器。

最后要注意,通过调用context.WithValue函数得到的Context值是不可撤销的。撤销信号在被传播时,若遇到它们则会直接跨过,并试图将信号直接传给它们的子值。

问题 3:怎样通过Context值携带数据?怎样从中获取数据?

既然谈到了context包的WithValue函数,我们就来说说Context值携带数据的方式。

WithValue函数在产生新的Context值(以下简称含数据的Context值)的时候需要三个参数,即:父值、键和值。与“字典对于键的约束”类似,这里键的类型必须是可判等的。

原因很简单,当我们从中获取数据的时候,它需要根据给定的键来查找对应的值。不过,这种Context值并不是用字典来存储键和值的,后两者只是被简单地存储在前者的相应字段中而已。

Context类型的Value方法就是被用来获取数据的。在我们调用含数据的Context值的Value方法时,它会先判断给定的键,是否与当前值中存储的键相等,如果相等就把该值中存储的值直接返回,否则就到其父值中继续查找。

如果其父值中仍然未存储相等的键,那么该方法就会沿着上下文根节点的方向一路查找下去。

注意,除了含数据的Context值以外,其他几种Context值都是无法携带数据的。因此,Context值的Value方法在沿路查找的时候,会直接跨过那几种值。

如果我们调用的Value方法的所属值本身就是不含数据的,那么实际调用的就将会是其父辈或祖辈的Value方法。这是由于这几种Context值的实际类型,都属于结构体类型,并且它们都是通过“将其父值嵌入到自身”,来表达父子关系的。

最后,提醒一下,Context接口并没有提供改变数据的方法。因此,在通常情况下,我们只能通过在上下文树中添加含数据的Context值来存储新的数据,或者通过撤销此种值的父值丢弃掉相应的数据。如果你存储在这里的数据可以从外部改变,那么必须自行保证安全。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268

推荐阅读更多精彩内容