2007年9月25日,Rob Pike,Robert Griesemer和Ken Thompson一直在讨论一种新的编程语言,Rob建议使用“Go”。
第二年,Ian Lance Taylor和我加入了这个团队,我们五个人共同构建了两个编译器和一个标准库,2009年11月10日我们开源了相关代码 。
在接下来的两年中,在Go开源社区的帮助下,我们尝试了大大小小的变化,改进了Go,并提出了2011年10月5日发布Go 1的计划 。
在Go社区的帮助下,我们修改并实施了该计划,最终于2012年3月28日发布了Go 1 。
Go 1的发布是我们近五年的创造和疯狂努力的高潮,把go从一个名字和一个想法的清单,变成一个稳定的生产语言。 它也标志着go从实验到稳定的明显转变。
在Go 1的几年开发时间中,几乎每周都会导致很多人的Go程序编译失败。 我们了解到,为了保持Go在生产环境中的使用,程序不得不每周重写以跟上语言变化。 随着Go 1的发布 ,我们的动机变成为创建可靠的产品,项目和出版物(博客,教程,会议谈话和书籍)提供稳定的基础,使用户有无须在新版本发布时重新修改代码。
Go 1发布后,我们知道我们需要花时间在生产环境中使用Go。 我们在自己的项目中使用Go,并改进了实现:我们移植到许多新系统,我们重写了几乎每个关键性能部分,使Go运行更有效率,我们还添加了关键工具,如race detector
现在我们有五年使用Go来构建大型的生产质量体系的经验。我们已经形成了一种什么可以工作而什么不行的认识。 现在是时候开启Go的进化和成长,来规划Go的未来。无论您是GopherCon的观众,还是观看视频,或者在今天晚些时候阅读Go博客,都可以在计划和实施Go 2时与我们合作。
在其余的讲话中,我将要解释我们对Go 2制定的目标; 我们的制约和限制; 整体过程; 关于我们使用Go的经验的重要性,特别是与我们可能尝试解决的问题有关; 可能的种类的解决方案; 我们将如何交付Go 2; 你们都可以帮忙。
Go的目标一直不曾变化。我们希望使程序员更有效地管理两类规模化问题:生产规模化,特别是要与很多服务器交互的并发系统,比如云软件;一个是开发的规模化,特别是由大量松散协作的工程师共同编写的大规模代码库,比如现代的开源软件。
这些规模化问题在各种规模的公司出现。 即使是一个五人创业公司也可能使用其他公司提供的大型基于云的API服务,并且使用比他们自己编写的软件更多的开源软件。 这些公司的生产规模化和开发规模化与谷歌一样重要。
Go 2 的目标就是解决 Go 1 在规模化方面做的还不好的地方
Go的目标从一开始就没有改变,但是Go的限制肯定有。 最重要的限制是现有Go代码。 我们估计全球至少有五十万个Go开发者 ,这意味着有数百万个Go源文件和至少十亿行Go代码。 这些程序员和源代码代表了Go的成功,但它们也是Go 2的主要限制。
Go 2必须带领所有这些开发者。 只有回报非常巨大时,才能让他们放弃旧习惯,学习新用法。 例如,在Go 1之前,由错误类型实现的方法被命名为String 。 在Go 1中,我们将其重命名为Error ,以将错误类型与其他类型区分开来。 有一天我正在实现一个错误类型,我命名它的方法String而不是Error ,这当然导致了编译失败。 五年后,我还没有完全摒弃旧方式。 这种澄清重命名是在Go 1中做出的重要变化,但是对于Go 2而言,如果没有很好的理由,那将是对GO 2造成很大的破坏。
Go 2还必须兼容所有现有的Go 1源代码。 我们不能分裂Go生态系统。 在Go 2中编写的包应该能轻松给Go 1使用,反之亦然。 我们必须弄清楚如何做到这一点; 像go fix这样的自动化工具一定会发挥作用。
为了最大限度地减少干扰,每次更改都需要仔细思考,计划和工具化,这反过来限制了我们可以做出的更改次数。 也许我们可以做两三个,肯定不会超过五个。
我不会轻易的进行更改,例如允许使用更多语言的标识符或添加二进制整数文字。 这些小的变化也很重要,他们更容易得到正确的结果。 我今天专注于可能的重大变化,例如对错误处理的额外支持,或引入不可变或只读值,或添加某种形式的泛型或其他尚未建议的重要主题。 我们只能做这些重大改变。 我们必须慎重选择。
这提出了一个重要的问题。 开发Go的过程是什么?
在Go语言开发的早期,我们只有五个人,我们在一个由玻璃墙隔开的相邻的共同办公室工作。 将每个人都拉到一个办公室来讨论一些问题很容易,然后回到我们的桌面来实施一个解决方案。 在实施过程中出现了一些问题,很容易再次聚集在一起。 Rob和Robert的办公室里有一个小沙发和一个白板,所以当我们中的一个人走进来,开始在板上写一个例子, 通常在写这个例子的时候,其他人都在自己的工作中达到了一个很好的停顿点,并准备坐下来讨论。 然而这个非正规性的讨论显然不会扩大到今天的全球Go社区。
Go开放源代码以来的一部分工作是已经将我们的非正式流程移植到更正式的邮件列表和发布跟踪器以及50万用户的世界中,然而我不认为我们明确地描述我们的整体流程。 有可能我们从来没有意识到这一点。 回想起来,我认为这是我们Go工作的基本纲要,这是自第一个原型运行以来我们一直在追求的过程。
第一步是用Go来积累经验。
第二步是确定Go可能需要解决的问题,并将其阐明,向其他人解释,以便将其写下来。
第三步是提出解决这个问题的方法,与他人讨论,并根据这个讨论修改解决方案。
第四步是实施解决方案,进行评估,并根据评估进行细化。
最后,第5步是送达解决方案,将其添加到语言,库或人们每天使用的一套工具。
同一个人不必为特定的变化做所有这些步骤。 事实上,人们可能在很多步骤上合作,并且可能会提出许多方案来解决问题。 此外,在任何时候,我们可能意识到我们不想进一步实施一个特定的想法,并回到前一步。
虽然我们不曾讨论整个过程,但我们已经解释了其中的一部分。 2012年,当我们发布Go 1,并说现在是使用Go并停止改变它的时候了,我们那时候在解释第1步。在2015年,当我们介绍Go改进提案流程时,我们解释了步骤3,4和但是我们从来没有详细介绍过第二步,所以我现在想这样做。
解释问题有两个部分。 第一部分,说明问题是什么。 我们的开发人员非常擅长这一点。 毕竟,我们编写的每一个测试都是一个需要解决的问题的陈述,语言如此精确,即使是电脑也能理解。 第二部分,描述问题的重要性,让每个人都可以理解为什么我们应该花时间解决问题并维护一个解决方案。 与正确地说明问题相反,我们不需要经常描述一个问题的意义,而且我们做的没有那么好。 计算机从来不问我们“为什么这个测试用例很重要? 你确定这是你需要解决的问题吗? 解决这个问题你可以做什么?”也许他们有一天可以,但不是今天。
我们来看一下从2011年开始的一个老例子。这是我写的关于在计划Go 1时将os.Error重命名为error.Value的内容。
它以一个精确的语句开始:在非常低级的库中,所有对os.Error都导入“os”。 然后有五行,我在这里强调了这一点,用于描述问题的重要性:“os”使用的包本身不能在其API中定义错误,因为其他包使用Error API就需要依赖于“os”但是却没有使用操作系统提供的服务。
这五行能说服你说这个问题很重要吗? 这取决于你如何填补我遗漏的内容:被理解需要对阅读对象有正确的预期。 对于我当时的观众来说,Google的Go团队中的另外十人正在阅读该文档 - 这五十个字就够了。 为了在去年秋天在GothamGo向观众介绍同样的问题 - 观众拥有多种多样的背景和专业领域 - 我需要提供更多的语境,而且我使用了大约二百个字,以及真实的代码示例和图表。 今天的全球Go社区的事实描述了任何问题的重要性,需要添加上下文,特别是具体的示例说明。
告诉别人一个问题的重要性是一个重要的步骤。 当一个问题显得微不足道时,几乎每个解决方案似乎都太昂贵了。 但是对于一个重大问题,通常有许多合理成本的解决方案。 当我们对是否采用特定的解决方案持有不同意见的时候,我们往往对解决问题意义的理解也是不同的。 这非常重要,我想以最近的两个例子,来说明这一点。
我的第一个例子是time API。
假设你想要了解某个事件消耗了多长时间。 您记下开始时间,运行事件,记下结束时间,然后从结束时间减去开始时间。 如果事件花了十毫秒,减法给出了十毫秒的结果,也许加上或减去一个小的测量误差。
start := time.Now() // 3:04:05.000
event()
end := time.Now() // 3:04:05.010
elapsed := end.Sub(start) // 10 ms
这个过程可能在闰秒期间失败。当我们的时钟与地球的日常旋转不完全同步时,会在午夜之前插入第二次正式11:59 pm(闰秒)。 与闰年不同,闰秒没有可预测的模式,这使得它们难以适应于程序和API。 操作系统通常会在午夜之前将时钟倒退一秒而非实现61秒,所以下午11:59:59秒发生两次。 这个时钟复位使时间看起来向后移动,所以我们的十毫秒事件可能被定时为负990毫秒。
start := time.Now() // 11:59:59.995
event()
end := time.Now() // 11:59:59.005 (really 11:59:60.005)
elapsed := end.Sub(start) // –990 ms
因为这样的时钟复位导致的时钟不准确,操作系统现在提供第二个时钟,单调时钟,它没有绝对的意义,而是计数秒,并且永远不会重置。
除了在时钟复位情况之外,单调时钟不比现有时间时钟更好,而时间时钟还具有有用的时间,所以为了简单起见,Go 1的时间API只会暴露时时间时钟。
2015年10月,一个错误报告指出,Go程序无法正确地处理时钟重置事件,特别是典型的闰秒。 建议的修补程序也是原始的问题标题:“添加一个新的API来访问单调时钟源”。我认为这个问题不足以证明新的API的重要性。 几个月前,对于2015年中闰年,Akamai,亚马逊和谷歌已经放慢了一整天的时间,减少了额外的秒数,从而不会重置时钟。 似乎最终广泛采用这种“飞跃涂片”方法消除生产系统的闰秒钟复位问题。 相比之下,向Go添加新的API将会增加新的问题:我们必须解释两种时钟,教育用户有关何时使用它们,并转换许多行现有代码,而这些都是很少出现的问题。
当没有明确的解决方案的时候,我们做了我们一直在做的事情:等等看。 等待给我们更多的时间来增加经验和理解问题,也有更多的时间找到一个很好的解决方案。 在这种情况下,等待会加深我们对这个问题的意义的理解。Cloudflare就中招了。 他们的Go代码在2016年底的闰秒期间对DNS请求进行了定时,产生了990毫秒的负数,这引起了服务器错误,破坏了峰值时的0.2%的DNS查询。
Cloudflare正是云计算(Go 就是为此设计)厂商,Cloudflare在John Graham-Cumming的博客文章中报告了他们的经验,题为“为什么闰秒影响Cloudflare DNS” 。通过分享他们在生产中的经验的具体细节,John和Cloudflare帮助我们了解解决时钟复位准确时序的问题太重要了。 在该文章发布两个月后,我们设计并实施了将在Go 1.9中发布的解决方案(实际上我们没有使用新的API )。
我的第二个例子是支持Go中的别名声明。
在过去几年中,Google已经建立了一个专注于大规模代码更改的团队,这意味着API迁移和错误修复应用在我们的数百万个源文件的代码库和数十亿行代码中(用C ++,Go,Java,Python,和其他语言)。 从团队的工作中学到的一件事是,在将API从使用一个名称更改为另一个名称时,重要的是能够以多个步骤更新客户端代码,而不是一次更新。 为此,必须将旧的名称的使用声明转换为新名称。 C++具有#define,typedef等来达到这一目的,但Go没有任何工具来做到这一点。 当然,Go的目标之一是扩展到更大的代码库,随着谷歌Go代码的数量的增长,我们需要一些转发机制,而且其他项目和公司也将遇到这个问题因为他们的Go码库也在增长。
在2016年3月,我开始和Robert Griesemer和Rob Pike谈论Go如何处理渐进的代码库更新,我们得到了别名声明,这就是所需的转发机制。 在这一点上,我对Go的发展方式感到非常好。 自从Go的早期以来,我们讨论过别名,但是每当我们讨论别名,却缺乏明确的用例。 现在我们提出添加别名不是因为它们是一个优雅的概念,而是因为他们解决了一个重要的实际问题,可以帮助Go达到可扩展软件开发的目标。 我希望这将成为Go的未来变化的榜样。
在今年晚春的时候,Robert和Rob写了一个提案 ,Robert在Gophercon 2016谈话中提出了这个建议 。 接下来的几个月没有顺利进行,这绝对不是将来改变Go的模式。 我们学到的许多教训之一是描述问题的重要性的重要性。
一分钟前,我向您解释了这个问题,提供了一些有关如何出现的原因以及为什么出现问题的背景知识,但没有具体的例子可以帮助您评估问题在某些时候可能会对您造成影响。 去年夏天的建议和闪电谈话给出了一个抽象的例子,涉及C,L,L1和C1到Cn的包,但没有开发人员可以涉及的具体例子。 因此,社区的大部分反馈都是基于这样的想法:别名只能为Google解决问题,而不是为其他人解决问题。
就像我们谷歌一开始并没有明白第二次处理闰秒正确重置的意义,我们没有有效地向更广泛的Go社区传达在大规模变革过程中处理逐步的代码迁移和修复的重要性。
在秋天,我们开始行动了。 我发表了一个演讲,并撰写了一篇文章,介绍了使用多个来自开源代码库的具体示例,显示了这个问题在何处发生,而不仅仅是Google内部。 现在更多的人了解这个问题,并且可以看出它的意义,我们讨论了什么样的解决方案是最好的。 结果是, 别名声明将包含在Go 1.9中 ,并将帮助Go扩展到更大的代码库。
这里的教训是,以一种在不同环境中工作的人可以理解的方式来描述问题的重要性是困难但至关重要的。 要讨论Go作为社区的重大变化,我们需要特别注意描述我们想要解决的问题的重要性。 最清楚的方法是通过显示问题如何影响真实的程序和实际的生产系统,如Cloudflare的博客文章和我的重构文章 。
像这样的报告将抽象问题变成一个具体的问题,并帮助我们了解其意义。 它们也作为测试用例:任何提出的解决方案都可以通过检查其对实际的现实问题的报告进行评估。
例如,我最近一直在研究泛型,但是我并没有清楚地看到Go用户需要泛型解决的具体问题。 因此,我无法回答一个设计问题,例如是否支持通用方法,也就是说与接收器分开参数化的方法。 如果我们有大量的真实用例,我们可以通过检查重要的一些来作为开始。
作为另一个例子,我已经看到了以各种方式扩展错误接口的建议,但我还没有看到任何经验报告,说明大型GO程序如何理解和处理错误,更不用说显示当前错误接口是如何阻止这些尝试的。这些报告将有助于我们更好地了解问题的细节和意义,在解决问题之前我们必须这样做。
我可以继续如此, Go的每个重大潜在变化都应该有一个或多个经验报告的动机,记录人们今天使用Go的原因,以及为什么不能很好地运行。 对于Go可能考虑的明显的重大变化,我没有很多这样的报告,特别是没有用现实世界示例说明的报告。
这些报告是Go 2提案流程的原材料,我们需要大家写这些报告,帮助我们了解Go的经验。 有五十万人在广泛的环境中使用Go。 在您自己的博客上写一篇文章,或者写一篇中级帖子,或写一个Github Gist (为Markdown添加一个.md文件扩展名),或者写一个Google文档 ,或者使用你喜欢的任何其他发布机制。 发布后,请将帖子添加到我们的新wiki页面golang.org/wiki/ExperienceReports 。
现在我们知道我们将如何识别和解释需要解决的问题,我想简要地指出,并不是所有的问题都是通过语言变化来解决的,没关系。
我们可能想要解决的一个问题是计算机通常可以在基本算术运算期间计算额外的结果,但是Go不能直接访问这些结果。 在2013年,罗伯特提出,我们可以将双结果(“逗号”)表达式的概念扩展到基本算术。 例如,如果x和y是例如uint32值,则lo, hi = x * y将不仅返回通常的低32位,而且返回产品的高32位。 这个问题似乎并不重要,所以我们记录了潜在的解决方案,但没有实现。 我们继续等待。
最近,我们为Go 1.9设计了一个包含各种位操作功能的 math/bits包 :
package bits // import "math/bits"
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实现,但编译器也可以使用特殊的硬件指令。 基于math/bits的经验,Robert和我现在都相信,通过改变语言来提供额外的算术结果是不明智的,相反,我们应该在诸如math/bits的包中定义适当的函数。 这里最好的解决方案是库更改,而不是语言变化。
在Go 1.0之后,我们想要解决的一个问题是,goroutines和共享内存使得在Go程序中引入竞争变得太容易,导致生产中的崩溃和其他不当行为。 基于语言的解决方案将是找到一些方式来禁止数据竞争,使其无法编写或至少编译具有数据竞争的程序。 如何将它融入像Go这样的语言在编程语言世界中仍然是一个悬而未决的问题。 相反,我们在发行版中添加了一个工具,并且使用起来非常简单:race detactor已经成为Go体验不可或缺的一部分。 这里最好的解决方案是运行时和工具更改,而不是语言变化。
当然也会有语言变化,但是并不是所有的问题都能在语言变化中得到最好的解决。
最后,我们将如何交付Go 2?
我认为最好的计划是将Go 2的向后兼容部分按照功能特征按照Go 1版本序列的顺序进行递增交付。首先,它按照时间表保持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,事实上,我们将最大限度地带走Go 1。
Go 2的讨论从今天开始,它将出现在公开场合,如邮件列表和问题跟踪器等公共论坛中。请在沿途的每一步帮助我们。 今天,我们最需要的是经验报告。请告诉我们Go是如何为你工作的,更重要的是为什么不工作。写一篇博客文章,包括真实的例子,具体的细节和真实的经历。并将其链接到我们的wiki页面。这就是Go可能想要改变的原因。