写点什么

Go 未来演进:基于共同目标和数据驱动的决策

作者:Tony Bai
  • 2023-12-11
    辽宁
  • 本文字数:7893 字

    阅读完需:约 26 分钟

Go未来演进:基于共同目标和数据驱动的决策

自从 Go 语言之父 Rob Pike 从 Google 退休并隐居澳洲后,Russ Cox 便成为了 Go 语言团队的“带头大哥”,虽然其资历还无法与依旧奋战在一线的另外一位 Go 语言之父 Robert Griesemer 相比。如今,Russ Cox 对 Go 语言未来的演化发展是很有“发言权”的,Go module 的引入便是 Russ Cox 的重要决策之一。从 Go 社区来看,这些年来,以 Russ Cox 为首的 Go 团队对 Go 演进决策总体上是良性的、受欢迎的,比如 Go module、Go 泛型、Go 对 wasm 的支持等,当然也有一些变化是受到质疑的,比如:Go 1.22版本很可能从试验特性到正式特性的loopvar等


注:我的极客时间《Go语言第一课》专栏中有对 Go module 和 Go 泛型的详细讲解,欢迎感兴趣的童鞋订阅阅读。


想必很多 Gopher 也和我一样,对 Go 团队就某一 proposal 的决策方式和依据很好奇 --到底他们是如何决定是否 accept 这个 proposal 的?Go 语言后续该如何演化?向哪个方向发展演化?


今年 9 月份举办的 GopherCon 2023 上,Russ Cox 代表 Go 团队做了名为“Go Changes”的主题演讲



在这个 talk 中,我们能找到一些答案。近期他重新录制了该演讲视频,并在其个人博客中放出。


本文就是基于这个视频内容进行整理加工后的文字稿,供国内广大 gopher 参考。




这是我在 2023 年 GopherCon 上做的一次演讲的重新录制视频。在这次演讲中,我和大家分享了三部分内容:为什么 Go 需要随着时间的推移而改变,我们如何应对 Go 的变化过程,以及为什么选择性遥测(opt-in telemetry)是这个过程中的一个重要且适当的部分。不过,这个演讲不是关于某个特定的 Go 特性变化,而是关于 Go 整体的变化过程,特别是我们是如何决定做出哪些改变的。


首先一个明显的问题是,为什么 Go 需要改变? 为什么我们不能对 Go 感到满意,然后将其束之高阁呢? 一个显而易见的答案是我们不可能一次就把事情做对,你对比一下上面图片中展示的第一版毛绒 Go 吉祥物和我们在 GopherCon 上发放的最终版本,你就能明白我的意思了。


但这里还有一个更深层次的答案:



我的一位前同事在他使用了多年的邮件签名中引用了生物学家兼科幻小说作家杰克·科恩(Jack Cohen)的一句名言。在这句名言中,科恩说:“我们生物学家使用的一个描述‘稳定(stable)’的专业词汇就是“死(dead)”。


所有的生命都在变化,适应新的环境,修复损伤等。编程环境也需要改变。除非我们想要 Go 死掉,否则它需要适应新的环境,比如新的协议、操作系统和重要用例。我们也需要发现并修复 bug -- 语言、库和生态系统的问题,这些问题只有随着时间的推移或 Go 发展到一定阶段和规模才会暴露出来。


Go 必须改变,并与时俱进。这次演讲就是关于我们如何决定做出哪些改变



这次演讲分三个部分:


  • 第一部分是关于我们对 Go 的愿景和期望。

  • 第二部分是关于我们如何利用数据来决定做出哪些改变。

  • 第三部分是关于我们在Go工具链中增加选择性遥测的计划,以便更好地理解 Go 的使用情况和出现问题的地方。


到演讲结束时,你将了解我们考量和决定 Go 变化的过程,并了解数据在做出这些决定中的重要性,我希望你能理解为什么选择性遥测是一个很好的额外数据来源,甚至可能愿意在系统推出时就选择加入。



让我们从这个开始:我们希望 Go 发生什么样的变化?如果我们在这个基本问题上意见不一致,我们也就无法就具体的变化达成共识。


例如,我们是否应该在 Go 中添加一个 Perl 语句,让我们可以用 Perl 编写函数?



我认为我们不应该,但假设你有不同意见。为了解决这个问题,我们需要理解为什么我们持不同意见。


约翰·奥斯特豪特(John Ousterhout)写了一份名为“开放决策制定(Open Decision Making)”的好文档,内容虽然来自他在创业公司的经验,但它几乎完全适用于开源项目。



在这份文档中,他提出的最重要的观点之一是:如果一群聪明人面对同一个问题,并具有相同的信息,如果他们有相同的目标,那么他们很可能得出相同的结论


如果你和我在 Go 中是否要嵌入 Perl 这个问题上存在分歧,根本原因肯定是我们对 Go 目标有不同的理解,所以我们必须建立明确 Go 的目标。



Go 的目标是更好的软件工程,特别是大规模软件工程。Go的独特设计决策几乎全部针对这个目标。我们已经多次阐述过这一点,包括在上述截图中的这两篇文章中。再说一次,Go 的目标是更好的软件工程


现在我们来说说 Perl。20 年前,当我很年轻、甚至有些天真、Go 还不存在的时候,我编写并部署了一个完全用 Perl 编写的大型分布式系统。我热爱 Perl 所擅长的东西,但它并不是以更好的软件工程为目标。如果我们在这一点上有分歧,那么我可能应该定义一下我所说的软件工程是什么意思。



注:如果要理解 Go 以更好软件工程为目标,或是 Google 的软件工程理念,可以阅读一下《Software Engineering at Google》这本佳作。


我喜欢说,当你给编程加入时间和其他程序员时,软件工程就出现了。编程意味着让一个程序工作。你有一个要解决的问题,你编写一些代码,运行它,调试它,得到答案,完成。这就是编程,这已经够难的了。



但是当那段代码不得不日复一日地继续工作时会发生什么,甚至和其他人一起对它进行维护?那么你需要添加测试,以确保你修正后的 bug 不会在 6 个月后由你自己或是一个不熟悉这段代码的新团队成员重新引入。这就是为什么 Go 从第一天开始就内置了对测试的支持,并建立了一种文化,那就是对任何 bug 的修复或新增代码都要添加测试。


那么随着时间的流逝,当代码必须在 Go 本身发生改变的情况下继续工作时会发生什么?那么我们需要强调兼容性,这是 Go1 版本以来一直在做的。事实上,Go 1.21版本发布了许多兼容性改进,我在 2022 年的 GopherCon 上对此有过介绍。


随着代码量的增长,如果需要某种全局清理时该怎么办?你需要工具,而不可避免的第一个绊脚石是那些工具需要模仿代码的格式化风格来编辑,以避免出现无关的差异。gofmt 的存在是为了支持 goimports、gorename、go fix 和 gopls 等工具,以及你自己可能使用我们提供的包编写的自定义工具。


既然提到了软件包,当你使用其他人提供的软件包时,不可避免的第一个绊脚石是多个人会用相同的名字(比如 sqlite 或 yaml)编写软件包。那么我们如何在一个给定的程序中识别究竟使用哪个了呢?为了在一个去中心化的方式无歧义地回答这个问题,Go 使用 URL 作为包导入路径。


随着时间的推移,下一个问题是挑选使用特定软件包的哪个版本,并决定该版本是否与所有其他依赖项兼容。这就是为什么 Go 提供了 modules、workspaces、Go modules mirror 镜像和 Go module 校验和数据库。


接下来的问题是每个人的代码都有 bug,包括安全 bug。你需要了解关于最重要 bug 的信息,这样你就知道需要更新到已修复的版本。这就是为什么我们添加了 Go 漏洞数据库和govulncheck,Julie 也在 GopherCon 上谈到了这一点,当有视频链接时我会在下面添加。


以上是较大的例子,但也有小的例子,比如添加新的协议如 HTTP/3,移除对过时平台的支持,以及修复或废弃容易出错的 API,以避免大型代码库中的常见错误。


这把我们带到了 Go 提案过程(Proposal Process),这是我们对是否接受(accept)和拒绝(decline)哪些变更做出决定的方式:



当我们考虑这些决定时,使用数据非常重要,这可以帮助我们达成共识。


简单地说,任何人都可以在 Go 的 GitHub 问题跟踪器上提出 Go 更改提案(Change Proposal)。然后,在该问题上进行讨论,我们试图在参与者之间就是否接受或拒绝该建议达成共识,或者该建议需要做出什么修改才能被接受。


随着时间的推移,我们越来越欣赏约翰·奥斯特豪特在他的观察中提出的第二句话的重要性:如果面对问题的人不仅共同的目标,还有共同的信息,他们很可能会达成共识。



在 Go 的早期,只有我们几个人做决定。我们根据技术判断和直觉做出决定,这些判断和直觉是基于我们过去的经验。那些经验就是我们使用的信息。由于我们的过去经验有足够的重叠,我们大多数时候能达成共识。大多数小项目都是这种工作方式。


随着决策涉及的人数大大增加,共享经验就会减少。我们需要一个新的共享信息来源。我们发现的最好信息来源是收集实际数据,然后将这些数据作为共享信息来做决策。但是我们从哪里获得这些数据呢?对 Go 来说,我们有许多潜在的来源,每一个都适合具体的决策类型。在这里,我将向你展示其中的一些。


一个数据来源是与 Go 用户交谈。我们以各种方式做到这一点:



首先是 Go 用户调查,我们从 2016 年开始每年做一次,最近开始一年做两次。调查非常适合了解 Go 最流行的用途以及人们面临的最常见问题。多年来,最常见的问题是缺乏依赖管理和泛型。我们使用这些信息将开发 Go 模块和泛型作为优先事项。


另一个数据来源是我们可以在 VSCode 中使用 VSCode Go 插件运行的调查。这些调查可以帮助我们了解 VSCode Go 体验的实效性。


来自用户的最后一个直接数据来源是我们全年进行的研究访谈和用户体验研究。这些研究允许我们从小规模的用户群体中识别模式或获取更多关于特定主题的信息。


调查和访谈通过与用户交谈来收集数据。另一个数据来源是阅读代码:我们可以分析已发布的开源 Go module 代码。


例如,在添加新的“go vet”检查之前,我们会在开源代码库的一个子集上运行它,然后读取一些随机样本的结果,看检查是否指出了真实的问题,以及它是否有太多的假阳性。


在 Go 1.22 版本,我们计划添加一个 go vet 检查,检查对 append 的调用是否没有 append 任何内容。这里有检查器标记的两段代码:



顶部的一段代码表明开发人员可能认为 append 总是复制其输入 slice。底部的一段代码可能是正确的,但难于措辞来描述。


这里还有另外两段代码:



在顶部的一段中,或者 for 循环从未运行,或者它永远不会完成,因为 e.Sigs 的长度永远不会改变。底部的代码也似乎是一个清晰的 bug:代码正在仔细决定将消息追加到哪个列表中,然后它没有将其追加到任何一个列表中。


由于我们对样本代码段进行的所有采样都是可疑的或完全错误的,我们决定添加该检查。在这里,数据比直觉更好。


所有这些方法都是在少量样本上工作。对于典型的代码分析,我喜欢手动检查 100 个样本,与世界上所有 Go 代码的量相比,这只是一个微小的比例。最后一份 Go 开发者调查有不到 6000 名受访者,而全世界可能有 300 万 Go 开发者,样本比例不到 1%。


一个很好的问题是为什么这些极小的样本能告诉我们有关更大人群的信息?答案是抽样精度只依赖于样本数量,而不依赖于总体规模。



这乍一看似乎反直觉,但假设我有一个装有 100 万只 Go 吉祥物的大箱子,我随机拿出两个。首先我拿到一个蓝色的,然后我拿到一个粉红色的。根据这两个样本,我估计箱子中的吉祥物大约一半是蓝色的,一半是粉红色的。但如果我告诉你箱子里有粉红色、蓝色和灰色的吉祥物,你是否会感到十分惊讶? 不会非常惊讶!如果箱子正好分三分之一粉红色、蓝色和灰色,那么这 9 对颜色组合中的每一对都同样可能:



得到一个非灰色吉祥物的机会是 2/3,得到两个的机会就是 2/3 的平方,即 4/9。没看到灰色的情况出现概率将近一半。这就是为什么我们不会非常惊讶的原因。


现在假设我取出 100 只,有 48 只蓝色和 52 只粉红色。我再次估计箱子大约一半是蓝色,一半是粉红色。现在如果我告诉你箱子里有粉红色、蓝色和灰色的吉祥物,你会有多惊讶?你应该会非常惊讶。


事实上,你完全不应该相信我。如果那是真的,得到 100 只连续的非灰色吉祥物的机会是 2/3 的 100 次方,约等于 10 的负 48 次方:



随机出现这种情况的可能性为零。要么我在说谎,要么我没有随机抽取。可能所有的灰色吉祥物都在箱子底部,我没有抽取到足够深的地方。


请注意:这都不依赖于箱子中有多少只 Go 吉祥物,它只取决于我们取出了多少只。用于特定预测精度的数学更复杂,但具有相同的效果:只有样本数量重要,箱子中的吉祥物数目不重要


一般来说,手工计算这些数学太困难了,所以这里有一个表格,你可以在我的博客上找到:



它说明,如果你提取 100 个样本并根据这些样本估计百分比,那么 90%的时间你的估计将在真实百分比的正负 8%之内。99%的时间它们将在 13%之内。如果像 Go 调查中那样有 5000 个样本,那么 90%的时间估计误差在正负 1%之内,99%的时间在正负 2%之内。超过这个数量,我们实际上不需要更多样本。


有一个注意事项是样本需要是随机的, 或者至少与你正在估计的内容不相关。你不能只从箱子的顶部抽取吉祥物,然后对整个箱子做出断言。



如果你避免了这个错误, 那么当你试图估计一个新的 API 是否有用或者某个特定的 vet check 是否值得的时候, 花一个小时左右手动检查 100 个样本是合理的。如果是一个坏主意, 那将很快显现出来。而如果看起来是一个好主意, 再花几个小时检查更多的样本, 无论是手动检查还是用程序检查,都会大大提高你的估计准确性。与做出错误决策的代价相比,这是一个非常小的成本。


简而言之,采样的魔力在于将许多一次性估计转变为可以手动或用少量数据完成的工作。这就是为什么我们已经看到的所有数据来源都能够相当好地代表整个 Go 开发者群体的原因。


现在进入演讲的第三部分:Go 工具链中的遥测(Telemetry):



遥测也将是 Go 开发者使用的一个小样本,但它应该是一个有代表性的样本,并且回答不同的问题,而不是调查和代码分析所做的问题。


遥测始终是一个有争议的话题,特别是对于开源项目来说,所以让我从最重要的细节开始说起:上传遥测报告是完全自愿和选择加入的:



除非你运行一个显式命令选择加入数据收集,否则不会上传任何数据。而且,这不是那种上传你的全部活动的详细跟踪的遥测系统。这种遥测也只适用于我们作为 Go 发行版的一部分分发的命令,比如 gopls、go 命令和编译器(compiler),它不会涉及你构建的任何程序


在我更详细地描述完这个系统之后,我希望你会发现你会愿意选择加入这个遥测系统。实际上,我们给自己设定的主要设计限制是,即使由其他人运行,我们也愿意选择加入该系统。


在我以 2023 年 11 月的录制这个内容时,该系统刚刚开始运行,只有少数人被要求在 VSCode Go 中选择加入 gopls 遥测。所以总体来说,你现在还不能选择加入。但希望很快你就可以了。


在我们深入了解细节之前,遥测的动机是它提供了与调查和代码分析不同的信息。它主要提供的两个类别是使用信息(Usage Information)和故障信息(Breakage Information)。调查让我们能够询问关于 Go 使用的广泛问题,但对于详细的使用信息来说并不好。那将是太多问题,对于调查对象来说,90%的问题要回答"no"是一种浪费时间。



这个幻灯片显示了我们在之前的版本中警告过即将删除的 Go 功能列表。列表中的最后一项,buildmode=shared,是我们试图移除的功能,但在事先警告后,至少有一个用户提出了异议,我们将其保留了下来。即便如此,buildmode=shared 与 Go module 基本不兼容,所以它的使用可能非常有限。但我们没有数据,所以它仍然存在于代码库中。遥测可以为我们提供基本的使用信息,以便我们可以基于数据而不是猜测做出这些决策。


另一个重要的类别是故障信息:



如果 Go 工具链明显有问题,我们希望在 GitHub 上收到错误报告。但是 Go 工具链也可能以用户注意不到的微妙方式出现问题。一个例子是,在 macOS 上的 Go 1.14 到 Go 1.19 的版本中,标准库包的二进制文件在预先构建时使用了非默认的编译标志,这是一个意外,这使得它们看起来像是过时了,Go 命令在运行时会重新编译它们,这意味着如果你的程序导入了 net 包,你需要安装 Xcode 中的 C 编译器来构建程序。我们希望 Go 能够自行构建纯 Go 程序,而无需其他工具链。因此,要求安装 Xcode 是一个 bug。但是我们没有注意到这个问题,也没有用户在 GitHub 上报告它。遇到这个问题的人似乎只是安装了 Xcode 并继续进行了工作。遥测可以提供基本的性能指标,比如标准库缓存命中率,这样 Go 工具链的开发人员即使用户没有意识到这个问题,也能注意到这个问题。


另一个例子是编译器的内部崩溃:



Go 编译器在程序的第一个错误处不会停止。它会继续进行,尽可能多地查找和报告不同的错误。但是有时,继续分析已知错误的程序会导致意外的 panic。我们不希望向用户显示这样的崩溃。相反,编译器会从 panic 中恢复,并且仅报告已经发现的错误。这样,Go 用户可以纠正这些错误,这也可能纠正隐藏的 panic。用户的工作不会因为看到编译器崩溃而中断。这对用户来说是好的,但是 Go 工具链的开发人员仍然希望了解这个崩溃并修复这个错误。遥测可以确保即使用户不知道这个错误,但我们还能了解到这个错误。


为了收集使用情况和故障信息,Go 遥测设计记录“计数器和崩溃”:



像 go 命令、Go 编译器或 gopls 这样的 Go 工具链程序可以定义命名事件计数器,并在事件发生时递增计数器。事件还可以按堆栈跟踪单独计数。这些计数器在本地的磁盘文件中维护,每次保留一周的时间。在幻灯片上,gopls 和其他工具正在将计数器写入每周的文件中。


每周一次,Go 工具链中的上传程序(uploader)将从遥测服务器获取一个“上传配置”,其中列出了该周收集的特定事件名称。只有在遥测特定的提案审查过程达成共识后,才会更改该配置。该配置作为一个模块(module)提供,以保护下载的完整性,并保留过去配置的公共记录。然后,上传程序仅上传上传配置中列出的计数器。在幻灯片上,上传程序仅为 gopls 发送一份报告,仅包含少量计数器,即使磁盘上可能还有更多计数器。报告中包含关于使用 gopls 的编辑器的统计信息,以及关于完成请求的延迟的信息,还有一个发生了一次的 gopls/bug 事件,其中包含一个栈跟踪。


请注意,上传的数据中没有事件跟踪或任何用户数据,只有计数器、已在公共上传配置中列出的事件名称,以及 Go 工具链程序中的函数名称。还要注意,栈跟踪不包括任何函数的参数,只有函数名称,因此没有用户数据。


开源中的遥测可能会在拥有数据访问权限和没有数据访问权限的人之间产生信息失衡。我们希望避免这种情况。请记住奥斯特豪特规则:为了达成共识,我们需要每个人拥有相同的信息。由于 Go 的遥测上传不包含任何敏感数据,并且是在明确的选择同意的情况下收集的,我们可以完整地重新发布这些报告,以便任何人都可以进行任何数据分析。我们还将发布一些基本的图表,用于做出决策。我们唯一可能看到但没有重新发布的是报告来自哪些 IP 地址,我们的服务器会将这些信息与报告一起记录。


一个明显的问题是,是否有足够多的人选择启用遥测,以使数据足够准确以做出决策。幸运的是,采样的神奇之处在于可以帮助解决这个问题。



全球大约有 300w Go 开发者。当系统准备就绪并要求人们启用遥测时,即使只有千分之一的开发者选择参与,也会有 3000 名开发者,根据我们的图表显示,误差不到 3%,置信度为 99%。如果全球三分之二的 Go 开发者启用了遥测,那将是 20000 个样本,误差不到 1%,置信度为 99%。除此之外,我们实际上不需要更多的样本。如果我们持续获得更多的报告,我们可以调整上传配置,告诉系统在某个特定的周选择随机不上传任何东西。例如,如果有 20 万个系统选择了参与,我们可以告诉每个系统在任何给定的周上传的概率为 10%。因此,即使我们预计选择参与率会很低,系统应该能够运行得很好,随着选择参与率的提高,Go 遥测将从任何给定系统收集更少的数据。当然,这使得每个选择参与的人对我们来说更加重要。目前来说,Go 遥测对于你们中的任何人来说都还没有准备好,但当准备好时,我希望你们会选择参与。


在结束之前,我希望你们从演讲中获得以下几点:



首先,Go 需要不断变化,特别是随着计算世界的变化。


其次,任何改变的目标都是为了使 Go 在软件工程中变得更好,尤其是在规模化(scaling)方面。


第三,一旦我们确定了目标,达成共识的下一个最重要的部分是拥有共享数据来做出决策。


第四,Go 工具链遥测是增补我们现有调查和代码分析数据的重要数据来源。


最后,在整个演讲中,虽然涉及到了数据和适当的统计,但我们评估的想法、假设和潜在的变化始终始于个人故事和对话。我们喜欢听到这些故事,并与你们所有人讨论如何使用 Go,关于什么有效和什么无效。所以,请无论在什么情况下,无论是在会议上、邮件列表上还是在问题跟踪器上,请确保让我们知道 Go 对你们的工作情况以及存在的问题。我们总是很乐意听到这些。非常感谢。





发布于: 10 小时前阅读数: 3
用户头像

Tony Bai

关注

还未添加个人签名 2017-12-01 加入

极客时间专栏《Go语言第一课》讲师

评论

发布
暂无评论
Go未来演进:基于共同目标和数据驱动的决策_golang_Tony Bai_InfoQ写作社区