从一个错误处理提案,谈谈 Go 语言的问题和现状
最近,Go 语言社区一件让人比较挫败的事情是,为了简化错误处理而提出的 try 提案,几经变更,又经过社区的反复讨论,最后还是黄了。关于该问题的一些讨论线索可以在 Github 专题讨论页面 Proposal: A built-in Go error check function, "try" #32437 上看到。
一个语法提案没有通过本来在开源社区不算什么大事,本来通过民主讨论决定语言的发展方向,也是集思广益的正常之举。但我以为在这个问题之后,折射出 Go 语言本身、以及社区目前存在的一些问题,这还是值得玩味的。
该问题(以及提案)产生的背景是,Go 语言中的错误处理一直以来都比较简单而原始,需要大量重复性的样板代码,很多程序员一直对此颇有烦言。为解决此问题,Go 社区也一直才尝试各种不同的路子,比如 xerrors。该提案的产生也主要是希望为此问题提供一个统一的解决方案,但从各种渠道的反应来看,社区的意见分歧相当大。早期官方曾经出现过发明新关键字(try
/handler
)的设计方案,这比较类似于其他语言的思路,然而似乎是因为引入了新的语法,以及对解析器变动较大的原因,后来被否决了。新的方案是基于函数的思路,对语言来讲变动很小,能简化比较常见的错误处理代码。但也有很多意见指出,该方法容易让返回值的处理出现混淆,在多重错误处理等场景下也可能会变得更加棘手,因此反对的声音仍然很大。在始终无法取得一致意见的情况下,最终该提案在几天前被取消了。
目前,有消息说 Go 语言团队可能会在未来结合泛型(generic)提供一个新的错误处理机制,但具体内容仍付阙如。这可能意味着对于 Go 语言的错误处理方案在未来相当长一段时间内不会有实质性的进展。开发者还是要继续像以前那样处理错误。
在关于该提案的讨论中曾经出现过这样一种意见,即:新的方案基于函数,开发者可以自行决定用或不用,对现有的错误处理方案没有影响,因此没有理由反对。我个人觉得这种看法是经不起推敲的。错误处理是一个全局性的策略问题,也是有“侵入性”的————如果你使用的某个库使用了某种特定的错误处理机制,那么你必须遵守它的规定,错误处理代码也会在很大程度上影响你的程序结构;把两个错误处理方式不同的库粘合在一个应用程序中,虽然不是不可能,至少也是非常痛苦的。这并不是一个简单的用或不用的问题。
一个提案被取消本来是无足为奇的。但如果我们从外部的角度来看这个问题,会看到什么?
首先,Go 语言出现至今已经 10 年,虽然比起资历更老的语言来说它还是个小弟弟,但也绝不算年轻了;
其次,Go 语言的设计者都是大家非常认可的行业大牛,这也是 Go 之所以被广泛推崇的一个重要原因。(如果有人质疑 Go 语言的某些设计,那么一定会有拥护者拿设计者的资历来辩护。某种程度上这不一定是个好现象)
一个诞生已经 10 年以上的语言,在错误处理这种语言设计的核心问题上仍然举棋不定,和其他同类相比,这种现象应该说是有点不太正常的。我们知道,Java 在从 1.4 发展到 5.0,Python 从 2.x 到 3.x 的进程中,也经历了内容的大幅变更,但它们的错误处理机制从语言诞生以来几乎没有大的变动。Go 有如此了不起的设计者,为什么不能设计一个稳定的错误机制呢?
个人意见,或许从语言本身的选择和社区文化可以找到一些原因。
Go 语言一直以来被称道的主要优点之一是它语法简单、心智负担小、容易写出一致的代码。但我觉得,这是一个设计决策的问题————你在获得什么的时候,一定也会失去某些东西。Go 为了简单性而失去的是什么?主要是强大而灵活的表达能力。如果你想看到相反的例子,或许可以参考 Ruby。Ruby 有着极其强大、甚至可以定制出 DSL 的能力,但相对地在性能、严谨性以及解释器复杂度等方面则有所牺牲,我们可以认为它和 Go 正好处于两个不同的极端。
在某种程度上,我认同在网上看到的一种意见:Go 是一种“上限比较低”的语言。上限低,按照我的理解,就是语言给你的选择很有限,所以你无需在设计选择的问题上花费太多脑筋,依靠直觉写下去就是。但另一方面,这也限制了你考虑其他设计思路、重新组织代码结构的能力。
当然,这可能也有一些个人口味的因素在内。对我来说,语言中存在多种不同的设计方法,能够让我停下来思考、选择,这是一种福利而不是负担。直接地说,用 Go 写代码或许很高效,但我很少能感觉到思考的乐趣。其他人可能未必同意我的看法。对我来说,Ruby(或许也可以包括 Scala) 有点灵活过了头,甚至有时候有点黑魔法的嫌疑;而 Go 和 Java 属于灵活性较少、比较死板的另一端;Python 和 Kotlin 则大致处在中间位置,它们是我比较偏爱的语言。
Go 的这种设计决策还有另一方面的问题。说“心智负担轻”,这主要是从写代码的人的视角来说的,而当你去读 Go 代码的时候,心智负担就未必那么轻了。大量的错误处理代码、往往被滥用(也许是不得已)的 interface{}、使用半吊子的 OOP 语法,很难一眼看清设计者的意图,这都是缺乏抽象能力的表现,从而使得阅读 Go 代码并不是那么愉快的体验。缺少经验的开发者往往会注重写代码的感受、而忽略读代码的问题,实际上从长期来说,读比写要重要得多。当然,肯定会有的粉丝说 Go 代码更加直白、容易理解,但我以为,缺乏足够的抽象能力,仅有直白的代码是算不上好代码的。
说到这里,可能你会觉得我是在否定 Go 语言的价值。其实不然,我也说过,任何选择都是得与失的权衡,而 Go 的这些设计决策,在以下的特定领域是非常有利的:功能要求比较稳定、需要高并发或避免依赖的场景,包括系统工具、运维、与业务关系不大的服务程序,都可以归入此类。 在这些领域 Go 的优势比较明显,鉴于逻辑相对稳定,不太会因为业务变化而发生重大变更,所以 Go 的缺陷显得不那么重要,用笨一些的方法来写也没有大的问题。Go 的杀手级 应用 Docker 就可以算是这方面的典型代表。但另一方面,如果用它来写频繁变化的业务程序、Web 服务、GUI 程序,则是明显不适合的————我尊重 Go 开发者在这些领域的努力,但个人认为,在这些领域成功的可能性很小,反而会放大 Go 的缺点。
从这些年 Go 的发展足迹可以看出,Go 的语言核心一直非常精简,而语言团队对于加入新的关键字与语法结构夸张点说几乎到了避犹不及的地步。本次提案中使用新关键字的方式很早就被否定了,这从侧面说明核心团队对此是多么谨慎。这种精简在 Go 社区一直宣传为优点(“大道至简”)。从现状来看,这种选择在 Go 的核心领域取得了不错的效果。但当 Go 语言不满足于这个小圈子,希望扩大自己的领地时,精简的核心不足以支持更广泛、更灵活的要求了,而要扩大语言的能力,恐怕 Go 语言将不得不变得更加复杂,精简的优势恐怕也不复存在。这是 Go 语言设计者要解决的难题,但也是当初的设计选择带来的必然结果。
总之,我对 Go 语言的看法是:它可以在一个局部的领域(系统和服务领域)发挥长处,但语言的基因已经决定了,它的优点和劣势都过于明显,要成为一门通用性的语言是不太可能的。提案的取消也在一定程度上说明,Go 语言的发展目前可能已经遇到了瓶颈。但这或许并不是坏事?在一个圈子里安安稳稳做一个小而美的语言,也没什么不好吧。