向 Go 2 进发
Russ Cox
2017 年 7 月 13 日
介绍#
[这是我在 Gophercon 2017 的 我今日之演讲 的讲稿文本,我们与整个 Go 社区讨论关于 Go 2 的计划。]
2007 年 9 月 25 日,在 Rob Pike,Robert Griesemer 和 Ken Thompson 讨论了一种新的编程语言几天之后,Rob 提出了 “Go” 这个名字。
第二年,Ian Lance Taylor 和我加入了团队,我们五个人共同构建了两个编译器和一个标准库,最终在 2009 年 11 月 10 日发布了 开源发布 。
接下来的两年中,在新的 Go 开源社区的帮助下,我们尝试了各种改进,完善了 Go 并制定了 Go 1 计划 ,在 2011 年 10 月 5 日发布。
在 Go 社区的帮助下,我们修订并实施了该计划,最终于 2012 年 3 月 28 日 发布了 Go 1 。
Go 1 的发布标志着将近五年的极富创造性的、疯狂的努力达到了顶峰,这使我们从名称和一堆想法变成了稳定的可供生产环境使用的编程语言。这也标志着从变化和流失到稳定的明显转变。
在开发 Go 1 的那些年中,我们改变了 Go ,几乎每周都打破 Go 的标准。我们了解到,这使 Go 不能在生产环境中使用,因为生产环境无法每周重写程序以适应语言变化。正如 宣布 Go 1 发布的博客文章 所说的那样,其驱动动机是为创建可靠的产品、项目和出版物 (博客,教程,会议演讲和书籍) 提供稳定的基础。使用户确信自己的程序在未来几年内将继续编译并运行而无需更改。
Go 1 发布后,我们知道我们需要花时间去为生产环境优化 Go。我们明确地从改变语言转变为在自己的项目中使用 Go 并改进实现:我们将 Go 移植到许多新系统上,我们重写了几乎所有对性能至关重要的部分,以使 Go 更加高效地运行,并添加了诸如 竞态探测器 。
现在,我们已有 5 年使用 Go 来构建大型,生产质量系统的经验。我们已经了解了什么有效,什么无效。现在是时候开始 Go 的发展和增长的下一步,计划 Go 的未来了。我今天在这里请求所有 Go 社区中的所有人,无论您是 GopherCon 的听众还是今天晚些时候观看视频或阅读 Go 博客的观众,在我们计划和实施 Go 2 时与我们一起工作。
在本讲的其余部分,我将解释 Go 2 的目标;我们的制约和局限;整个向 Go 2 前进的过程;写下使用我们使用 Go 的经验,尤其是与我们可能尝试解决的问题有关;可能的解决方案;我们将如何交付 Go 2;以及大家如何提供帮助。
目标#
今天,Go 的目标与 2007 年相同。我们希望使程序员更有效地管理两种规模:生产规模,尤其是与许多其他服务器交互的并发系统,今天以云软件为例;以及开发规模,特别是许多工程师松散地协调工作而开发的大型代码库,今天的现代开源开发就是例证。
这些规模出现在各种规模的公司中。即使是五人创业公司,也可能会使用其他公司提供的基于云的大型 API 服务,并且使用的开源软件要比他们自己编写的软件更多。生产规模和开发规模在该初创公司中的重要性与在 Google 中一样重要。
Go 2 的目标是修复 Go 无法规模化的最重要的方式。
(有关这些目标的更多信息,请参见 Rob Pike 在 2012 年的文章 “Go ta Google: 软件工程服务中的语言设计” 和我的 GopherCon 2015 演讲 “Go,开源,社区。”)
约束#
Go 的目标从一开始就没有改变,但是 Go 的约束肯定已经改变。最重要的限制是现有的 Go 使用情况。我们估计全世界至少有 50 万个开发人员,这意味着有数百万个 Go 源文件和至少十亿行 Go 代码。这些程序员和源代码代表了 Go 的成功,但它们也是 Go 2 的主要限制。
Go 2 必须带动所有这些开发人员。我们必须要求他们只有在回报丰厚的情况下才能取消旧习惯,学习新习惯。例如,在 Go 1 之前,通过错误类型实现的方法被命名为 String
。在 Go 1 中,我们将其重命名为 Error
,以将错误类型与可以自行格式化的其他类型区分开来。前几天,我实现了一个错误类型,并且不加思索第将其方法命名为 String
而不是 Error
,该方法当然不会编译。五年后,我仍然还没有完全忘记旧方法。这种澄清的重命名是 Go 1 中要进行的一项重要更改,但如果没有充分的理由,那么对于 Go 2 来说就太具破坏性。
Go 2 还必须带来所有现有的 Go 1 源代码。我们决不能分裂 Go 生态系统。混合程序(其中 Go 2 编写的包导入 Go 1 编写的包,反之亦然)必须在多年的过渡期内毫不费力的工作。我们必须确切的知道如何做到这一点。像 go fix 这样的自动化工具肯定会发挥作用。
为了最大程度地减少中断,每个更改都需要仔细考虑、规划和工具化,从而限制了我们可以进行的更改数量。也许我们可以做两三个,当然不能超过五个。
我没有在统计细微的内务变化,例如可能允许更多的口头语言或添加二进制整数字面量。此类微小更改也很重要,但更容易解决。今天,我将重点放在可能的重大更改上,例如对错误处理的额外支持,引入不可变或只读值,添加某种形式的泛型或尚未建议的其他重要主题。我们只能进行其中的一些重大更改。我们将必须谨慎选择。
#### 流程
这就提出了一个重要的问题。 Go 的开发流程是什么?
在 Go 的早期,我们只有五个人,我们在一对相邻的共享办公室工作,这些办公室被玻璃墙隔开。将所有人召集到一个办公室讨论一些问题,然后回到我们的办公桌来实施解决方案很容易。在实施过程中出现一些皱纹时,很容易再次召集所有人。罗伯和罗伯特的办公室有一个小沙发和一个白板,所以通常我们当中一个人进来并开始在黑板上写一个例子。通常在示例出现时,其他所有人都在自己的工作中达到了一个很好的停止点,并准备坐下来讨论。这种非正式性显然无法扩展到当今的全球围棋社区。
自从 Go 的开源发布以来,部分工作是将我们的非正式流程移植到更正式的邮件列表和问题跟踪器以及 50 万用户中,但是我认为我们从未明确描述过我们的整体流程。我们可能从未有意识地考虑过这一点。但是,回顾一下,我认为这是我们在 Go 上工作的基本要点,这是自第一个原型运行以来我们一直遵循的过程。
步骤 1 是使用 Go 积累经验。
第 2 步是确定 Go 可能需要解决的问题,并将其表达出来,向他人解释,然后写下来。
步骤 3 是提出该问题的解决方案,与他人讨论,然后根据该讨论修改解决方案。
步骤 4 是实施解决方案,对其进行评估,然后根据该评估对其进行完善。
最后,第 5 步是交付解决方案,将其添加到人们日常使用的语言,库,工具集中。
同一个人不必针对特定更改执行所有这些步骤。实际上,通常有许多人在任何给定步骤上进行协作,并且可能针对单个问题提出许多解决方案。另外,在任何时候我们都可能意识到我们不希望对某个特定想法走得更远,而是回头一步。
尽管我不相信我们曾经讨论过整个过程,但是我们已经解释了其中的一部分。 2012 年,当我们发布 Go 1 并说现在是时候使用 Go 并停止对其进行更改时,我们在解释步骤 1。在 2015 年,当我们介绍 Go 更改提案流程时,我们在解释步骤 3、4 和 3。 5. 但是我们从未详细解释过第 2 步,所以我现在想做。
(有关 Go 1 的开发以及如何摆脱语言变化的更多信息,请参见 Rob Pike 和 Andrew Gerrand 在 OSCON 2012 上发表的演讲 “ [The Go to Go 1](《Go Blog 中文翻译》 -go-1)。有关提案流程的更多信息,请参阅安德鲁・格朗德 (Andrew Gerrand) 在 GopherCon 2015 上发表的演讲 “ How Go was Made” 和 “ 提案流程文档。
#### 解释问题
有两个部分来解释问题。第一部分 - 较容易的部分 - 确切说明问题所在。我们的开发人员非常擅长于此。毕竟,我们编写的每项测试都是要解决的问题的陈述,其语言如此精确,甚至连计算机都可以理解。第二部分 - 较难的部分 - 很好地描述了问题的重要性,每个人都可以理解为什么我们应该花时间解决问题并维护解决方案。与精确地陈述问题相反,我们不需要经常描述问题的重要性,而且我们也不擅长于描述问题。计算机从不问我们 “为什么这个测试用例很重要?您确定这是您需要解决的问题吗?解决这个问题是您可以做的最重要的事情吗?” 也许他们有一天会,但不是今天。
让我们看一下 2011 年的一个旧示例。这是我在计划 Go 1 时将 os.Error 重命名为 error.Value 的内容。
它以对问题的精确的单行声明开始:在非常低级的库中,所有内容都为 os.Error 导入 “os”。然后有五行内容,我在这里重点说明了问题的严重性:“ os” 使用的程序包本身无法在其 API 中出现错误,而其他程序包则依赖 “ os”,其原因无济于事。做操作系统服务。
这五行是否使您确信此问题很严重?这取决于您能否很好地填写我遗漏的上下文:被理解需要预见别人需要知道的内容。对于当时的我的听众 - Google 的 Go 团队中的其他十个人正在阅读该文档 - 五十个单词就足够了。为了在去年秋天向 GothamGo 的受众群体 (背景和专业领域更加多样化的受众群体) 提出同样的问题,我需要提供更多的上下文,并且我使用了大约 200 个单词以及真实的代码示例和图表。当今全球围棋社区的一个事实是,描述任何问题的重要性都需要添加上下文,尤其是通过具体示例进行说明,而与同事交谈时会忽略这些上下文。
说服别人一个重大问题是至关重要的一步。当问题显得无关紧要时,几乎所有解决方案都显得过于昂贵。但是对于一个重大问题,通常有许多合理成本的解决方案。当我们不同意是否采用特定解决方案时,我们通常实际上不同意要解决的问题的重要性。这是如此重要,以至于我想看两个最近的例子,至少在事后看来,这些例子清楚地表明了这一点。
示例:Le 秒#
我的第一个例子是关于时间的。
假设您想计时事件花费的时间。写下开始时间,运行事件,写下结束时间,然后从结束时间中减去开始时间。如果事件花费了十毫秒,则减法将得出十毫秒的结果,可能加上或减去一个很小的测量误差。
开始:= time.Now()// 3:04:05.000
事件()
结束:= time.Now()// 3:04:05.010
过去:= end.Sub(start)// 10毫秒
这个明显的过程可能会在 le 秒期间失败。当我们的时钟与地球的日常旋转不完全同步时,便会在午夜之前插入 leap 秒 (正式时间为 11:59 pm 和 60 秒)。与 leap 年不同,leap 秒没有遵循可预测的模式,这使其很难适应程序和 API。操作系统通常不尝试表示偶尔的 61 秒分钟,而是通过将时钟调回正好在午夜之前的一秒来实现 a 秒,从而使 11:59 pm 和 59 秒发生两次。此时钟重置使时间似乎向后移动,因此我们的十毫秒事件可能被计时为负 990 毫秒。
开始:= time.Now()// 11:59:59.995
事件()
结束:= time.Now()// 11:59:59.005(真的11:59:60.005)
过去了:= end.Sub(start)// –990毫秒
由于像这样的时钟重置中的定时事件,时钟时间不准确,因此操作系统现在提供了第二个时钟,即单调时钟,它没有绝对含义,但计数秒数,并且永不重置。
除了在奇数时钟重置期间外,单调时钟并不比每日时钟好,而且每日时钟还具有用于告诉时间的附加好处,因此为简单起见,Go 1 的时间 API 仅公开时间时钟。
2015 年 10 月,一份错误报告指出,Go 程序无法在整个时钟重置期间正确计时事件,尤其是典型的 leap 秒。建议的修补程序也是原始问题标题:“添加新的 API 以访问单调时钟源。” 我认为此问题的重要性不足以证明新 API 的合理性。几个月前,在 2015 年中期的 and 秒中,Akamai,亚马逊和 Google 整天降低了时钟速度,吸收了额外的秒数而又不倒转时钟。似乎最终广泛采用这种 “ leap smear” 方法将消除 leap 秒时钟重置,这是生产系统上的一个问题。相比之下,向 Go 中添加新的 API 会带来新的问题:我们将不得不解释两种时钟,教育用户何时使用每种时钟,以及转换许多行现有代码,所有这些都是很少发生且可能合理的问题自行消失。
如果没有明确的解决方案,我们会做我们总是会做的事情:我们等待着。等待使我们有更多的时间来增加经验和对问题的理解,也有更多的时间来寻找好的解决方案。在这种情况下,以一种感激的 [Cloudflare 的小故障] 的形式增加了对问题重要性的理解 (www.theregister.co.uk/2017/01/04/c...) 。他们的 Go 代码在 2016 年底将 DNS 请求计时为 leap 秒,大约为 990 毫秒,这导致其服务器同时出现恐慌,高峰时打破了 0.2%的 DNS 查询。
Cloudflare 正是 Go 打算使用的那种云系统,并且由于 Go 无法正确计时事件,他们发生了生产中断。然后,这是关键点,Cloudflare 在 John Graham-Cumming 的博客文章 “[如何以及为什么 experience 秒影响 Cloudflare DNS]”(blog.cloudflare.com/how-and - 通过分...
示例:别名声明#
我的第二个示例是在 Go 中支持别名声明。
在过去的几年中,Google 建立了一个致力于大规模代码更改的团队,这意味着 API 迁移和错误修复适用于我们的数百万个源文件和数十亿行代码的代码库,以 C ++,Go,Java,Python 和其他语言编写。从该团队的工作中学到的一件事是,当将 API 从使用一个名称更改为另一个名称时,能够以多个步骤而不是一次全部更新客户端代码的重要性。为此,必须可以编写一个声明,将旧名称的使用转发到新名称。 C ++ 具有 #define,typedef,并使用声明来启用此转发,但是 Go 没有任何内容。当然,Go 的目标之一就是很好地扩展到大型代码库,并且随着 Google 的 Go 代码数量的增长,很明显,我们既需要某种转发机制,也需要其他项目和公司来解决此问题。随着他们的 Go 代码库的增长。
2016 年 3 月,我开始与 Robert Griesemer 和 Rob Pike 讨论 Go 如何处理渐进式代码库更新,并且我们到达了别名声明,这正是所需的转发机制。在这一点上,我对 Go 的发展方式感到非常满意。我们从 Go 成立之初就开始谈论别名 - 实际上,第一个规范草案已经使用别名声明的示例,但是每次我们讨论别名,然后再讨论别名时,我们都没有明确的用例,因此我们将其省略。现在,我们提议添加别名不是因为它们是一个优雅的概念,而是因为它们满足了 Go 的可扩展软件开发目标,从而解决了一个重大的实际问题。我希望这将成为将来 Go 更改的模型。
春天晚些时候,罗伯特和罗布 (Robert and Rob) 撰写了提案,罗伯特在 [Gophercon 2016 闪电演讲](https:///www。 youtube.com/watch?v=t-w6MyI2qlU)。接下来的几个月进展并不顺利,它们绝对不是未来 Go 变革的典范。我们吸取的许多教训之一是描述问题重要性的重要性。
在一分钟前,我向您解释了该问题,并提供了有关该问题的产生方式和原因的背景信息,但没有具体示例可以帮助您评估该问题是否会在某种程度上影响您。去年夏天的提案和闪电演讲给出了一个抽象的示例,涉及 C,L,L1 和 C1 到 Cn 包,但没有与开发人员相关的具体示例。结果,社区中的大多数反馈都基于这样的想法,即别名只能解决 Google 的问题,不能解决其他所有人的问题。
正如 Google 最初并不了解正确处理 leap 秒二次重置的重要性一样,我们也没有有效地向更广泛的 Go 社区传达在大规模变更期间处理渐进代码迁移和修复的重要性。
在秋天,我们从头开始。我使用了 talk 并写了介绍问题的文章从开源代码库中提取的多个具体示例,展示了这个问题如何在所有地方发生,而不仅仅是在 Google 内部。现在,更多的人了解了这个问题并看到了它的重要性,我们进行了富有成果的讨论关于哪种解决方案是最好的。结果是类型别名将包含在 Go 1.9 中,这将有助于 Go 扩展到更大的代码库。
体验报告#
这里的教训是,以一种在不同环境中工作的人可以理解的方式来描述问题的重要性是困难的,但至关重要。要讨论 Go 社区的重大变化,我们将需要特别注意描述我们要解决的任何问题的重要性。最清晰的方法是显示问题如何影响实际程序和实际生产系统,例如 Cloudflare 的博客文章 和 我的重构文章。
诸如此类的经验报告将一个抽象的问题变成一个具体的问题,并帮助我们理解其重要性。它们还充当测试用例:可以通过检查其对报告所描述的实际问题的评估来评估任何建议的解决方案。
例如,最近我一直在研究泛型,但是我对 Go 用户需要使用泛型来解决的详细而具体的问题并不清楚。结果,我无法回答一个设计问题,例如是否支持通用方法,也就是说与接收器分开参数化的方法。如果我们有大量的实际用例,则可以通过检查重要的用例来开始回答这样的问题。
再举一个例子,我看到了以各种方式扩展错误接口的建议,但是我还没有看到任何经验报告,这些报告显示了大型 Go 程序试图完全理解和处理错误,更不用说显示当前错误接口如何阻碍了它。这些尝试。这些报告将帮助我们所有人更好地了解问题的细节和重要性,这是我们在解决问题之前必须做的。
我可以继续。 Go 的每项主要潜在变化都应由一份或多份经验报告来激发,这些报告记录了人们今天如何使用 Go 以及为何效果不够好。对于我们可能会考虑使用 Go 进行的重大更改,我不了解许多此类报告,尤其是没有以实际示例说明的报告。
这些报告是 Go 2 提案流程的原始资料,我们需要所有人编写它们,以帮助我们了解您的 Go 经验。你们中有一半的人在广泛的环境中工作,而我们当中的人并不多。在您自己的博客上撰写帖子,或撰写 Medium 帖子,或撰写 Github Gist(添加一个.md
Markdown 的文件扩展名),或编写 Google 文档,或使用您喜欢的任何其他发布机制。发布后,请将帖子添加到我们的新 Wiki 页面,golang.org/wiki/ExperienceReports。
解决方案#
现在我们知道了如何识别和解释需要解决的问题,我想简要地指出,并非所有问题都可以通过更改语言来最好地解决,这很好。
我们可能要解决的一个问题是,计算机通常可以在基本算术运算期间计算其他结果,但是 Go 不能直接访问这些结果。罗伯特 (Robert) 在 2013 年提出,我们可以将二结果 (“逗号分隔”) 表达式的概念扩展到基本算术。例如,如果 x 和 y 是 uint32 值,则 lo,hi = x * y
将不仅返回乘积的通常低 32 位,而且还返回高 32 位。这个问题似乎并不特别严重,因此我们记录了可能的解决方案但未实现。我们等了。
最近,我们为 Go 1.9 设计了一个数学 / 位包,其中包含各种位操作功能:
包位//导入“数学/位”
func LeadingZeros32(x uint32)int
func Len32(x uint32)int
func OnesCount32(x uint32)int
func Reverse32(x uint32)uint32
func ReverseBytes32(x uint32)uint32
func RotateLeft32(x uint32,k int)uint32
func TrailingZeros32(x uint32)int
...
该软件包对每个函数都有良好的 Go 实现,但是编译器在可用时也会替代特殊的硬件指令。基于对数学 / 位的这种经验,Robert 和我现在都认为通过更改语言来提供附加的算术结果是不明智的,相反,我们应该在数学 / 位之类的程序包中定义适当的函数。最好的解决方案是更改库,而不是更改语言。
在 Go 1.0 之后,我们可能想解决的另一个问题是,goroutines 和共享内存使将种族引入 Go 程序太容易了,从而导致崩溃和生产中的其他不良行为。基于语言的解决方案将是找到某种方法来禁止数据争用,以使其无法编写或至少不能通过数据争用来编译程序。在编程语言世界中,如何将其适应 Go 语言仍然是一个悬而未决的问题。取而代之的是,我们在主发行版中添加了一个工具,使其使用起来很简单:该工具种族检测器已成为 Go 体验中不可或缺的一部分。最好的解决方案是更改运行时和工具,而不是更改语言。
当然,语言也会发生变化,但是并不是所有的问题都能用语言得到最好的解决。
交付 Go 2#
最后,我们将如何运输和交付 Go 2?
我认为最好的计划是,作为 Go 1 的发布顺序的一部分,逐步地、逐个功能的将 Go 2 的向后兼容的部分运输。这有几个重要的特性。首先,它使 Go 1 的发布保持在通常的时间表上,以继续进行用户现在所依赖的及时的错误修复和改进。第二,它避免了 Go 1 和 Go 2 之间开发工作的分裂;第三,它避免了 Go 1 和 Go 2 之间的分歧,便于大家最终的迁移。第四,它可以让我们一次专注于并交付一个改变,这应该有助于保持质量。第五,这将鼓励我们在设计功能时要向后兼容。
我们需要时间来讨论和规划,然后才会在 Go 1 的版本中开始实施,但在我看来,一年后的 Go 1.12 左右,我们可能会看到一些小的改动。这也让我们有时间先把包管理支持落地。
一旦所有的向后兼容工作都完成了,比如说在 Go 1.20 中,我们就可以在 Go 2.0 中进行非向后兼容的修改。如果结果发现没有不向后兼容的改动,也许我们就宣布 Go 1.20 是 Go 2.0。无论如何,到了那个时候,我们将从 Go 1.X 的发布顺序过渡到 Go 2.X 的发布顺序,也许会有一个支持 Go 1.X 最终版本的扩展窗口。
这都是一些推测,我刚才提到的具体发布数字只是一个临时性的估计,但我想说明的是,我们不会放弃 Go 1,事实上,我们会尽可能地把围棋 1 带到最远。
求助#
我们需要你的帮助。
Go 2 的对话从今天的开始,这将在公开场合进行,比如邮件列表和问题追踪器等公共论坛上进行。请大家在每一步都要帮助我们。
今天,我们最需要的是体验报告。请告诉我们 Go 是如何为你工作的。请写一篇博文,包括真实的例子,具体的细节,真实的经验。并将其链接到我们 wiki 页面。这样我们就会开始讨论我们 ——Go 社区,可能会对 Go 做出什么样的改变。
谢谢你。
本译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。
推荐文章: