本文根据我 2023 年 11 月 10 日在悉尼 GopherConAU 大会上的闭幕演讲(视频)整理而成。Go 语言作为开源项目推出已整整 14 年。文中图片来自我演讲时使用的幻灯片。
简介
大家好。
首先我要感谢 Katie 和 Chewy 给我这次机会,让我做闭幕式演讲。
2009 年 11 月 10 日
今天是 2023 年 11 月 10 日,Go 语言作为开源项目推出已整整 14 年。
如果我没记错的话,那天下午,加利福尼亚时间 3 点,Ken Thompson、Robert Griesemer、Russ Cox、Ian Taylor、Adam Langley、Jini Kim 和我,满怀期待地看着这个网站上线,全世界都知道了我们在做什么。
14 年了,值得回顾的事情有很多。今天,我想借此机会给大家分享下我们从那天起汲取的一些主要的经验和教训。即使是最成功的项目,在回过头来看时,也会有美中不足之处。当然,事后来看,那可能是他们取得成功的关键。
首先,我要说明一下,我在这里所说的内容只代表我自己,不代表 Go 团队或谷歌。Go 有一个专门的团队和一个庞大的社区,Go 的发展离不开他们付出的巨大努力,以前是,今后也是。因此,如果你同意我说的任何话,请感谢他们。如果你不同意,可以责怪我,但请不要告诉别人。
看到这个题目,许多人可能会期待我分析这门语言的好与坏。当然,我会做些分析,但其他内容会更多,原因如下。
首先,说一门语言的好与坏,更多的是观点而非事实。不管是对于 Go 语言,还是其他语言,即使是最微不足道的特性也会存在争议。
其次,关于这方面的讨论已经很多了,比如,关于换行符的位置、nil 的工作机制、export 时大写、垃圾收集、错误处理,等等。无疑,关于这些话题,要说的东西很多,但几乎都已经有人说过了。
但是,我之所以要谈更多语言之外的东西,真正的原因在于,那些话题不是这个项目的全部。我们最初的目标是创建一门新的编程语言,创建一种更好的软件编写方式。我们使用的语言有问题——每个人都会遇到,不管他使用什么语言——但我们遇到的一些深层次问题对那些语言的核心特性而言并不重要,但对于我们在谷歌创建的、用于构建软件的流程来说则至关重要。
印在 T 恤上的第一个 Gopher
创建新语言为我们开辟了探索其他想法的路径,但这只是一个推动因素,而非重点。如果那时我构建二进制文件不需要 45 分钟,Go 可能就不会出现,但那 45 分钟并非因为编译器慢,它不慢,也不是因为语言写的差,它不差。慢是由其他因素导致的。
这些因素正是我们希望解决的问题:构建现代服务器软件的复杂性、控制依赖关系、与人员不断变动的大型团队一起编程、易于维护、高效测试、充分利用多核 CPU 和网络,等等。
简而言之,Go 不只是一门编程语言。当然,它是一门编程语言,那是它的定义,但它的目的是提供一种更好的方法,让我们可以开发出高质量的软件,至少对于我们 14 多年前的环境来说是如此。
直至今天,那仍然是它的意义所在。Go 是一个简化生产软件构建、提高生产力的项目。
几周前,当我开始准备这次演讲时,我起了一个题目,和这个略有不同。为了继续这项工作,我在 Mastodon 上征询了其他人的意见,有不少人回复了。在这些回复中,我注意到了一个趋势:人们认为我们犯的错都在语言上,而我们做对的地方则是在一个更大的故事中,在语言周边的东西中,如 gofmt、部署、测试。说实在的,我觉得这很鼓舞人心。我们的努力似乎已经有了效果。
但有一点得承认,我们一开始并没有明确真正的目标是什么。也许我们觉得那是不言而喻的。为了解决这个问题,我在 2013 年的 SPLASH 大会上做了一个题为“Go 在谷歌:为软件工程服务的语言设计”的演讲。
Go 在谷歌
对于 Go 为什么会出现,那次演讲以及相关的博文或许是最好的说明。
今天的演讲可以说是 SPLASH 那次演讲的延续,回顾下我们过去构建这门语言时汲取的经验教训,并投身于更为广阔宏伟的目标。
那么接下来……一些经验教训。
当然,首先,我们有 Gopher。
The Gopher
从 Gopher 开始说起似乎很老套,但 Go Gopher 是 Go 取得成功之前最早考虑的因素之一。早在发布之前我们就知道,我们需要一个吉祥物来装饰我们的宣传品(schwag)——每个项目都需要宣传品——Renee French 愿意帮我们设计一个。无疑,我们做得非常正确。
下面这幅图片里的地鼠毛绒玩具是第一个 Gopher 实物。
The Gopher
下面这幅图里有 Gopher 的第一版原型,不那么成功。
Gopher 和未完全进化的祖先
Gopher 是一个吉祥物,是一个荣誉徽章,甚至是 Go 程序员的身份标识。此时此刻,你正在参加的这个会议叫 GopherCon,这样的会议有很多,这只是其中的一场。从第一天起就有一个高识别度的有趣生物准备分享信息,这对于 Go 的发展至关重要。它貌似憨态可掬,实则聪明伶俐——它能构建任何东西。
Gopher 在构建一个机器人( Renee French 绘制)
—— 这奠定了社区参与项目的基调,这是一个技术卓越同时又乐趣非凡的项目。最重要的是,Gopher 成了社区的一面旗帜,一面把大家团结在一起的旗帜,尤其是在 Go 刚刚踏入编程世界的初期。
下图是几年前 Gopher 们在巴黎参加一个大会。看看,他们是多么兴奋!
巴黎的 Gopher 观众(照片由 Brad Fitzpatrick 拍摄)
话虽如此,在知识共享许可协议下发布 Gopher 设计似乎并不是最好的选择。一方面,这可以鼓励人们以有趣的方式修改它,而这反过来又有助于培养社区精神。
Gopher 造型表
Renee 创建了一个“造型表”,这既能帮助艺术家们对它进行修改,又能让它忠于自己的精神。
有不少艺术家乐此不疲地摆弄这些特征,创作出了自己的 Gopher 版本;Renee 和我最喜欢的是日本设计师 @tottie 的作品:
@tottie 的 Gopher
以及游戏程序员 @tenntenn 的作品:
@tenntenn 的 Gopher
但令人沮丧的是,许可的“归属”经常会引发争论,或者有人会把非 Renee 创作的形象,以及不符合原作精神的形象说成是她的。说实话,人们经常不愿意或根本不尊重署名。举个例子,有家公司使用了 @tenntenn 的 Gopher 插图,我怀疑他未必收到了补偿甚至感谢。
gophervans.com
因此,如果重新来做的话,我们可能得仔细想想如何确保吉祥物保持它的理想形象。这是一个难题,维护一个吉祥福的方案很费捉摸。
不过接下来,让我们看一些更偏技术的东西。
做对了的地方
下面这个列表是我们认为自己做对了的地方。不是每种语言都做了这些事,但每一件都对 Go 最终的成功至关重要。都是些大家熟悉的话题,我会尽量简单点。
1. 规范。我们是从一个正式的规范开始的。这不仅可以在编写编译器时锁定行为,还可以使多个实现共存并且行为一致。单独的编译器并不是规范。你在测试编译器时是依据什么进行的?
规范,就像网上看到的那样
顺便说一下,规范的初稿是在这里写的,在悉尼达令港一栋大楼的 18 层上。我们现在是在 Go 的家乡庆祝 Go 的生日。
2. 多个实现。Go 有多个编译器,都实现了相同的规范。有一份规范在那里,这更容易做到。
有一天,Ian Taylor 给我们发了一封邮件,说他已经读过规范草稿,并自己编写了一个编译器。这让我们大吃一惊。
Subject: A gcc frontend for Go
From: Ian Lance Taylor
Date: Sat, Jun 7, 2008 at 7:06 PM
To: Robert Griesemer, Rob Pike
Ken Thompson
我办公室的一位同事指给我看了 http://…/go_lang.html 。它看上去是一门有趣的语言,我匆忙给它写了个 gcc 前端。当然,可能会缺失许多特性,但它可以编译网页上的基本筛选代码。
这令人震惊,但后面还有更多,所有这些都是因为有一份正式的规范。
许多编译器
这既可以帮助我们优化语言,打磨规范,又可以让那些对我们的方法不是特别感冒的人可以选择其他的环境。(后面还会详细介绍)
如今已有许多兼容的实现,那很棒。
3. 可移植性。我们使交叉编译变得很简单,这样程序员就可以在他们喜欢的任何平台上开发,并迁到任何需要的平台上。这在 Go 中可能比在其他任何语言中都更简单。人们很容易把编译器看成是本机原生的,但它没有理由那么做。打破这个假设令人震撼,对于许多开发人员来说都成了新闻。
可移植性
4. 兼容性。我们努力开发,让它达到了 1.0 版本。然后,为了保证兼容性,我们锁定了这个版本。这对于 Go 的广泛采用有着很大的影响,我不明白为什么其他项目都拒绝这样做。是的,维护强兼容性是有成本的,会妨碍功能,但在一个几乎没有什么保持稳定的世界里,不用担心 Go 的新版本会破坏你的项目会令人非常愉快。
Go 的兼容性承诺
5. 库。它的发展有点偶然,因为开始时没有其他地方可以安装 Go。这个库可靠、有效,其中几乎包含编写 21 世纪服务器代码所需的一切。它成了一项主要的资产。在我们积累起足够的经验并懂得应该提供其他的什么东西之前,整个社区都一直在使用同样的工具包。这真的很有效,防止了库的各种变体,维护了社区的统一。
库
6. 工具。我们确保该语言易于解析,使工具构建成为可能。起初,我们觉得 Go 需要一个 IDE,而简易工具意味着 Go IDE 会逐步出现。确实,它们随着 gopls 出现了,而且非常棒。
工具
我们还在编译器中提供了一系列辅助工具,如自动化测试、覆盖率和代码审查。当然还有 Go 命令,它整合了整个构建过程,许多项目只用它就可以完成 Go 代码的构建和维护。
快速构建
而且,这也没有影响 Go 的快速构建。
7. Gofmt。我将 gofmt 从 Go 的工具箱里单拎了出来,因为这个工具不仅在 Go 语言上打下了自己的印记,还在整个编程社区打下了自己的印记。在 Robert 编写 gofmt 之前(顺便提一下,他坚持从头写起),自动格式化工具的质量不高,因此几乎都没人用。
Gofmt 谚语:Gofmt 的风格没人喜欢,但 Gofmt 却是每个人的最爱
Gofmt 已经证明它可以做得很好,而现如今,几乎每一种语言都有必要使用一种标准的格式化器。有了格式化器,我们就不必再为空格和换行而争论。它节省的时间值得我们去定义一个标准的格式,并编写一段复杂的代码来实现自动格式化。
此外,gofmt 为其他无数的工具提供了可能,如简化器、分析器,甚至是代码覆盖工具。因为 gofmt 的 guts 变成了一个任何人都可以使用的库,所以你可以用它解析程序,编辑 AST,也可以只是打印 byte-perfect 输出,供人类和机器使用。
感谢 Robert。
下面说一些比较有争议的话题。
并 发
并发有争议?好吧,2002 年我加入谷歌的时候确实如此。John Ousterhout 曾写过一篇很著名的文章说线程很糟糕。许多人都同意他的说法,因为线程似乎很难使用。
John Ousterhout 不喜欢线程
谷歌的软件几乎总是避免使用它们,几乎完全禁止它们,发布禁令的工程师引用了 Ousterhout 的话。这让我很困扰。自 20 世纪 70 年代以来,我一直在做类似于并发的事情,有时甚至没有意识到这一点。在我看来,它似乎很强大。经过仔细思考之后我发现,Ousterhout 犯了两个错误。首先,他笼统的表述超出了他使用线程的领域;其次,他抱怨的主要是要通过笨拙的低级包(如 pthread)使用线程,而不是其基本思想。
像这样把解决方案和问题混淆是工程师们普遍会犯的错误。有时候,提出的解决方案比它所要解决的问题都难,而且很难看到有更容易的途径。不过,我跑题了。
根据我的经验,我知道有更好的方法来使用线程,或者不管我们怎么称呼它们,我甚至在 Go 之前做了一个关于线程的演讲。
Newsqueak 中的并发
但并非只有我知道这一点;许多其他语言和关于并发编程的论文甚至书籍都表明它可以做得很好。它只是还没有成为主流思想,Go 的诞生部分就是为了解决这个问题。在那个堪称传奇的 45 分钟构建中,我试图向一个非多线程二进制文件种添加一个线程,其困难程度令人沮丧,因为我们使用了错误的工具。
回顾过去,我认为可以公平地说,Go 在说服编程界并发是一个强大的工具方面发挥了重要作用,特别是在多核网络世界中,并且它可以比 pthread 做得更好。现在,大多数主流语言都有了很好的并发支持。
Google 3.0
而且,Go 版本的并发性有其新颖之处,至少在导致它产生的语言中是这样,因为它使 goroutine 变得平淡无奇。没有 coroutine,没有任务,没有线程,没有名称,只有 goroutine。我们发明了“goroutine”这个词,因为现有的术语没有合适的。直到今天,我都希望 Unix 的 spell 命令能够学它。
说句题外话,因为我经常被问到这个问题,所以请允许我聊一下 async/await。Async/await 模型及其相关风格是许多语言选择支持的并发方式,这让我有点难过,不过,相比 pthread,它绝对是巨大的改进。
从语言实现者的角度来说,async/await 比 goroutine、channel 和 select 更容易构建,也更小,可以通过改造现有平台来实现。但是它把一些复杂性推给了程序员,经常导致 Bob Nystrom 所说的“彩色函数”。
你的函数是什么颜色的?
我认为,Go 证明 CSP(这是一个比较老的不同的模型)非常适合于不存在这种复杂性的过程式语言。作为一个库,我甚至好几次见人这样做。但是它的实现,如果还要实现得比较好,运行时的复杂性就会很高,这样就可以理解为什么有些人不愿意在他们的系统中构建它。但是,无论你提供什么并发模型,都必须只提供一个,因为提供多个并发实现的环境可能会出现问题。当然,在这个问题上,Go 将其放在了语言中,而不是一个库。
探讨这些问题可能需要一整场演讲,但现在我们先聊这些就够了。
[题外话到此为止]
并发的另一个价值是,它让 Go 看起来像是一个新东西。正如我所说,以前也有一些其他语言支持并发,但它们从未能成为主流,而 Go 的并发支持是早期采用的主要推动力,那吸引了以前没有使用过并发但对其可能性感兴趣的程序员。
这里,我们犯了两个严重的错误。
窃窃私语的 Gopher(顺序进程协同)
首先,并发很有趣,我们很高兴拥有它,但我们想到的用例主要是服务器端的东西,也就是说在 net/http 等关键库中实现,而不是在每个程序中都实现。因此,许多程序员使用它时并不清楚并发给他们提供了什么帮助。我们应该事先说明,语言中的并发支持真正带来的是简化了服务器软件。这个问题对很多人来说很重要,但并不是对所有尝试 Go 的人都知道,没有提供这方面的指导责任在我们。
第二,我们花了很长时间来澄清并行性(parallelism,在多核机器上并行支持多个计算)和并发性(concurrency,一种构建代码的方式,为的是更好地完成多个计算)之间的区别。
并发非并行
无数程序员试图通过 goroutine 来实现并行,从而提高代码的速度,但常常却被由此所导致的速度变慢所困扰。如果所要解决的问题本质上是并行的,比如服务于 HTTP 请求,那么并发代码只有在并行化时才会更快。关于这一点,我们没能做好解释,让许多程序员感到了困惑,甚至可能导致了一些人的离开。
为了解决这个问题,2012 年,我在 Waza(Heroku 开发大会)上做了一个题为“并发非并行”的演讲。这是一个有趣的话题,但应该早点讨论。
很抱歉。但是还是有好的一面:Go 让并发作为服务器软件的构建方式而流行开来。
接口
很明显,并发接口是 Go 语言独有的一个概念。它们是 Go 对面向对象设计的回答,采用原始的、以行为为中心的风格,尽管后来者不断推动让结构体承担这种负担。
使接口动态化,不需要提前声明实现它们的类型,这对一些早期的批评者造成了困扰,并且还激怒了一些人,但这对 Go 所培养的编程风格来说很重要。许多标准库都是以它们为基础构建的,还有一些更为广泛的主题,如测试和依赖关系管理,都在很大程度上依赖了它们慷慨的“欢迎所有人”的性质。
我觉得,接口是 Go 中设计得最好的东西之一。
除了早期几次关于数据是否应该包含在定义中的讨论外,接口从诞生第一天起就完全定型了。
一个 GIF 解码器:Go 接口练习(Rob Pike 和 Nigel Tao,2011 年)
这里有一个故事。
在发布那天,我们问了一个问题,即如何处理多态性。Ken 和我从 C 语言中得知,qsort 可以作为一个有难度的测试用例,所以我们三个人开始讨论如何用我们这门刚诞生的语言实现类型安全的排序例程。
Robert 和我几乎同时提出了同一个想法:使用类型上的方法来提供排序所需的操作。这个概念很快就发展成了这样一种想法,即值类型具有行为,定义为方法,并且方法集提供的接口可以作为函数开发的依据。至此,Go 接口几乎是马上就出现了。
排序接口
人们一般都认可:Go 的 sort 是基于接口实现的函数。这不是大多数人熟悉的面向对象编程风格,但这是一个非常强大的想法。
这个想法让我们很兴奋,它成为一个基础编程结构的可能性令人陶醉。当 Russ 加入时,他很快就指出如何在这个想法中融入 I/O,于是很快这个库就出现了。它在很大程度上基于三个著名的接口:empty、Writer 和 Reader,其中包含了每个接口平均三分之二的方法。这些小方法是 Go 的惯用方法,它们无处不在。
接口的工作机制不仅成了 Go 的一个显著特征,而且成了我们思考库、通用性和组合的方式。那是个令人陶醉的东西。
但我们也许不该就此打住。
你知道,我们沿着这条路走下去,至少有一部分原因是因为我们经常看到,泛型编程促进了先于算法关注类型的思维方式。早期抽象取代有机设计。容器取代函数。
我们在语言中定义了泛型容器——map、slice、array、channel——而没有让程序员访问它们所包含的泛型。这可以说是一个错误。我们相信,我还是这么认为,这些类型可以很好地处理大多数简单的编程任务。但也有一些不能,语言提供的东西和用户可控制的东西之间存在的障碍确实让一些人感到困扰。
简而言之,尽管我不会改变接口的工作机制,但我们花了十多年的时间来纠正它对我们思维方式的影响。Ian Taylor 从一开始就敦促我们直面这个问题,但考虑到接口是 Go 编程的基石,这很难做到。
批评者经常抱怨说,我们应该只做泛型,因为它们很“简单”。也许在某些语言中可以这样,但接口的存在意味着任何新形式的多态性都必须考虑它们。找到一种与语言的其余部分完全契合的前进之路需要多次尝试,数次夭折的实现,以及数小时、数天甚至数周的讨论。最后,我们找来了一些类型理论专家来帮忙,由 Phil Wadler 牵头。即使如今,这门语言中已经有了可靠的泛型模型,仍然存在一些与接口(作为方法集)有关的遗留问题。
通用排序规范
正如你所知,最终的答案是设计通用接口,可以容纳更多形式的多态性,从“方法集”过渡到“类型集”。这是一个微妙而影响极大的举动,尽管我怀疑抱怨声永远不会停止,但大多数社区成员似乎对此很满意。
有时候,你需要过很多年才能弄明白一些事情,甚至是最终发现自己无法弄明白。但你还是要继续下去。
顺便说一下,我希望我们能有一个比“泛型”好点的术语。起初,泛型是指一种与众不同的、以数据结构为中心的多态风格。“参数多态性”是 Go 提供的一个更合适的术语,虽然有点拗口,但更准确。但是,我们一般都说“泛型”,尽管那并不完全正确。
编译器
有一件让编程语言社区颇为困扰的事是,早期的 Go 编译器是用 C 编写的。在他们看来,正确的方法是使用 LLVM 或类似的工具包,或者用 Go 自己编写编译器,这个过程称为自托管(自举)。我们没有用这两种方法,有几个原因。
首先,开发一门新语言时至少第一步是要用现有语言完成编译器。对我们来说,C 是显而易见的选择,因为 Ken 已经编写了一个 C 编译器,而且它的内部结构对于 Go 编译器开发是一个很好的基础。此外,用自己的语言编写编译器,同时开发该语言,往往会产生一种适合编写编译器的语言,但这不是我们追求的。
早期的编译器运转良好。它很好地推动了语言的发展。但有点奇怪的是,它实际上是一个 Plan 9 风格的编译器,在编写过程中延用了旧的理念,而不是像静态单赋值这样的新思想。生成的代码比较一般,内部结构也不是很漂亮。但它实用和高效,编译器代码本身的大小适中,我们大家都很熟悉,这使得我们很容易尝试新想法并快速做出改变。一个关键步骤是添加自增长的分段堆栈。我们很容易就将其添加到了编译器中,但如果我们一直使用像 LLVM 这样的工具包,那么考虑到要对 ABI 和垃圾收集器支持做必要的更改,我们就无法将更改集成到完整的编译器套件中了。
另一个效果良好的领域是交叉编译,那本就是原 Plan 9 编译器套件的工作方式。
按照我们自己的方式去做,不管多么不正统,这能帮助我们快速前进。有些人对这个选择感到不快,但在当时,这对我们来说是正确的选择。
Go 1.5 之后的 Go 编译器架构
对于 Go 1.5 版本,Russ 编写了一个工具,用半自动的方式将编译器从 C 语言转换为 Go 语言。那时,这门语言已经完成了,不用再担心把语言设计成面向编译器的语言了。网上有关于这个过程的讨论,值得一看。2016 年,我在 GopherCon 上做了一个关于汇编器的演讲,这是我一生追求可移植性的一个高潮。
Go 汇编器设计 (GopherCon 2016)
我们做了正确的事情,从 C 开始,但最终将编译器翻译成 Go。这使我们能够将 Go 所具有的所有优势带入其开发,包括测试、工具、自动重写、性能分析等等。现在的编译器比原来的干净需多,生成的代码也好得多。但是,当然,这就是自我引导的工作方式。
请记住,我们的目标不只是一门语言,我们的目标比这个大得多。
我们的做法有些与众不同,但这绝不是要冒犯 LLVM 或语言社区的任何人。我们只是根据自己的任务选用了最适合的工具。当然,如今已经有了一个 LLVM 托管的 Go 编译器,还有许多其他的编译器,事情就应该是这样的。
项目管理
我们从一开始就知道,要想成功,Go 就必须是一个开源项目。但我们也知道,在我们理清思路并提出可行的实施方案之前,私下开发会更有成效。在头两年里,不受干扰地理清我们要实现的目标至关重要。
向开源过渡是一个巨大的变化,而且很有教育意义。来自社区的反馈让我们应接不暇。参与社区工作需要花费大量的时间和精力,特别是对 Ian 来说,不知道他是怎么抽出时间回答了每个人提出的问题。但那也为我们带来了很多。我仍然惊叹于 Windows 移植的速度之快,那完全是由社区在 Alex Brainman 的指导下完成的。太神奇了。
我们花了很长时间来理解转为开源项目可能带来的影响,以及如何管理这个项目。
特别是,公平地说,我们在理解与社区合作的最佳方式方面花了太长时间。这主要是因为我们沟通不畅——即使我们自认为沟通得很好——由于误解和期望失配,我们浪费了很多时间。我们本来可以做得更好。
然而,随着时间的推移,我们说服了社区,至少是站在我们这边的那部分。尽管不同于通常的开源方式,但我们的一些想法还是有价值的。最重要的是,我们坚持通过强制性代码审查和对细节详尽而完整的关注来保持高代码质量。
任务控制(Renee French 绘制)
有些项目采用了不同的工作机制,它们会快速接受代码,并在提交后进行清理。Go 项目采用了另一种方式,优先考虑质量。我相信这是一种更有效的方式,但这会把更多的工作推给社区,他们需要了解其价值,否则他们就会感到不受欢迎。关于这一点,我们还有很多东西需要学习,但我相信现在情况已经好多了。
顺便说一下,有一个历史细节并不广为人知。该项目有 4 个不同的内容管理系统:SVN、Perforce、Mercurial 和 Git。Russ 做了一项艰巨的工作,使所有的历史都保持活跃,所以即使在今天,Git 存储库也包含了 SVN 中最早的更改。我们都相信保留历史是有价值的,感谢他做了这项繁重的工作。
还有一点。人们通常认为谷歌会告诉 Go 团队该做什么。这根本不是真的。谷歌对 Go 的支持非常慷慨,但并没有制定日程。社区的投入要多得多。谷歌有一个巨大的内部 Go 代码库,团队使用它来测试和验证发布,但这是通过从公共仓库导入到谷歌来完成的,而不是反过来。简而言之,Go 核心团队由谷歌发工资,但他们是独立的。
包管理
为 Go 开发包管理器的过程,我们做得并不好。我相信,该语言本身的包设计非常出色,我们花了大约一年的时间来讨论。如果你感兴趣的话,可以观看我之前提到的 SPLASH 演讲,其中详细解释了它的工作机制。
关键的一点是在 import 语句中使用普通字符串来指定路径。在我们看来,这提供了重要的灵活性。但是,从只有一个“标准库”到从 Web 导入代码的转变需要克服许多困难。
修理云朵(Renee French 绘制)
问题有两个。
首先,我们这些 Go 核心团队的早期成员熟悉谷歌的运作方式、熟悉它的单体库和每个人构建的东西。但是,对于使用包管理器来处理很多包的很多版本,以及解析依赖关系这个难题,我们没有足够的经验。直到今天,依然很少有人真正理解这项技术的复杂性,但这并不能作为我们没有从一开始就解决这些问题的借口。这令人特别尴尬,因为我曾是一个失败项目的技术主管,为谷歌的内部构建做了类似的事情,我应该意识到我们面临的问题是什么。
deps.dev
我在 deps.dev 上做的工作算是一种忏悔吧。
其次,让社区参与进来帮助解决依赖管理问题的初衷是好的,但是当最终设计出来时,即使有大量介绍相关理论的文档和文章,社区中的许多人还是感到被忽视了。
https://commandcenter.blogspot.com/2024/01/what-we-got-right-what-we-got-wrong.html
这次失败给团队上了一课,让我们知道应该如何与社区互动,从那以后,我们的工作得到了很大的改善。
不过,事情现在已经解决了,已经有技术上很优秀的设计,似乎对大多数用户来说也都很好用。只是花了太长时间,路太曲折。
文档和示例
另一件我们事先没有做好的事情是文档。我们编写了很多文档,并认为自己做得很好,但我们很快就发现,社区想要的文档程度与我们预期的不同。
Gopher 在维修一台图灵机 (Renee French 绘制)
我们的文档缺的主要是例子,甚至是最简单函数的例子。我们认为我们所要做的就是说明白某个东西做了什么;我们花了很长时间才接受这样一个事实,即文档展示如何使用它们会更有价值。
可执行示例
不过,我们吸取了教训。现在文档中有了很多例子,大部分是由开源贡献者提供的。我们很早就做了的一件事是让它们可以在 Web 上执行。2012 年,我在 Google I/O 大会上做了一个演讲,展示了并发的实际应用,Andrew Gerrand 写了一个有趣的 Web 软件,让我们可以直接在浏览器上运行代码片段。我怀疑这是第一次有人这样做,但 Go 是一种编译语言,许多人以前从未见过这种方式。后来,该技术被部署到了博客和在线包文档中。
Go 游乐场
也许更重要的是它被部署到了 Go 游乐场,这是一个免费的开放沙盒,供人们尝试甚至开发代码。
小结
我们已经走过了漫漫长路。
回顾过去,有很多事情我们显然做对了,它们帮助 Go 取得了成功。但也有很多事情我们本可以做得更好,重要的是承认这些错误并从中吸取教训。任何主持重要开源项目的人都会有这两方面的经验和教训。
希望我对这些教训及其原因的回顾能对大家有所帮助,对于那些对我们的做法持反对态度的人,这算是一个道歉,而对于希望了解我们的做法的人,这算是一个解释。
GopherConAU 2023 吉祥物(Renee French 设计)
但在发布 14 年后,我们现在的状态,公平地说,总体上相当不错。
在很大程度上,这是因为我们在设计和开发过程中做出的决定,将 Go 作为一种编写软件的方式——而不仅仅是作为一种编程语言——我们开辟了一片新天地。
我们能走到今天,有一部分原因是:
- 一个强大的标准库,实现了服务器代码所需的大部分基础特性
- 将并发性作为语言的一等组件
- 一种基于组合而不是继承的方法
- 可以澄清依赖关系管理的打包模型
- 集成快速构建和测试工具
- 严格一致的格式
- 关注可读性胜过聪明度
- 兼容性保证
而且,最重要的是,有一个令人难以置信、乐于助人的多样化 Gopher 社区的支持。
多样化社区(@tenntenn 绘制)
也许这些事情最有趣的结果是,不管谁编写,Go 代码的风格和工作方式都是一样的,基本上没有使用语言不同子集的派别,并且可以保证不管过多少时间都可以编译和运行。对于一门主要的编程语言来说,这可能是第一次。
我们无疑是做对了。
谢谢!
原文链接:
https://commandcenter.blogspot.com/2024/01/what-we-got-right-what-we-got-wrong.html