最近go社区一直在热议是否应该在下个版本中去掉上下文,将上下文集成到 io.Reader
,以便在上下文取消时中止读取操作。如果GO真在下个版本中去掉上下文,那么 io.Reader
就会变成这样:
type Reader interface {
Read(ctx context.Context, p []byte) (n int, err error)
}
读取流数据或者大文件是很常见的时间损耗任务,因此这建议看起来似乎是个不错的主意。
但是我却不是这样认为的。
1.Go 不是服务端专用语言
首先,Go 虽然很适合用来写服务端,但它本身定位应该是通用型语言(general purpose language),而不是服务端专用语言,而上下文基本都是写服务的时候使用。
2.上下文是病毒
上下文在程序中的传播就像病毒一样,跟随着请求的调用路径,每个函数都要将它作为第一个要传递的参数。
更糟糕的是,它还会扩散到不同的包。想象一下,如果一个 Go 代码库在执行中存在时间损耗(例如前面提到的 io
或者 sql
等等),且它可能会被用于服务端程序,那么它就应该支持上下文。如此一来,所有人都要考虑上下文了,即使你并不需要它!这又违背了 Go 是通用型语言这一点。
3.WithValue
不是个好东西
ctx.Value
存在很多缺陷(这是 context
提供的一种在上下文中传递数据的机制),并给出了如下理由:
- 它传递的值不是静态类型(使用反射实现)
- 使用的时候,需要给函数写注释,说明函数用到了哪些键和值
- 使用场景很像线程本地存储(thread-local storage),这玩意儿很糟糕,不灵活,使用复杂,测试麻烦……
- 太魔法了,很容易出 bug
ctx.Value
的确能让一些事情变得简单,但他相信,设计 API 的时候,还是应该避免使用它,总能找到替代方案的。
4.代码看上去太不优雅
现在创建一个上下文变量看起来已经很傻了:
代码语言:txt复制ctx context.Context
很像 Java:
代码语言:txt复制Foo foo = new Foo();
而这种事情是 Go 最开始创建出来就想要避免的。
如果把上下文再引入标准库的 io
包:
n, err := r.Read(context.TODO(), p)
更加不方便
5.goroutine 取消的难题
**context
这个包到底解决了什么问题?**
很简单,就是如何取消(cancel)协程。在 context
包出来之前,有过一个讲座专门谈论协程的取消问题,解决方案就是用原生 channel 实现,但伸缩性较差。但主要问题是:
- 别的库不会接收 cancel channel,所以只能在不同的操作之间做取消
- 设想一个 goroutine 树,要直接取消整个树是很简单的,但如果要取消一个子树,你还需要定义一个新的 cancel channel
随后 context
包的出现,解决了这两个问题,尽管它存在问题,但目前仍是最好的方案。
6.Go 应该从语言层面解决取消难题
我觉得,Go 应该在第 2 版中在语言层面解决「取消难题」。Go 已经让我们很方便地创建协程,以及在协程间通讯,但 context
包的存在,证明了 Go 还不能很好的处理协程取消的问题,这是 Go 的一个弱点。
对于新的解决方案,需要满足以下几条:
- 简洁优雅
- 可选性,非侵入式,非感染式
- 健壮且高效
- 只解决取消的问题,忽略
Values
我正在参与2023腾讯技术创作特训营第三期有奖征文,组队打卡瓜分大奖!