作者 | 郝林
本文是“2021 InfoQ 年度技术盘点与展望”系列文章之一。
计算机世界似乎已经出现了一种“两端一体”的现象。
一方面,越来越多的计算需求被转移到了云端(即云计算端)。若是相关从业者,应该已经对“把程序部署到云端”习以为常了;另一方面,一些终端变得越来越智能化了。有的厂商,甚至把神经网络引擎(一种用于执行机器学习和人工智能任务的处理单元)内置在了手机当中。
我们可以说,大的“智能”在云端、小的“智能”在终端的形态逐渐形成。不过,它们并不是割裂的,而是逐渐走向了融合。它们正在共同编织一张“人工智能协同处理网络”。
在这张拼图当中,Go 语言的定位和优势显然都是在云端的。如果说得更具体的话,Go 语言的主要战场在云端的应用层(如 Web 服务、中间件等),而不在云端的底层(如操作系统、驱动程序等)。
想当年,Go 语言号称是云计算时代的 C 语言。它的目标是达到 C 语言的程序运行效率和 Python 语言的程序开发效率。到目前为止,虽然我们不能说这一目标已经被完全实现,但却可以将其形容为“十分接近”了。
当然了,我们评估一门编程语言肯定不会只看它的程序运行效率和程序开发效率。这还涉及到开发团队、技术社区、技术生态等等相关问题。不过,这就是另外一个话题了。
趋势概览
在今年的 11 月 10 日,Go 语言迎来了它的 12 岁生日。从趋势来看,Go 语言在 2021 年是比较稳定的。TIOBE Index(著名的编程语言排行榜)对 Go 语言的使用情况统计(https://www.tiobe.com/tiobe-index/go/)很明显地说明了这一点。这与它从 2010 年到 2018 年间的大起大落形成了鲜明的对比。
图 1 - TIOBE Index 之 Go 语言趋势
而 Google 搜索引擎的趋势统计对此也有所印证。
图 2 - Google Trend 之 Go 语言趋势(2021 年,全球)
不出意外,今年又是中国的 Go 语言爱好者贡献了绝大部分的搜索量。
图 3 - Google Trend 之 Go 语言热度(2021 年,国家级别)
这简直就是引领全球啊!Go 语言在中国太火爆了!
当我们把焦点对准中国,就可以看到国内最关注 Go 语言的人都在哪些城市了。
图 4 - Google Trend 之 Go 语言热度(2021 年,城市级别,第 1 页)
图 5 - Google Trend 之 Go 语言热度(2021 年,城市级别,第 2 页)
请注意,上面所展示的热度都基于的是最近 12 个月的数据。说实话,在这里排名第一和第二的济南市和西安市很出乎作者的意料。也许这两座城市之中有某些学校和公司在大力地推广 Go 语言吧。当然了,这只是猜测而已,并没有数据上的支撑。
对于之后的那些城市的排名,就作者所掌握的情况来看,应该是差不多的。北京一向是科技公司所向往的地方。深圳的软件公司也在近些年来迅速的崛起。而由于成都的独特魅力,这两年也有不少的软件公司去那里安营扎寨。至于杭州、上海、广州也不用多说,它们一直以来也是互联网和软件公司比较扎堆的地方。
我们再来看看相关的搜索词。
图 6 - Google Trend 之 Go 语言热度(2021 年,搜索词)
作为研究 Go 语言多年的人,作者能够从这里看出,想学习或刚刚学习 Go 语言的人在搜索这门编程语言的人群当中占了绝大多数。这可能与 Go 语言已经开始在高校普及有关。当然了,“越来越多的公司(尤其是那些头部公司和新星公司)都开始使用 Go 语言”肯定也是很重要的一个原因。因此,不论如何,Go 语言在国内仍然有着非常大的潜力。
好了,我们在前面纵深式地观察了 Go 语言在中国的热度。现在,让我们再来横向地对比一下。
除了 Go 语言,作者还选取了一些在主打领域方面与它有较多交集的编程语言。如下图所示,它们是 C 语言、C 语言、Java 语言和 Rust 语言。
图 7 - Google Trend 之编程语言热度对比(2021 年,全球)
从全球范围看,Go 语言除了远不及 Java 语言,还与 C 语言和 C 语言在热度方面有着不小的差距。然而,如果我们聚焦于中国,那么就会另有发现。
图 8 - Google Trend 之编程语言热度对比(2021 年,中国)
对于国内,应用层面的编程语言显然会受到更多开发者的关注。作者认为,这应该主要得益于我们发达的互联网行业。Go 语言在这里仅次于 Java 语言。而作为底层开发霸主的 C 语言在这 5 门编程语言当中才排到倒数第二。虽然本篇文章主要解读的是 Go 语言,但这样的情况也不由得让作者陷入思考——国内的广大开发者对于软件基础设施的关注度是不是太低了?
我们再来看 StackOverflow(全球最大的编程社区和问答网站)发布的 2021 年度开发者生存报告(https://insights.stackoverflow.com/survey/2021)。该报告是全球性的。虽然中国开发者的参与度仍然较低,不过并不妨碍我们拿来参考。
图 9 - Stack Overflow Developer Survey 2021(最喜爱的编程语言)
在这份报告的“最喜爱的编程语言”排名中,Go 语言排在了第 10 位。与去年相比,它提升了 2 位。而 Rust 语言与去年一样,依然占据着第一的位置。然而,如果我们再回头去看 Rust 语言在 Google 搜索引擎中的热度,是不是可以看出一些端倪呢?也许,这与 Rust 语言较高的学习门槛有关。纵观而论,比 Go 语言更受爱戴的那些编程语言,不是语言特性的魅力十足,就是拥有庞大的语言生态(或者说,第三方程序库和开发工具异常丰富)。
相比之下,作者认为下面的“最想用的编程语言”排名对于专业的程序员来说更有参考价值。在此排名中,Go 语言和 Rust 语言是非常接近的。不过,它们还不及那些绝对主流的脚本语言。从这里我们也可以看出,编程语言的易用性(以及程序的开发效率)对于开发者们的选择来说是至关重要的。
图 10 - Stack Overflow Developer Survey 2021(最想用的编程语言)
当然啦,我们也不能免俗,最后看一看这份报告中的(全球范围内的)薪资与开发经验象限。
图 11 - Stack Overflow Developer Survey 2021(薪资与开发经验象限)
此象限代表的是一个基于大数据统计的结果。其中的薪资取的是开发者年薪的中位数,而开发经验取的是开发者从业年数的平均值。不过,它仍然有一定的参考价值。
我们可以看到,在全球范围内,Go 语言的薪资还是很高的。这甚至比更加新颖的 Julia 语言和 Swift 语言还要高。而作为 Go 语言强劲对手的 Java 语言,可能是由于市场过于饱和,相关开发者的年薪中位数与 Go 语言相差甚远。
到这里,我们已经一起看了不少的图表。从中我们能够看出,Go 语言的发展趋势还是很稳的。在这方面,国内的情况明显好于国外。近年来,越来越多的国内开发者开始学习和尝试 Go 语言,越来越多的国内互联网公司和软件公司也开始接纳并使用 Go 语言。作者认为,Go 语言在中国的市场占有率还远没有到顶,后面还是大有可为的。
年度回顾
讲完了大的趋势,下面我们来说说 Go 语言在 2021 年里都更新了哪些东西。
按照惯例,Go 语言会在每年的 2 月份和 8 月份各发布一次小版本(即 minor version)更新。在 2021 年,官方团队按时发布了 Go 语言的 1.16 版本和 1.17 版本。由于篇幅有限,我们只在这里讨论那些看起来最重要或者最显著的更新。
新的官方网站
Go 语言的爱好者们肯定都知道,Go 语言的官方网站是 golang.org。然而,由于一些网络原因,国内的用户访问这个网站会有一定的困难。但毕竟中国的 Go 语言使用者群体是非常庞大的,因此 Go 语言官方团队(以下简称 Go 团队)在前些年又启用了一个中国专属的官方网站:golang.google.cn 。
然而,在 2019 年 2 月,Go 团队又注册了一个新的域名:go.dev 。这里的 .dev 是 Google 公司自己搞出来的域名后缀。很显然,这个域名非常的短小,而且很容易记忆。自此,Go 团队开始逐渐地把 golang.org 以及相关网站上的内容整合到 go.dev 之下,力求把诸多分散的 Go 官方内容统一起来。
在 Go 语言 12 岁生日的那一天,Go 团队发表了一篇特殊的博客文章,并在其中宣布了它们对 go.dev 网站的大改版。这标志着关于 go.dev 的内容整合工作基本完成。除了原先在 golang.org 下的主要内容(如下载说明、文档、教程、官方博客、playground 等)都被迁移到了 go.dev 之外,作为 Go 程序包档案集散地的 godoc.org 也被合并进了 go.dev,且拥有了新的二级域名 pkg.go.dev 。
图 12 - 新版 pkg.go.dev 的截图
大家可以去 pkg.go.dev 上看一看,它现在已经非常强大了。我们不但可以非常方便地搜索和查看(包括官方程序包和第三方程序包在内的)Go 程序包及其中的代码和文档,还可以快速获知某个程序包的最新版本、更新日期、分发协议、模块管理、仓库地址等信息,并可以轻易地了解到它使用了哪些其他的程序包,以及有哪些程序包正在使用它。毫不夸张地说,查找 Go 程序包,使用它就够了。
最后,最关键的是,我们在国内访问 go.dev 目前是畅通无阻的。
模块管理
今年,Go 团队在 Go 语言的模块管理方面下了非常多的功夫。可以说,现在的模块管理功能已经相当好用了。如果你维护的 Go 程序包还在基于原始的 GOPATH,并且有计划迁移到 go module 上来,那么我强烈建议你去看一看我在前不久发表的一篇短小的教程:《Go 项目迁移:从 GOPATH 到 go mod》。
简单解释一下,我们在这里所说的模块(module)其实与前面所讲的程序包含义相同。自从 Go 官方的 go module 机制诞生,这两个名词在 Go 模块管理的场景之下就完全可以划等号了。它们指的都是,可以统一打包发布并供给其他程序使用的代码集合。唯一不同的是,程序包更像是民间的非正式说法,而模块是官方的正式说法。
与之相关的还有一个名词,叫做代码包(package)。一个代码包是指,处在同一个文件目录中的多个源码文件(source file),且这些源码文件都有完全相同的 package 声明语句。多个代码包可以同属于一个模块。反过来讲,一个模块可以按照功能、层次等逻辑再把其中的源码文件进一步划分到多个代码包当中。当然了,一个模块当中也可以只有一个代码包。而一个代码包肯定会属于某一个模块。
因此,当 Go 官方的模块管理功能正式上岗之后,我们就有了一个从模块到代码包(以及子代码包)再到源码文件的多级代码组织方案。其中,源码文件是代码编译的最小单元,代码包是代码组织的最小单元,而模块则是代码发布的最小单元。一旦理解了这些,你通常就可以很快上手 Go 语言的模块管理工具了。
由于主题所限,我们下面暂且抛开那些模块管理的方法和技巧,重点来说一说 Go 团队在这方面的更新情况。
系统环境变量 GO111MODULE
如果你使用过 Go 语言的旧版本的话,那么肯定会知道 GO111MODULE 这个系统环境变量。它的目的是,方便开发者们在原始的 GOPATH 机制和新的 go module 机制之间做切换。这是 Go 团队的一贯做法。
当一个新的机制落实并发布之后,他们会先通过提供可选的功能让广大开发者试用一段时间,同时也方便他们在此期间对该机制进行改进。在这个过程中,Go 团队会随着该机制的逐渐成熟在新版本的 Go 语言里调整对应的系统环境变量的默认值。
比如,他们起初会先设置默认值为 off(即默认关闭)。等相关的 bug(即缺陷)大都修正完毕了,功能也改进得差不多了,他们就会把默认值设置为 auto(即自动选择)。待该机制基本成熟之后,他们还会把默认值设置为 on(即默认开启)。最后,一旦这个机制趋于完善, Go 团队就会把对应的系统环境变量(以及相应的切换功能)从新版本的 Go 语言中去掉,并以此完全切换到新的机制之上。
系统环境变量 GO111MODULE 的存在已经有几年的时间了。这也从侧面说明了 go module 从诞生至今历经了些许的坎坷。不过,Go 团队终于在 Go 语言的 1.16 版本中把 GO111MODULE 的默认值设置为了 on 。这标志着 go module 机制的成熟。同时,这也说明 Go 团队已开始正式普及 go module 机制。
从 Go 官方提供的标准工具来看,原有的那些 go 命令都已经完全适配了 go module 机制。比如,go get 命令现在可用于调整 Go 模块的依赖关系,go install 命令现在可用于下载、编译和安装 Go 模块, go test 命令现在也可用于编译并测试 Go 模块,等等。
Go 模块的配置文件:模块图修剪
再说 Go 模块的配置文件。大家可能已经知道,go module 机制的落实正是围绕着配置文件 go.mod 和 go.sum 展开的。而这两个配置文件会由 go mod 命令在当前的 Go 模块(或称主模块)的根目录下自动生成。
第一个变化是,在 go.mod 文件中,针对主模块的直接依赖模块记录和间接依赖模块记录已变得完整。(以下会把直接依赖模块和间接依赖模块统称为依赖模块)
在 1.17 版本之前,Go 团队一直在适当调整依赖模块的记录方式。比如,在 1.16 版本中,go.mod 文件并不会记录间接的依赖模块。在这种情况下,一些 go 命令(如 go build)就需要去彻底查找主模块可能会依赖的所有模块。即使一个模块只是与某个真正的依赖模块有连带关系,但实质上并未被主模块真正依赖,它也会被那些 go 命令认定为依赖模块。但实际上,这里的依赖应该是打引号的。
因此,如果这个“依赖”模块还没有被存储到本地计算机上,那么 go 命令就会报错。它会报告“缺少某某‘依赖’模块”,也就是说,没有在本地计算机上找到这个“依赖”模块。这显然是不合理的。既然没有实际用到,就应该无视它。何必去费力查找呢。而且,更不应该在找不到的时候报错。
实际上,这种情况发生的可能性并不大。但由于 Go 社区对此问题的质疑声很大,所以 Go 团队最终在 1.17 版本中对这方面做了彻底的梳理和修正。
如果主模块的 go.mod 文件声明的 Go 版本是 1.17 或更高,那么 go mod 命令就会在该文件中记录主模块的所有直接依赖模块和间接依赖模块。如此一来,其他的 go 命令就可以只去查找在这里记录的那些模块了。显然,go mod 命令与其他 go 命令之间的分工更加合理了。新版本的这种行为也被称为模块图修剪(module graph pruning)(https://go.dev/ref/mod)。
虽然模块图修剪可能会使主模块的 go.mod 文件记录比以前多得多的东西,但通常来讲问题是不大的。而且,新版本的 go.mod 文件在可读性方面也得到了明显的优化。
此外,为了让大家更加平滑地更新 go.mod 文件,go mod tidy 命令添加了对 -go 标记(即 flag)的支持。我们可以在执行该命令的时候追加这个标记,并以此修改 go.mod 文件中对其使用的 Go 版本的声明(具体体现为 go 指令),如:
代码语言:javascript复制go mod tidy -go=1.17
当这样的命令执行的时候,不但 go.mod 文件中的 go 指令会被改变,而且其中相应的依赖模块记录也会随着这里指定的版本而修改(就像我们在前面说的那样)。
Go 模块的配置文件:新的指令
在 1.16 版本中,Go 团队为 go.mod 文件增加了一个新指令。这个指令的名字叫做 retract。我们在这里可以把它理解为“撤回”。
当我们已经对外发布了当前模块的某个版本,但又由于某种原因想把它撤回来的时候,就可以使用这个 retract 指令。
这个指令的作用是,告知 go 命令“当前模块的某个版本不应该被他人使用”。我们可以随着新版本的发布来对外告知某个旧版本的“撤回”。例如,我们要提交模块 A 的 1.2.3 版本,并同时对外告知“该模块的 1.2.2 版本存在一个高危漏洞”,强烈建议大家不要再使用。那么,我们就可以在模块 A 的(1.2.3 版本的)go.mod 文件中添加:
代码语言:javascript复制// 此版本存在一个高危漏洞,请尽快升级!
retract v1.2.2
在这个模块的 1.2.3 版本被成功推送到代码仓库之后,当其使用者试图通过某些 go 命令更新该模块的信息的时候(如:go list -m -u),就可以在显示屏上看到相应的警告了。
Go 模块的配置文件:新的注释
我们刚刚提到的 retract 指令是用来“撤回”某个模块的某个版本的。而在 Go 语言的 1.17 版本中为 go.mod 文件增设的 deprecation 注释则是用来废弃整个模块的。
当我们想要对当前模块进行非向后兼容的版本升级(如从 v1 到 v2),或者想完全放弃维护当前模块的时候,就可以考虑在 go.mod 文件中的 module 指令之上添加 deprecation 注释,如:
代码语言:javascript复制// Deprecated: use example.com/mod/v2 instead.
module example.com/mod
这里的废弃注释的含义是,mod 模块已更新到了一个非向后兼容的大版本(v2),并且旧的大版本(v1)将不再受到进一步的维护。这相当于建议和敦促所有正在使用 v1 版本的开发者迁移到 v2 版本上去。
解释一下,废弃注释的前缀“// Deprecated: ”是固定的。在它后面的应该是相应的废弃说明,并且该说明不能另起新行书写。不过,整个废弃注释除了可以紧邻在 module 指令之上,还可以被写在该指令的右边,如:
代码语言:javascript复制module example.com/mod // Deprecated: use example.com/mod/v2 instead.
注意,这里对 go.mod 文件的更改需要随着当前模块的 v1 版本的更新一起提交到代码仓库之上。这样才能够使得这一注释起到相应的作用,其使用者在试图更新该模块信息的时候才能够看到相应的提示。
标准命令
关于 Go 语言内置的标准命令,除了我们已经在前面说过的那些随着模块管理功能优化而来的调整之外,Go 团队还在易用性方面对它们进行了进一步提升。
go install 命令
首先,在 1.16 版本,Go 官方对 go install 命令进行了改进,使它可以接受一种版本后缀(如:@v1.0.0),并以此来下载、编译并安装(以下统称为安装)某个代码包的特定版本。其中,“@”必须作为版本后缀开头的字符。而在它之后的版本号则需要遵循语义版本规范( https://www.infoq.cn/article/CEoMOxgW4X7GCYr4eUEi)(但也有例外,具体请参看版本查询的文档https://go.dev/ref/mod)。Go 语言本身的版本号实际上也是遵循该规范的。如此一来,我们就可以像下面这样使用 go install 命令:
代码语言:javascript复制go install example.com/cmd/pkg@v1.0.0
上述命令会安装代码包 example.com/cmd/pkg 的 1.0.0 版本。如果这个代码包是可执行的(即 main 包),那么该命令将会把生成的可执行文件放入系统环境变量 GOBIN 所指向的目录(前提是已经设置了这个系统环境变量)。否则,该命令就会把生成的归档文件(一种扩展名为 .a 的文件)放置到专用的构建缓存目录。不过,无论哪种情况,go install 命令都会先把它下载的相关文件放进模块缓存目录。
大家可能已经知道,在 go module 机制下,go install 命令通常会先查找当前目录或其父目录中的 go.mod 文件,然后依据其中记录的对应代码包的信息(如果有的话)来安装指定的版本。但如果我们在使用 go install 命令时携带了版本后缀,那么该命令就会无视 go.mod 文件中的记录,转而安装我们显式指定的版本。
go get 命令
从 1.16 版本开始,Go 官方推荐开发者在 go module 机制下只使用 go install 命令来安装代码包。虽然 go get 命令也可以用来安装代码包,但是它还会在 go module 机制下修改相应的 go.mod 文件。这也是作者在前面说“go get 命令可用于调整 Go 模块的依赖关系”的原因。显然,目前这两个命令在功能方面是有重叠的。这很可能会给开发者带来困扰。
因此,Go 官方向开发者提出了强烈建议:应该在使用 go get 命令的时候携带 -d 标记。该标记会使 go get 命令只修改相应的 go.mod 文件(即调整模块的依赖关系),而不会下载、编译和安装它。
实际上,不携带 -d 标记的 go get 命令已经在 Go 语言的 1.17 版本中被废弃了。而且,在即将推出的 1.18 版本里,go get 命令将会默认启用 -d 标记。也就是说,从 1.18 版本开始,我们即使在使用 go get 命令时不携带 -d 标记,该命令的行为也会等同于携带了 -d 标记。这样的话,go get 命令和 go install 命令就可以各司其职了。相应的,go build 和 go test 这样的命令也不会再去修改 Go 模块中的任何配置文件了。
顺便说一句,在我们使用 go get 命令调整当前模块的依赖关系的时候,若想在 go.mod 文件中删掉某个直接依赖模块的记录,那么可以使用 @none 这个版本后缀。
除此之外,1.16 版本的 Go 语言还废弃 go get 命令对 -insecure 标记的支持。并且,该标记已经在 Go 语言的 1.17 版本中被删除掉了。这个标记原先用于从不安全的网站(如基于 HTTP 而非 HTTPS 协议的网站)获取 Go 代码包,并且还用于绕过关于模块的安全校验。如果我们仍需使用上述功能,那么可以通过设置系统环境变量 GOINSECURE、GOPRIVATE 或 GONOSUMDB 来实现。
标准库
下面,我们来简要地说一下 Go 语言标准库中的变化。其中大大小小的变更有很多,但从整体来看它们都不是最关键的。因此,作者只会在这里提及那几个新增或废弃的代码包。
新增的 3 个代码包
从 1.16 版本开始,Go 语言的标准库中增加了 runtime/metrics 包。简单来说,这个代码包的出现是为了方便 Go 程序在其运行的时候自行获取它的各种指标。这样的指标有很多,涉及垃圾回收、内存使用、并发调度等。这个代码包在功能上取代了之前已经存在的 runtime.ReadMemStats 函数和 debug.GCStats 结构体,并且更加的通用和高效。
此外,io/fs 包和 embed 包也都是在 1.16 版本中被引入的。
代码包 io/fs 代表了一种全新的文件系统模型,或者说,它是关于文件系统的一个统一的高层抽象。它的出现使得 Go 语言标准库中的不少代码包都发生了变化,并且出现了一些新的 API。当然了,这些变化都是有利于 Go 语言和开发者的。
而代码包 embed 则主要用于在 Go 程序的可执行文件中嵌入额外的资源,比如:文本文件、图片文件、音视频文件,以及其他的数据文件等等。而且,我们还可以为此指定多个文件,甚至多个目录。这会涉及到注释指令 //go:embed 的合理使用。
我在本系列的前一篇文章(即:解读 Go 语言的 2020:变革前夜 https://www.infoq.cn/article/CEoMOxgW4X7GCYr4eUEi)当中已经对 io/fs 包和 embed 包有过简要的说明,所以在这里就不再赘述了。对于想用好这几个新包的开发者,作者强烈建议:仔细查阅 Go 标准库的相应文档,并在必要时阅读相关的 Go 语言源码。
废弃 io/ioutil 包
Go 团队现在已经认定,io/ioutil 包是一个定义不清而且难以理解的程序集合。因此,从 Go 语言的 1.16 版本开始,这个包中提供的所有功能(的主要实现代码)都已被迁移到其他的代码包当中(如 io 包和 os 包)。不过,为了保持向后的兼容性,io/ioutil 包会被继续保留,并像以前那样提供正确的功能。下面是关于此的功能迁移列表:
- io/ioutil.Discard 的功能已移至 io.Discard;
- io/ioutil.NopCloser 的功能已移至 io.NopCloser;
- io/ioutil.ReadAll 的功能已移至 io.ReadAll
- io/ioutil.ReadDir 的功能已移至 os.ReadDir(但要注意,两者返回的第一个结果值的类型不同,前者是 []fs.FileInfo,而后者是 []os.DirEntry);
- io/ioutil.ReadFile 的功能已移至 os.ReadFile;
- io/ioutil.TempDir 的功能已移至 os.MkdirTemp;
- io/ioutil.TempFile 的功能已移至 os.CreateTemp;
- io/ioutil.WriteFile 的功能已移至 os.WriteFile 。
语法
Go 语言的 1.17 版本中增加了一项微小但强大的改进,即:支持从切片到数组指针的转换。更具体地说,类型为 []T 的切片现在可以被正确地转换为以 *[N]T 为类型的数组指针了,如:
代码语言:javascript复制slice1 := []int{0, 1, 2, 3, 4}
array1 := (*[5]int)(slice1)
不过,需要特别注意的是,如果我们给定的数组指针类型不恰当,那么这里的第二行代码就会立即抛出一个运行时异常(即 panic)。例如,如果我们给定的类型当中的(代表数组长度的)数值大于要被转换的那个切片的实际长度,如代码 (*[6]int)(slice1) ,那么程序就会由于这里抛出的异常而崩溃(如果没有妥善处理的话)。
关于性能
按照惯例,Go 团队每年都会对 Go 语言在某些方面的性能进行改进。今年当然也不例外。
在 1.16 版本中,Go 语言的链接器在性能方面得到了进一步的提升。在 64 位的 Linux 操作系统上,其链接速度比 1.15 版本快了 20%-25%,同时链接操作所占用的内存空间也减少了 5%-15%。在其他的计算平台上,此类性能提升有过之而无不及。此外,由于更激进的符号修剪,Go 程序经处理后产生的二进制文件通常也更小了。顺便说一下,这里所说的计算平台是计算架构(如 386、amd64、arm 等)和操作系统(如 windows、linux、darwin 等)的组合和统称。其中的 darwin 是 macOS 操作系统在 Go 语言当中的代号。
在 1.17 版本中,Go 团队实现了一种使用寄存器而不是堆栈来传递函数参数值和结果值的新方法。大家都知道,堆栈指的是内存中的某块空间。所以,使用堆栈通常可以不关心各个计算架构(以及不同型号的 CPU)之间的差异。但其缺点也很明显,即性能较差。大家应该也知道,寄存器是 CPU 中的存储器件。因此,使用寄存器的话就不得不去关注计算架构这种更底层的东西了。这显然是更加困难但性能更优的方式。总之,这一新方法让 Go 程序的运行性能提升了大约 5%。并且,Go 程序产出的二进制文件通常也会小 2% 左右。目前,在 Linux、macOS 和 Windows 操作系统的 64 位计算结构上,Go 语言都自动启用了此功能。
其他更新
众所周知,Apple 公司已经推出了自己的 ARM 计算架构的 CPU,并且把它用在了自家的电脑上。因此,一种新的计算架构 - 操作系统组合(即计算平台)出现了,它就是:darwin/arm64 。
以前,在 Go 语言中,iOS 操作系统所对应的计算平台代号是 darwin/arm64 。因为在那个时候,Apple 公司只在智能手机 iPhone 和平板电脑 iPad 当中使用 ARM 计算架构的 CPU 。然而,今日不同往日,所以 Go 团队在 Go 语言的 1.16 版本中适配了上述新的组合。
他们把 macOS 操作系统对应的计算平台代号确定为 darwin/arm64,而原先的 iOS 操作系统所对应的计算平台代号被重命名为 ios/arm64 。也就是说,在 Go 语言的上下文中出现了一个新的操作系统代号:ios 。目前,它与 darwin 一起覆盖了 Apple 公司推出的主要操作系统。
另外,Go 1.16 还添加了一个代号为 ios/amd64 的计算平台。这又是一种新的组合。这个计算平台针对的是,在基于 64 位 AMD 计算架构的 macOS 操作系统之上运行的 iOS 模拟器。
随后,在 1.17 版本中,Go 语言又支持了 Windows 操作系统与 64 位 ARM 计算架构的新组合,代号为 windows/arm64 。这也可以从侧面体现出,移动计算平台正在与原先的桌面计算平台融合。
好了,以上就是作者针对 Go 语言在 2021 年的主要变化做出的一个简要的梳理。作者认为,对于广大的开发者而言,Go 语言在该年度最喜人的变化莫过于模块管理功能以及相关命令方面的大幅改进。当然了,Go 语言标准库中新增的那三个代码包也很重要。
未来展望
近年来,中国的 Go 语言使用者一直在大幅的增长。随着这股增长势头出现的是,大家对 Go 语言的不满。我们时常开玩笑说:“还没有抱怨四起的编程语言肯定不是流行的语言”。随说这是一句玩笑话,但却有一定的道理。因为,一旦大家用的多了,就肯定会发现一门编程语言之中存在着这样或那样的问题。人无完人,对于编程语言来说更是如此。
对于 Go 语言来说,中国开发者抱怨的主要问题有三个,即:模块管理工具、泛型语法支持(以下简称 Go 泛型),以及程序错误的处理方式。
对于程序错误的处理方式,Go 团队尝试过解决,但至今还没找到一个令他们满意的方案。而且,据作者估计,Go 团队最早也要等到 Go 泛型足够完整和稳定之后才会再把它提上日程。所以,本文不会对它进行讨论。
那么,我们下面先从前两个问题开始,对 Go 语言的未来稍作展望。
模块管理工具
如前文所述,在模块管理工具方面,Go 团队其实已经解决的差不多了。不过,在 2022 年,Go 语言在这方面还会有一些改进。
比如,在明年 2 月份发布的 1.18 版本中,Go 语言将会支持一种被叫做工作区的新模式(workspace mode)。如果大家一直在使用 Go 语言的话,可能还会记得 GOPATH 机制下的工作区目录。但这里所说的工作区模式与那个工作区目录完全是两码事。所以请大家注意,千万不要混淆。
在默认情况下,go module 机制下的标准命令会到网络上寻找当前模块的依赖模块。它们搜寻的地点会包括 Go 语言支持的那些公共的代码仓库(如 Github 等),以及我们自行搭建的私有代码仓库(前提是我们已经做了相应的设置)。
然而,当我们在本地计算机上同时维护着存在依赖关系的多个 Go 模块的时候,常常想让被依赖的模块(以下简称依赖模块)中的改动即时地体现到依赖它的那些模块(以下简称主模块)之上,而不需要事先把这种改动推送到代码仓库当中。
在当前的 go module 机制下,我们可以通过在主模块的 go.mod 文件中添加 replace 指令来满足这一需求。指令 replace 可以在依赖模块的导入路径与它在本地计算机的存储路径之间建立一个映射,就像这样:
代码语言:javascript复制replace example.com/mod => /Users/haolin/GoWorkspace/mod
上述指令为一个导入路径为 example.com/mod 的模块建立了一个映射,将它的导入路径指向了它在(装有 macOS 操作系统的)本地计算机上的存储路径。如此一来,go 命令在编译该主模块的时候就可以顺利地找到这个名为 mod 的依赖模块了。随后,在我们运行这个主模块的时候,它就可以及时地体现出(在本地计算机上的)那个依赖模块中的新改动了。
这个解决方案显然是可行的。但却带来了一个问题,那就是:它将针对于本地开发环境的私有配置与属于整个项目的公共配置混淆在了一起(即都放置在了 go.mod 文件中)。这使得我们在向代码仓库提交公共配置的时候,不得不携带上私有配置。这种私有配置对其他开发者理解、使用和改进当前模块没有任何的好处,而且还对公共配置造成了污染。更重要的是,它还可能会让其他开发者无法顺利地编译这个模块。因为,其私有配置中的依赖模块存储路径在其他计算机上很可能是不正确的。
以上正是 go module 机制的工作区模式诞生的主要原因。在作者编写本文的时候(即 2021 年 12 月上旬),这个功能还在开发当中,其中的一些东西还没有完全确定下来。
不过,该模式预计会涉及到新的标准命令 go work、新的配置文件 go.work,以及新的配置指令 directory 。简单来说,go work 命令可以被用来创建拥有若干 Go 模块的工作区目录。同时,go work 命令还会在这样的目录中生成 go.work 文件,并在其中添加相应的 directory 指令。这个 go.work 就是专门容纳本地开发环境配置的文件。其中也可以有 replace 指令。如此一来,我们在提交和推送代码的时候就可以把 go.work 文件排除在外,并以此避免私有配置的上传。
泛型语法支持
泛型(generics)在很多时候也被称为类型参数(type parameter)。我们在这里所说的泛型语法支持问题,其实说的是,对开发者自定义泛型类型和泛型函数的支持。
事先声明,作者并不想在这里深入讨论泛型的定义和设计。因为那是一个非常庞大的话题,恐怕即使用足一整篇文章也不一定能说清。作者只想通过两条线来简要地阐释一下关于 Go 泛型的思考。
第一条线,编程风格线。
Go 语言实际上早已对泛型有了一定的支持。像数组(array)、切片(slice)、字典(map)这种语言内置的可作为数据容器的类型(以下简称容器类型)其实一开始就是支持泛型的,如:[]Int 和 map[int]string 。然而,那些后加入的标准库级别的容器类型却没有泛型的支持,如 container 包中的类型 List 和 Ring 等。这正是因为 Go 语言中没有关于自定义泛型的语法。
Go 语言是一门崇尚简约、面向工程的通用编程语言。不论是语言语法、标准库还是标准工具,都对此有着深刻地体现。在一个概念、一条语法或一项功能被正式推出的时候,它肯定不会与 Go 语言的其他部分存在重叠和混淆。也就是说,它们之间都是正交的。
Go 程序中可以有接口声明。我们可以说,接口在 Go 程序中承担着类型抽象化和类型约束的职责。然而,自定义泛型的到来将改变这一职责的划分。
就设计方案来看,Go 语言中的自定义泛型会与接口接合起来形成新的正交组合,从而发挥出比以前强大得多的类型抽象能力。毫不夸张的说,这将使 Go 语言在这方面的能力获得维度上的提升。这类似于从基于二维呈现的照片转变到基于三维呈现的全息投影。
不过,我们在欢喜的同时,也要做好心理准备。这种大幅度的转变必然会带来程序复杂度方面的陡增,而且也会给 Go 语言和 Go 程序的简约带来新的挑战。虽然,从 Go 泛型设计方案的数次版本更新来看,Go 团队一直在不遗余力地简化自定义泛型的语法,并尽量降低自定义泛型带来的关联修改,但是,它给 Go 语言及其编程风格带来的重大影响是不可避免的。对于普通的开发者来说,我们最起码要尽快地适应泛型定义中的方括号“[”和“]”,以及新接口定义中的竖线符号“|”和波浪符号“~”,等等。这明显会给我们带来更多的心智负担,至少对于没有接触过泛型的开发者而言是这样。
最后,泛型与其他那些拥有极大魅力的语法一样,一定会诱使开发者们随处使用,甚至滥用。这种滥用对于后续维护程序的开发者而言很可能意味着深坑。这也是为什么 Go 语言之中极少有语法糖的主要原因。
第二条线,向后兼容线。
与模块管理工具一样,Go 语言的泛型语法支持也是一个老生常谈的问题。开发者们对此的呼声由来已久、从未中断,恐怕已经有数年的时间了。即使只从“Go 团队同意在 Go 语言中加入泛型”开始算起,至今也有 4 年之久了。
Go 团队在 2018 年下旬发布了包含泛型语法支持的 Go2 草案。之后,又经历了数次的调整和细化,直到 2021 年 8 月,Go 团队才放出了一个终极的设计方案:Type Parameters Proposal(https://github.com/golang/proposal/blob/master/design/43651-type-parameters.md) 。至此,一个紧密贴合了 Go 语言的泛型模型才算正式出炉。
实际上,Go 语言的 1.17 版本中已经包含了一些与自定义泛型有关的代码。只不过它们并没有对外开放。可见,Go 团队对于这项重大改进是非常谨慎的。他们在一小步一小步地向前进。
Go 语言的创始人 Rob Pike 说“增加 Go 泛型是 Go 语言正式发布以来最大的一个变化”。因此,即使是 Go 1.18 也只会包含支持自定义泛型的语法,而几乎不会在其标准库中包含关于自定义泛型的修改。除了一个名为 constraints 的代码包,它是编写自定义泛型的基础。一些相关的改动会先在实验性模块 golang.org/x/exp 当中进行。该实验性模块不会保证向后兼容性。而这正是为了 Go 语言的正式版对向后兼容性的绝对保证。
如果不出意外的话,我们可以使用 Go 语言的 1.18 版本编写出带有泛型的代码。不过,我们若想畅快地使用 Go 泛型,恐怕就要等到 Go 1.19,甚至 Go 1.20 了。因为只有到了那时,Go 官方的泛型编程最佳实践才会出现,标准库中各种泛型相关的代码包才能就绪。而且,由于这次关于泛型的改动比较大,所以那些流行的第三方工具很难在短时间内跟上。目前来看,这一切都要慢慢来。
Go 团队认为,保持稳定性和向后兼容性是最重要的事。作者对此非常的认同。更何况,Go 团队的 Go 泛型设计方案相当的优秀,只有同样优秀的实现代码才能配得上它。所以,大家在这件事情上不要着急。好事多磨。反正也等了那么久了,不怕再多等一段时间。
总之,作者强烈地建议大家在明年积极地体验 Go 泛型,以及那些实验性的泛型包。但同时,作者也建议大家,步子不要迈得太大,以免受伤。
其他期待
除了上面所说的与模块管理和 Go 泛型相关的更新之外,Go 团队还会在 Go 语言的 1.18 版本中进行不少的改进。下面,作者再简单地列举几个比较显著的改动:
- Fuzzing:这将是 Go 语言新支持的一种程序测试方式,也被称为模糊测试。这种测试会自动地生成符合程序要求的输入数据(通常是随机的),并以此对程序进行持续的调用。这么做的目的是,测试那个程序对各种输入数据(包括非正常数据)的反应是否与预期相符。这样的随机数据肯定会比开发人员手动提供的输入数据全面许多。所以,模糊测试通常能够对程序进行更加彻底的检查。为了支持模糊测试,go test 命令会接受新的标记 -fuzz ,测试文件也会增加新的测试函数命名格式 FuzzXxx ,另外 testing 包里还会增加新的类型 F 以及一系列新的方法。
- 内嵌于文件的信息:新的 go 命令会将版本控制信息嵌入到二进制文件当中,包括当前的 checked-out 修订版本、提交时间,以及用于指示当前文件的状态(如“已编辑”、“未跟踪”等)的标记。如果 Go 项目使用的是那些主流的版本控制系统(如 Git、Mercurial、Bazaar 等),那么这个功能就会默认开启。若要忽略此信息,我们可以在 go 命令之后附加标记 -buildvcs=false 。除此之外,内嵌的信息还会包含一些与程序构建有关的标记(可使用标记 -buildinfo=false 予以忽略)。若要读取上述这些信息,我们可以执行 go version -m <文件> 命令,或者调用 runtime/debug.ReadBuildInfo 函数,以及使用新的 debug/buildinfo 包。作者记得,以前为了方便 Go 程序的部署和运维,我们会手动地把上述信息添加到程序的源码当中,并对外提供用于打印它们的标记。这下好了,这项工作终于可以自动化了。
- 锁的新方法:即 TryLock 方法。类型 sync.Mutex 将会拥有此方法。如果我们在调用该方法的时候,当前的 goroutine 已经持有了这个锁,那么该方法就会立即返回结果值 true,而不会阻塞当前的 goroutine 。除此之外,sync.RWMutex 类型还将会拥有 TryRLock 方法,以便尝试锁定其中的读锁。此方法实现了广大开发者(包括作者)翘首以盼的功能。在这之前,我们很难探查当前的 goroutine 是否已经持有了某个锁(若重复调用这个锁的 Lock 方法,则会导致当前 goroutine 的阻塞)。
上述几处更新是作者最期待的,仅代表个人意见。作者相信,不同的开发者会对 Go 语言的新版本有不同的期待。更全面详细的 Go 1.18 更新说明请参看(https://tip.golang.org/doc/go1.18)(如果在你读到此处的时候,Go 1.18 已经正式发布了,那么可以尝试访问(https://go.dev/doc/go1.18)
另外,作者当然也同样期待 Go 语言在中国有更大的影响力,国内的技术社区更加庞大、更加多样化。实际上,这样的趋势早已存在了。但作者仍然希望在这之上能再添一把火。
总 结
好了,让我们来快速总结一下。
如果用一个字来代表 Go 语言在 2021 年的表现的话,那就是“稳”。
Go 语言爱好者们都知道,Go 团队在近些年来一直积蓄着力量。终于在今年,Go 语言的模块管理功能得到了进一步的大幅完善,其全新的文件系统模型也出现在了标准库当中。以前造成我们诸多不便的资源文件打包问题也随着 embed 代码包的加入而得到了彻底的解决。
像其他的爱好者一样,作者在期盼 Go 泛型的同时也有着一些担心。我们担心 Go 泛型会破坏 Go 语言的向后兼容性,担心 Go 泛型的语法太过复杂,担心它的到来会使 Go 程序的开发难度和阅读难度明显增加。幸好,由于 Go 团队的充足准备和过硬的设计功底,我们的这些担心都已经随之消失了。而且,作者相信,Go 团队也会在后面给出足够有用的最佳实践,以帮助我们更加正确和高效地使用 Go 泛型。
如果说 Go 语言在 2021 年的更新关键词是“模块管理”的话,那么它在 2022 年的更新关键词就一定是“(自定义)泛型”。不过,无论是这样的较大更新还是本文未曾提到的那些相对小的改进,都在让 Go 语言成为更好的通用编程语言。
对于 Go 程序的开发者,我要说的是:“我们应该对 Go 语言和 Go 团队有足够的信心,相信它和他们都会稳定的发展下去”。另一方面,如果你还没有使用过 Go 语言,那么作者会强烈建议你尝试一下。马上从(https://go.dev/dl/)下载 Go 语言,并把它安装到你的计算机上吧!
参考资料
Go 语言官方文档及 Go 语言源码
Go 1.16 Release Notes: https://go.dev/doc/go1.16
Go 1.17 Release Notes: https://go.dev/doc/go1.17
Go 1.18 Release Notes(DRAFT): https://tip.golang.org/doc/go1.18
作者简介
郝林,国内知名的编程布道者,技术社群 GoHackers 的发起人和组织者,微信公众号“迭代码”的主理人。他发布过很多 Go 语言技术教程,包括开源的《Go 命令教程》、极客时间的付费专栏《Go 语言核心 36 讲》,以及图灵原创图书《Go 并发编程实战》,等等。其中的专栏和图书有着数万的订阅者或购买者,而那个开源教程的 star 数也有数千。另外,他还在 2020 年出版了一本名为《Julia 编程基础》的技术图书。这本书介绍的是源自 MIT 的 Julia 编程语言,主要面向的是广大的编程初学者,以及对函数式编程和数据科学感兴趣的软件开发者。