提及编程语言,2023 年,除了老牌的 C++ 和新晋之秀 Rust 热度最高之外,就要数 Go 了。 从 2009 年由 C 语言获取灵感而发布,到如今风靡已久的高性能语言,Go 已经走过了 14 个年头。
“Go是一个项目,不只是一门语言。我们最初的目标不是创建一种新的编程语言,而是创建一种更好的软件编写方式。”
2023 年 11 月 10 日是 Go 作为开源项目推出 14 周年。Go语言之父 Rob Pike 在在悉尼 GopherConAU 会议上进行了一场耐人寻味的演讲。Go走到今天,做对了什么?做错了什么?这里总结在此,以飨诸君。
今天是 2023 年 11 月 10 日,Go 作为开源项目推出 14 周年。2009年的这天,加州时间下午 3 点(如果没记错的话),Ken Thompson、Robert Griesemer、Russ Cox、Ian Taylor、Adam Langley、Jini Kim 和我满怀期待地看着网站上线,全世界都知道了我们在做什么。
Go是一个项目,不只是一门语言。
即使是最成功的项目 ,经过反思,也有一些可以做得更好的地方。当然 ,事后看来,这些事情似乎是他们成功的关键。
这些因素正是我们想要解决的:构建现代服务器软件的复杂性:控制依赖性、与人员不断变化的大型团队一起编程、易于维护、高效测试、多核 CPU 和网络的有效使用等等。
根据大家的反馈看,认为 Go 做错的事情都在语言层面, 而其实Go做对的事情,都在更大的故事中,即围绕语言的周边工具和生态, 比如 gofmt 以及部署和测试。这说明Go成员们做对了。
Go为什么会取得现在的成功?总结了有以下几点。每一个都至关重要。
我们从正式的规范开始。这不仅可以在编写编译器时锁定行为,还可以使 多个实现共存并就该行为达成一致。编译器本身并不是一个规范。您测试编译器的依据是什么?
有多个编译器,它们都实现相同的规范。有了规范就可以更容易地实现这一点。
有一天,伊恩·泰勒(Ian Taylor)发邮件通知我们,在阅读了我们的规范草案后,他自己编写了一个编译器,这让我们感到惊讶。
我的一位同事向我指出了 http://.../go_lang.html 。 它看起来是一种有趣的语言,我拼凑了一个 gcc前端。 当然,它缺少很多功能,但它确实在网页上编译了素数筛代码。
这是令人兴奋的,但更多的事情也随之而来,所有这些都因正式规范的存在而成为可能。
拥有多个编译器帮助我们改进了语言并完善了规范,并为那些不太喜欢我们类似 Plan-9 的业务方式的其他人提供了替代环境。
如今有很多兼容的实现,这很棒。
我们使交叉编译变得微不足道,这使得程序员可以在他们喜欢的任何平台上工作,并交付到任何需要的平台。 人们很容易将编译器视为是与运行的本机是原生的。Go可以打破这个假设,对许多开发者来说都是新闻。
我们努力使语言成型1.0 版本,然后通过兼容性保证将其锁定。Go的受欢迎程度已经很高,在一个几乎没有其他东西是稳定的世界中,不用担心新的 Go 版本会破坏你的项目,是最让人心安的。当然,很多其他项目不会这么做,因为维护强大的兼容性是需要成本的。
库的发展有点偶然。一开始可以安装 Go 代码的地方非常贫瘠,但这样一个可靠、制作精良的库的存在,其中包含编写 21 世纪服务器代码所需的大部分内容,是一项重要资产。它让社区所有人都使用同一个工具包,直到我们有足够的经验来了解还应该提供什么。这非常有效,有助于防止变体库的出现,有助于统一社区。
我们确保该语言易于解析,从而支持工具构建。起初我们认为我们需要一个适用于 Go 的 IDE,但拥有简单的工具反而会意味着,IDE 迟早将出现在 Go 上。这些工具和 gopls 一起就做到了,而且他们非常棒。
我们还为编译器提供了一套辅助工具,例如 自动化测试、覆盖率和代码审查。当然还有 go 命令,它集成了整个构建过程,并且是许多项目构建和维护 Go 代码所需的全部内容。
此外,Go 获得了快速构建的声誉。
我将 gofmt 作为一个单独的项目从工具中拿出来,因为 它不仅给Go,而且给整个编程社区上都带来了新的活力。在 Robert 编写 gofmt 之前,自动格式化程序的质量不高,因此大多未使用。
Gofmt 表明它可以做得很好,今天几乎每种值得使用的语言都有一个标准格式化程序。不争论空格和换行符所节省的时间值得花在定义标准格式和编写这段相当困难的代码以实现自动化上的所有时间。
此外,gofmt 还使无数其他工具成为可能,例如简化器、分析器甚至代码覆盖率工具。因为 gofmt 的内容成为了任何人都可以使用的库,所以您可以解析程序、编辑 AST,然后打印字节完美的输出,供人类和机器使用。
并发性成就了 Go,它让Go看起来像是一种新事物。Go 对并发的支持是一个主要的吸引因素,提高了Go的早期采用率。同时,Go 的诞生在一定程度上让并发编程成为了一种主流思想,它让编程世界相信并发是一个强大的工具(特别是在多核网络世界中)方面发挥了重要作用,并且它可以比 pthread 做得更好。现在大多数主流语言都对并发有很好的支持。
更有意思的是,Go 版本的并发,非常新颖。没有协程,没有任务,没有线程,没有名称,只有 goroutine。我们发明了“goroutine”这个词,因为没有合适的现有术语。直到今天我还是希望 Unix 的拼写命令能够学会它。
问题来了,这就是我们犯了两个重大错误的地方。
首先,并发很有趣,但我们想到的用例主要是服务器的东西,也就意味着在 net/http 等关键库中完成,而不是在每个程序中随处可见。因此,当许多程序员使用它时,他们很难弄清楚它如何真正帮助到他们。
我们应该预先解释声明一下,该语言中的并发支持真正带来的是更简单的服务器软件。这个问题就在于许多人都认为重要,但并不是对所有尝试过 Go 的人来说都很重要。“缺乏响应的指导是我们的责任。”
第二点就是,我们花了太长时间来澄清并行性(支持多核机器上并行的多个计算)和并发性(这是一种构造代码以实现这一点的方法)之间的区别。
无数程序员试图通过使用 goroutine 并行化来提高代码速度,但常常对由此产生的速度减慢感到困惑。如果底层问题本质上是并行的,比如处理 HTTP 请求,那么并行代码只会在并行化时运行得更快。我们在解释这一点上做得很糟糕,结果让许多程序员感到困惑,并可能赶走了一些程序员。
为了解决这个问题,2012 年,我在 Heroku 的 Waza 开发者大会上发表了演讲 ,名为“并发不是并行”。这是一个有趣的演讲,但应该早点发生。
对此表示歉意。但好处仍然存在:Go 帮助普及了并发性作为构建服务器软件的一种方式。
我认为接口是 Go 中设计得最好的东西之一。而且很明显,接口和并发性是 Go 中的一个独特的想法。它们是 Go 对面向对象设计的回答,采用原始的、以行为为中心的风格,尽管新来者不断推动结构体承载这种负载。
使接口动态化,无需提前宣布哪些类型实现它们,这困扰了一些早期的批评者,并且仍然激怒了一些人,但这对于 Go 所培育的编程风格很重要。标准库的大部分内容都是 建立在它们的基础上的,而更广泛的主题(例如测试 和管理依赖项)在很大程度上依赖于它们慷慨的“ 欢迎所有人”的性质。
这里有一个故事要讲。在罗伯特和我办公室的那个著名的第一天,我们问了一个问题:如何处理多态性。Ken 和我从 C 语言中知道 qsort 可以作为一个困难的测试用例,因此我们三个开始讨论我们的“胚胎语言”如何实现类型安全的排序例程。
罗伯特和我几乎同时提出了相同的想法:使用类型上的方法来提供排序所需的操作。这个概念很快就发展成为这样的想法:值类型具有行为,定义为方法,并且方法集可以提供函数可以操作的接口。Go 的接口几乎立刻就出现了。
这是经常被忽视的事情:Go 的排序是作为在接口上运行的函数实现的。这不是 大多数人熟悉的面向对象编程风格 ,但它是一个非常强大的想法。
当 Russ 加入时,他很快就指出了 I/O 如何完美地融入这个想法,并且库的发展很快,很大程度上基于三个著名的接口:empty、Writer 和 Reader,平均持有三分之二的数据、各一个方法。 这些微小的方法是 Go 的惯用方法,而且无处不在。
接口的工作方式不仅成为 Go 的显著特征,还成为我们思考库、通用性和组合的方式。这是令人兴奋的事情。但问题就产生在这里。你看,我们走这条路,很大一部分原因是我们经常看到泛型编程鼓励一种这样一种思维方式:倾向于关注类型而不是算法、关注早期的抽象而不是有机设计、关注容器而不是函数。
我们用语言本身定义了通用容器——映射、切片、数组、通道——但没有让程序员访问它们所包含的通用性。这可以说是一个错误。虽然这些类型可以很好地处理大多数简单的编程任务。但让人感到困扰的是,语言提供的内容和用户可以控制的内容之间存在障碍。
简而言之,这种思维方式陈年已久,我们花了十多年的时间才纠正过来。Ian Taylor 从一开始就敦促我们面对这个问题,但考虑到接口作为 Go 编程的基石,这很难做到。
批评者经常抱怨我们应该只做泛型,因为它们 很“简单”,也许它们可以用在某些语言中,但接口的存在意味着任何新形式的多态性都必须考虑它们。找到一种与该语言的其余部分良好配合的前进方式需要多次尝试、几次中止的实现以及数小时、数天和数周的讨论。
最终我们请来了一些由 Phil Wadler 领导的类型理论家来帮忙。即使在今天,语言中已经有了可靠的通用模型,但接口作为方法集的存在仍然存在一些挥之不去的问题。
如您所知,最终的答案是设计一个接口的泛化,可以吸收更多形式的多态性,从“方法集”过渡到“类型集”。这是一个微妙但意义深远的举措,大多数社区似乎都同意这一举措,但我的怀疑态度并未打消。
有时要花很多年的时间才能搞清楚一件事,甚至搞清楚“自己原来没太搞清楚”这件事。然后继续前进。
顺便说一句,我希望我们有一个比“泛型”更好的术语,它起源于一种不同的、以数据结构为中心的多态性风格。“参数多态性”是 Go 提供的内容的正确术语,但它有些拗口。但我们平时提“泛型”,但也不太准确。
要想成功,Go 必须是一个开源项目。但在我们弄清楚关键想法并进行有效实施之前,私下开发会更有成效。因此,前两年我们需要这个过程不受干扰。
向开源的过渡是一个巨大的变化,而且具有教育意义。社区的投入是巨大的。与社区互动需要花费大量的时间和精力,尤其是对于 Ian 来说,他竟然还能抽出时间来回答任何人提出的每个问题。但它带来了更多。我仍然对 Windows 移植的速度如此之快感到惊讶,这完全是由社区在 Alex Brainman 的指导下完成的。
我们花了很长时间才理解转向开源项目的含义以及如何管理它。我们说服了支持我们的社区,坚持通过强制代码审查和对细节的全面关注来保持高代码质量,而不是采用快速接受代码并在提交后进行清理的方式。这种做法将更多的工作推回给社区,需要他们了解其价值,否则他们不会感到受到应有的欢迎。
有一个历史细节并不广为人知。该项目有 4 个不同的内容管理系统:SVN、Perforce、Mercurial 和 Git。感谢Russ为项目所做的历史内容管理系统的维护工作。
此外,Go语言项目是独立于Goole之外的,尽管Google提供了支持,但核心团队是独立的,没有设定议程。Google 拥有庞大的内部 Go 代码库,团队用它来测试和验证版本,但这是通过从公共存储库导入 Google 来完成的。
Go语言的包管理开发过程做得并不好,关键在于使用纯字符串来指定导入语句中的路径,虽然提供了灵活性,但也带来了很多问题。
Go核心团队在早期缺乏处理具有大量包版本的包管理器以及解决依赖关系图等复杂问题的经验。尽管如此,我们仍然试图让社区参与解决依赖管理问题,但最终设计出来时,许多社区成员感到被轻视。
这次失败给团队上了一课,让我们更加了解如何真正与社区互动。尽管道路崎岖不平,但现在事情已经得到了解决,出现的设计在技术上非常出色,并且对大多数用户来说效果很好。
推出 14 年后,我们最后成功了。总的来说,很大程度上是因为最初把设计和开发 Go 作为一种编写软件的方式(而不仅仅是作为一种编程语言)这项决定,才带领我们来到了这样一个新地方。
我们到达这里的部分原因是:
最重要的是,得益于令人难以置信的、乐于助人、且多元化的 Gophers 社区的支持。