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 会被触发:
当 5s 超时后,ctx3 会被触发,不仅如此,其子节点 ctx5 和 ctx6 也会被触发,即便 ctx5 本身的超时时间还没到,但因为它的父节点已经被触发了,所以它也会被触发:
总体来说,Context 是一个实战派的产物,虽然谈不上优雅,但是它已经是社区里的事实标准。实际使用中,任何有可能「慢」的方法都应该考虑通过 Context 实现退出机制,以避免因为无法退出导致泄露问题,对于服务端编程而言,通常意味着你很多方法的第一个参数都会是 Context,虽然丑爆了,但在出现更好的解决方案之前,忍着!
以上是关于Golang 之 Context 的迷思的主要内容,如果未能解决你的问题,请参考以下文章