Golang 之 Context 的迷思

Posted 火丁笔记

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Golang 之 Context 的迷思相关的知识,希望对你有一定的参考价值。

对我而言,Golang 中的 Context 一直是谜一样的存在,如果你还不了解它,建议阅读「快速掌握 Golang context 包,简单示例https://deepzz.com/post/golang-context-package-notes.html」,本文着重讨论一些我曾经的疑问。

Context 到底是干什么的?

如果你从没接触过 Golang,那么按其它编程语言的经验来推测,多半会认为 Context 是用来读写一些请求级别的公共数据的,事实上 Context 也确实拥有这样的功能:

  • Value(key interface{}) interface{}

  • WithValue(parent Context, key, val interface{}) Context

不过除此之外,Context 还有一个功能是控制 goroutine 的退出:

  • func 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)

把两个毫不相干的功能合并在同一个包里,无疑增加了使用者的困扰,Dave Cheney 曾经吐槽:「Context isn’t for cancellation https://dave.cheney.net/2017/08/20/context-isnt-for-cancellation」,按他的观点:Context 只应该用来读写一些请求级别的公共数据,而不应该用来控制 goroutine 的退出,况且用 Context 来控制 goroutine 的退出,在功能上并不完整(没有确认机制),原文:

Context‘s most important facility, broadcasting a cancellation signal, is incomplete as there is no way to wait for the signal to be acknowledged.

此外,Michal Štrba 的观点更为尖锐,按他的观点「Context should go away for Go 2 https://faiface.github.io/post/context-should-go-away-go2/」:用 Context 来读写一些请求级别的公共数据,本身就是一种拙劣的设计;而用 Context 来控制 goroutine 退出亦如此,正确的做法应该是在语言层面解决,不过关于这一点,只能寄希望于 Golang 2.0 能有所作为了。

从目前社区对 Context 的使用情况来看,基本上主要还是使用 Context 控制 goroutine 的退出,不管你喜不喜欢,Context 已经成为了一种事实标准。

Context 一定是第一个参数么?

如果你用Context 写过程序,那么多半看过文档上建议不要在 struct 里保存 Context,而应该显式的传递方法,并且作为方法的第一个参数:

Do not store Contexts inside a struct type; instead, pass a Context explicitly to each function that needs it. The Context should be the first parameter.

可是我们偏偏在标准库里就能看到一个反例 http.Request:

1type Request struct {
2    // ...
3
4    // ctx is either the client or server context. It should only
5    // be modified via copying the whole Request using WithContext.
6    // It is unexported to prevent people from using Context wrong
7    // and mutating the contexts held by callers of the same request.
8    ctx context.Context
9}

go

一边说不要把 Context 放到 struct 里,另一方面却偏偏这么干,是不是自相矛盾?实际上,这是文档描述问题,按照惯用法,Context 应该作为方法的第一个参数,但是如果 struct 类型本身就是方法的参数的话,那么把 Context 放到 struct 里并无不妥之处,http.Request 就属于此类情况,关键在于只是传递 Context 不是存储 Context。

顺便说一句,把 Context 作为方法的第一个参数真是丑爆了!引用「Context should go away for Go 2」的话来说:「Context is like a virus」,看着代码想死的心都有了。

Context 控制 goroutine 的退出有什么好处?

我们知道 Context 是在 Golang 1.7 才成为标准库的,那么在没有 Context 的时候,人们是如何控制 goroutine 退出呢?下面举例同时运行多个 goroutines,看看如何退出:

 1package main
2
3import (
4    "fmt"
5    "sync"
6)
7
8func main() {
9    var wg sync.WaitGroup
10
11    do := make(chan int)
12    done := make(chan int)
13
14    for i := 0; i < 10; i++ {
15        wg.Add(1)
16
17        go func(i int) {
18            defer wg.Done()
19
20            select {
21            case <-do:
22                fmt.Printf("Work: %d\\n", i)
23            case <-done:
24                fmt.Printf("Quit: %d\\n", i)
25            }
26        }(i)
27    }
28
29    close(done)
30
31    wg.Wait()
32}

代码里的 wg 之类的代码只是为了演示效果,可以忽视,只要关注 done 的使用就可以了,它用来控制什么时候关闭 goroutines,实际使用非常简单,只要调用 close 即可,所有的 goroutines 都会从 done 收到关闭的消息。如此说来,用 Context 控制 goroutine 的退出有什么好处?这是因为 Context 实现了继承,可以完成更复杂的操作,我们引用「如何正确使用 Context – Jack Lindamood https://blog.lab99.org/post/golang-2017-10-27-video-how-to-correctly-use-package-context.html」中的例子来说明一下:

 1type userID string
2
3func tree() {
4    ctx1 := context.Background()
5    ctx2, _ := context.WithCancel(ctx1)
6    ctx3, _ := context.WithTimeout(ctx2, time.Second*5)
7    ctx4, _ := context.WithTimeout(ctx3, time.Second*3)
8    ctx5, _ := context.WithTimeout(ctx3, time.Second*6)
9    ctx6 := context.WithValue(ctx5, userID("UserID"), 123)
10
11    // ...
12}

如此构造了 Context 继承链:


当 3s 超时后,ctx4 会被触发:

Golang 之 Context 的迷思


当 5s 超时后,ctx3 会被触发,不仅如此,其子节点 ctx5 和 ctx6 也会被触发,即便 ctx5 本身的超时时间还没到,但因为它的父节点已经被触发了,所以它也会被触发:


总体来说,Context 是一个实战派的产物,虽然谈不上优雅,但是它已经是社区里的事实标准。实际使用中,任何有可能「慢」的方法都应该考虑通过 Context 实现退出机制,以避免因为无法退出导致泄露问题,对于服务端编程而言,通常意味着你很多方法的第一个参数都会是 Context,虽然丑爆了,但在出现更好的解决方案之前,忍着!

以上是关于Golang 之 Context 的迷思的主要内容,如果未能解决你的问题,请参考以下文章

Golang 高效实践之并发实践context篇

golang之context详解

golang goroutine例子[golang并发代码片段]

golang代码片段(摘抄)

有漏应以正见段之哲学迷思——人活着有什么意义?

golang context