Go 1.15 新特性前瞻
Go 1.15 计划于 8 月 1 日发布,它是Go 编程语言的第 16 个主要版本。相比以往,这次的改动较少,但许多重要变化都发生在底层和工具链层面:例如,全新的链接器将加快构建速度并减小二进制文件的体积。此外,该版本还对语言运行时进行了性能优化,调整了支持的架构,并对标准库进行了一些更新。总体而言,这应该是一次扎实的语言升级。
自Go 1.0 发布以来,Go 团队在每个版本中都会持续改进工具链和标准库,但在语言本身的改动上一直持保守态度。许多其他语言每个版本都会推出重要的语言特性,而 Go 自 1.0 以来的各个版本中只加入了少数几个小特性。
这是有意为之的设计选择:自 1.0 发布以来,团队的重点一直是稳定性和简洁性。Go 1 兼容性承诺保证,为 Go 1.0 编写的所有程序无需任何修改,就能在所有 1.x 版本中继续正确运行。Go 开发者通常认为这是一件好事——他们的程序可以“开箱即用”地持续运行,而且总体上会越来越快。
在即将到来的 1.15 版本中,正如预期的那样,语言规范几乎没有任何变化;改进主要集中在工具链、编译器性能和标准库上。正如技术负责人 Russ Cox 所指出的那样,考虑到疫情影响,核心开发者计划在 1.15 中采取格外保守的策略:
我们不知道未来几个月会有多艰难,所以还是保守一些,不要在最后关头提交那些需要调试的细微改动,给自己增加不必要的压力。把它们留到下一个周期的开始,让它们得到充分的验证时间。
[...] Go 1.15 将会是一个比往常更小的版本,这没关系。
5 月 1 日,Go 1.15 进入功能冻结,Go 团队计划在 8 月 1 日发布正式版,遵循一贯的六个月发布周期。
Go 的开发模式与大多数开源语言颇为不同。该语言由 Google 设计,大多数核心开发者都在 Google 工作(因此其持续开发实际上由 Google 资助)。Go 采用宽松的 BSD 风格许可证,开发过程完全公开,一般性讨论在 golang-dev 邮件列表上进行。新功能或变更通过 GitHub 仓库的 issues 提出和讨论,代码审查则通过对 Gerrit 代码变更(称为“changelists”或“CL”)发表评论来完成。
新的链接器
1.15 中最大的工具链变化之一是完全重写的链接器。这份新链接器设计文档由 Go 核心贡献者 Austin Clements 于 2019 年 9 月撰写,详细阐述了重写的动机及将带来的改进。新链接器有三项主要的结构性变化:
- 将工作从链接器转移到编译器:这使得并行化成为可能,因为编译可以在多个 CPU(或多台机器)上并行执行,而链接步骤几乎总是在构建的最后串行完成。此外,编译器的结果会被 Go 工具链缓存。
- 改进关键数据结构,主要是避免使用字符串。当前的链接器使用一个以字符串为索引的庞大符号表;新设计则通过符号编号技术尽可能避免使用字符串。
- 避免一次性将所有输入目标文件加载到内存:这使得新链接器在处理大型程序时占用更少内存,总体内存分配也更少(当前的链接器有超过 20% 的时间花在垃圾回收上)。
随着原始链接器的作者 Ken Thompson 已经退休,可维护性也成了一个问题。正如 Clements 所说:
最初的链接器比现在简单得多,其实现完全可以装进一位图灵奖得主的脑子里,因此几乎没有抽象和模块化。可惜的是,随着链接器不断发展壮大,它依然缺乏结构,而我们唯一的图灵奖得主已经退休了。
鉴于这些影响深远的长期改动,这项工作是在一个分支(dev.link)上进行的,只在稳定时才会合并到 master。负责新链接器工作的 Than McIntosh 介绍了已为 1.15 完成的部分:设计文档中的大多数结构性改进已经完成,包括新的目标文件格式和更紧凑的符号表示。构建速度已经比 1.14 更快、内存占用也更少,但一些功能(例如采用 DWARF 5 调试格式)则要等到 1.16 才能实现。
Clements 补充了更多细节,介绍了并行化工作以及这项工作的分阶段推进方式:
我们在过程中还做了许多其他改进,比如并行化关键阶段、移除大量不必要的 I/O 同步。为了最大程度地利用过去在链接器上的所有成果,我们以“波前”的方式进行这次转换,通过一个将新表示转换为旧表示的阶段,并将该阶段在链接器中越推越靠后。我们还没有完成:这个转换阶段依然存在,不过它具体在何时发生、做什么工作则取决于平台。对于 amd64 ELF 平台,它出现得很晚,工作量也相对较少。对于其他平台,它还没有那么靠后,工作量也更大,因此目前的收益还没那么明显。无论如何,1.16 还有更多值得期待的改进。
就目前而言,链接器在链接的最后阶段仍会将输出转换回旧的内存表示。可以推测,在未来的某个 Go 版本中,这些最后的步骤将被迁移到新链接器中,转换阶段将被彻底移除,从而进一步缩短链接时间、降低内存占用。
更小的二进制文件
与此相关的是多项减小 Go 1.15 构建可执行文件体积的改进。正如 Brad Fitzpatrick 所展示的那样,新链接器能消除更多未使用的代码,使他那个(相当刻意的)测试程序的体积从 Go 1.14 中的 8.2MB 降至 1.15 中的 3.9MB。对于更实际的程序,二进制体积则降低了 3.5% 甚至高达 22%。我运行的一个 Web 服务器程序也从 21.4MB 降至 20.3MB,降幅为 4.9%。
对此贡献最大的是新链接器中对未使用代码的消除,以及几项有针对性的改进,例如 Clements 的 CL 230544,它减少了可执行文件中包含的栈和寄存器映射的数量。这些映射被 Go 的垃圾回收器(GC)用来判断哪些对象仍然存活,但现在只需在调用点保留,而不必为每条指令都保留。这一改动使 go 二进制文件的体积减小了 5.7%,同时也显著加快了编译和链接速度。
由于 Go 能够在运行时检查类型(通过 reflect 包),Go 二进制文件中包含了大量类型信息。Cherry Zhang 提交的 CL 231397 仅在符号被转换为接口时才在输出中包含其类型信息(只有转换为接口的值才能用于反射)。这一改动使 hello-world 程序的体积减小了 7.2%。
还有一些对二进制体积的其他小幅改进,例如 Brad Fitzpatrick 的 CL 228111,如果只使用了 TLS 客户端和服务端中的一端,就避免在输出中同时包含两者的代码,使一个 TLS 拨号 hello world 程序的体积减小了 3.2%。
性能改进
Go 1.15 带来了许多细微的性能改进,其中两项较受关注的改进来自多产的非 Google 贡献者 Josh Bleecher Snyder。CL 216401 在将小整数转换为接口值时避免了内存分配,使编译到汇编的耗时提升了 2%。将值转换为接口类似于其他语言中的“装箱”;这一优化在思路上与 Python 的小整数缓存相似,不过由于静态类型的缘故,在 Go 中这种情况要少得多。
Snyder 的另一项改动是位于编译器和运行时内部的 CL 226367,它让编译器可以在垃圾回收器的写屏障调用中使用更多的 x86 寄存器。Go 使用写屏障(有点类似于锁)来在 GC 与用户代码并发运行时维护堆上的数据完整性(这篇关于 Go GC 的详细分析中有更多信息)。这使得二进制文件略微变小,编译时间也提升了 1%。
Michael Knysze 通过重新设计内存分配器的“mcentral”数据结构来减少锁争用,显著提升了大块内存分配的吞吐量。对于 12KB 或更大的内存块,新的分配代码速度提升了一倍以上。
工具链与移植
Go 的“modules”功能(Go 的依赖管理系统)最初在 Go 1.11 中引入,并在 1.13 中加入了对模块镜像即“代理”的支持。1.15 版本新增了对回退代理的支持,允许 go 工具在下载模块源码时,如果首选代理失败则回退到备用主机。回退通过 GOMODCACHE 环境变量新增的“|”分隔符来指定。
Go 1.15 移除了两个较旧的移植版本:darwin/386 和 darwin/arm,它们曾为 macOS 及其他 Apple 操作系统提供 32 位二进制文件。Fitzpatrick 指出,macOS Catalina 已不再支持运行 32 位应用,因此移除这些移植将有助于释放 macOS 构建机器,同时也能让编译器体积略微减小。这些移植在 Go 1.14 版本中已被宣布弃用,并将在 Go 1.15 中被移除。
另一方面,linux/arm64 移植被升级为“一级移植”,这意味着该平台的构建失败将会阻断版本发布;Go 团队也会为其提供官方二进制文件和安装文档。正如 Fitzpatrick 所指出的那样,Linux 64 位 Arm 如今至少与已是一级移植的 32 位 Arm 同等重要。
在 Windows 上,Go 1.15 现在默认生成启用地址空间布局随机化(ASLR)的可执行文件(默认启用)。ASLR 利用位置无关代码在启动时随机化各个数据区的地址,使攻击者更难预测目标地址并制造内存破坏漏洞。
标准库新增内容
Go 的标准库体量庞大且相当稳定;在 Go 1.15 中仅加入了相对次要的新功能。
标准库中的 testing 包非常精简——Go 的理念是避免为编写测试和断言引入领域特定语言,而是直接使用开发者已掌握的普通 Go 代码来编写。但核心开发者认为创建临时目录这一操作足够常用,因此批准新增了一个 TempDir() 方法,该方法会为当前测试惰性地创建一个临时目录,并在测试结束时自动将其删除。
net/url 包新增了一个 URL.Redacted() 方法,它以字符串形式返回 URL,但其中的密码会被隐去(替换为 xxxxx)。像 https://username:[email protected]/ 这样带密码的 URL 如今在浏览器中已不常用,但在脚本和工具中仍然出人意料地常见。Redacted() 可用于更安全地记录 URL,符合 RFC 3986 中关于不应将 : 之后部分以明文呈现的指导原则。
新增的 time/tzdata 包允许在可执行文件中嵌入一份静态的时区数据库副本。由于它会给可执行文件增加约 800KB 的体积,因此需要主动选用:要么导入 time/tzdata 包,要么在编译时使用 timetzdata 构建标签。嵌入的数据库可以使时区数据库的访问在某些系统(尤其是 Windows)上更加一致和可靠,在 Docker 容器和 Go playground 等虚拟化环境中也可能很有用。
结语
Go 使用 GitHub issues 来跟踪所有缺陷和功能请求,因此你可以通过查看 Go 1.15 里程碑中已关闭的 issue 列表来进一步了解该版本包含的内容。距离 1.15 正式版发布还有两个多月,但你可以使用 gotip 工具轻松地用最新版本测试自己的代码,或者等待定于 6 月 1 日发布的二进制测试版。现在发现的缺陷几乎肯定会在 1.15 正式版发布前得到修复。
随机一篇博客
评论
登录后参与讨论