作者 郝林
在作者去年年底撰写《解读 Go 语言的 2019》的时候,绝没有想到 2020 年将会如此的不平凡。全球范围内的疫情在大大地限制了人们和企业的对外活动之余,还带来了一个副作用,即:线下活动向线上的迅速迁移。
实际上,对于这种迁移,我们国内的民营企业和事业单位早就在做了,只不过在 2020 年之前还没有这么急迫。不知道你发现了没有,在 2020 这一年,那些已经存在的远程办公、视频会议、在线医疗、在线教育等方面的基础设施和应用程序给予了我们莫大的支撑。即便说它们辅助保障了社会的正常运转,也不为过。
目前来看,全球的疫情还会存在一段时间。虽然这个事件本身绝对不值得高兴,但是反过来想,这会倒逼国内数字经济的大踏步前进,甚至飞跃。
从基础层面讲,数字经济的发展必须要有半导体等高精尖领域的强力支持。而从应用层面说,数字经济将会依托于云计算、大数据和人工智能。更具体地说,云计算是高级的基础设施,大数据和人工智能是建立在云计算之上的高级应用。Go 语言,早已霸占了云计算的大半个江山,今后它也将在大数据和人工智能方面发挥重要作用。
趋势纵览
下面,我们依旧先从整体趋势上看看 Go 语言在今年的发展。
在全球范围内,从 2010 年的集体追新,到之后几年内的理性对待,再到 2016 年、2017 年的“第二春”,直至 2018 年的升降大反差和 2020 年的新反弹。Go 语言可谓是经历了诸多风风雨雨,持续地在各种好评和诟病之间砥砺前行,既得意过也失意过。
下图展现了 TIOBE Index(著名编程语言排行榜)对 Go 语言使用情况的最新统计。
图 1 - TIOBE Index 之 Go 语言(2020 年 12 月)
图 2 - TIOBE Index(2020 年 12 月)
我们从上面这两幅图中可以看出, Go 语言在今年的排名又有了大幅的提升。作者个人认为,这与 go mod 工具的转正和推广,以及“泛型”实现的排期确定是分不开的。
同时,据 StackOverflow(全球最大的编程社区和问答网站)在前不久发布的一份开发者生存报告显示,Go 语言在 2020 年是继 Python、Java、C++ 和 C 之后、排名第五的通用型、全平台编程语言。如果把脚本语言和标记语言都算在内的话,它的总排名是第 12 名。
图 3 - Stack Overflow Servey 2020 - The Most Popular Languages
不但如此,Go 语言在“最喜爱”和“最需要”的编程语言排行中也名列前茅。
图 4 - Stack Overflow Servey 2020 - The Most Loved Languages
图 5 - Stack Overflow Servey 2020 - The Most Wanted Languages
我们可以看到,Go 语言不但是开发者们非常喜爱的编程语言之一(“最喜爱”排行榜第五名),而且从实际应用的角度看,大家也是非常需要它的(“最需要”排行榜第三名)。作者认为,正因为 Go 语言有着崇尚简约和实用主义的编程哲学,广大软件工程师才会如此地爱用它。
更重要的是,Go 软件工程师的薪资待遇也是相当不错的。
图 6 - Stack Overflow Servey 2020 - The Highest Salaries
你可能会奇怪,为什么 Perl 程序员的薪资排在了第一位?这可能是因为物以稀为贵,Perl 程序员在当代已经非常少见了。而在当今很热门的通用型编程语言中,从薪资角度来看,Scala 语言、Go 语言和 Rust 语言都有着相当大的优势。
当然了,这是在全球范围内的情况,并且参与这份调查的中国开发者并不多。很可惜,作者没能找出一份公认且权威的国内开发者调查报告。
不过,从作者的亲身经历来看,Go 语言在国内恐怕并不亚于国际上的热度,甚至还要更火热一些。
作者这两年一直在断断续续地帮助一些互联网企业招聘 Go 软件工程师。除了作为老一代霸主的 BAT(百度、阿里巴巴、腾讯)以及作为新一代翘楚的 TMD(今日头条、美团、滴滴)之外,还有很多知名的互联网公司都在招聘掌握 Go 语言的开发工程师和系统运维人员。像 PingCAP、七牛、哔哩哔哩、探探、Grab 这些公司,在很早以前就混迹于 Go 语言圈子了。而在最近几年才进入 Go 语言圈子的知名公司还有华为、小米、映客、云智联、轻松筹、贝壳网、美菜网、游族网络等等。就连刚开始大红大紫的工业互联网领域,也有不少公司选择 Go 语言作为其主力开发语言之一。比如,积梦智能、必可测等。
这么多的优秀企业,以及活跃在技术社区中的大佬和新秀共同营造出了 Go 语言工程师的供需网络。作者认为,在国内的服务端编程市场,除了 Java 和 PHP,就当属 Go 语言了。
2020 年回顾
在了解了 Go 语言的发展趋势之后,我们再一起来看看它在 2020 年都有哪些重要的更新。
模块:终于稳定
自 2020 年 2 月份发布的 1.14 版本起,Go 语言官方就开始正式地推广 go modules 了。这说明它已经完全可以在生成环境中使用了。
如果你是老牌的 Go 工程师的话,那么肯定使用过像 glide、dep、govendor 这类第三方的依赖管理工具。这些工具都非常的优秀,并且在很大程度上解决了我们在项目开发过程中遇到的痛点。
不过,现在是时候迁移到 go modules 了。Go modules 综合了这些第三方工具的优点,并经历了数年的设计和磨合,最终成为了 Go 程序依赖管理的官方工具。
即使现存的项目已经使用了前面提及的某一个依赖管理工具,那么也无需担心。我们只需要在 Go 项目的根目录中运行命令“go mod init <项目主模块的导入路径>”就可以实现自动地迁移。go mod 命令会读取那些已经存在的依赖配置文件,然后在其创建的 go.mod 文件中添加相应的内容。不过在这之后,我们最好再次使用 go build 命令构建一下项目并运行相应的单元测试,以确保一切正常。
还记得系统环境变量 GOPATH 吗?现在的 go 命令会自动地把项目所需的依赖包下载到它指向的第一个工作区目录中的 pkg/mod 子目录里。这里有一点需要注意,如果我们的项目中存在处于顶层的 vendor 目录,那么 go 命令将会优先在该目录中查找对应的依赖包。
如果我们使用的是 Go 语言的 1.15 版本,那么也可以通过设置系统环境变量 GOMODCACHE 来自定义上述存储依赖包的目录。这实际上是为以后彻底废弃 GOPATH 埋下的一个伏笔。
另外,执行一下 go mod tidy 命令也是一个很好的主意。这个命令会对 go modules 的依赖配置文件进行整理,添加那些实际在用的依赖项,并去除那些未用的依赖项。换句话说,它会确保项目的依赖配置文件与项目源码的实际依赖相对应。
Go 语言的大多数标准命令都得到了不同程度的改进以更加适配 go modules,包括一些标记(flag)的调整和一些行为上的优化。比如,go get 命令在默认情况下不再会去更新非兼容版本的依赖库。不兼容的依赖库更新常常会让我们很恼火,但现在不会再出现这种情况了。
环境变量:跟进的调整
Go 语言可识别的系统环境变量 GO111MODULE 在 1.14 和 1.15 版本中的默认值都是 auto。这意味着,go 命令仅在当前目录或上层目录中存在 go.mod 文件的情况下才会以 go modules 的方式运行,否则它就会退回到之前以 GOPATH 为中心的运行方式。不过,预计在明年发布的 1.16 版本中,Go 语言将会把这个环境变量的默认值设置为 on。也就是说,到了那时,GOPATH 这一古老但能勾起我们满满回忆的东西终于要默默地退出了。
另外,我们现在可以在系统环境变量 GOPROXY 的值中使用管道符“|”了。在这之前,GOPROXY 的值中只能出现分隔符“,”。如果一个代理 URL 跟在了分隔符后面,那么只有在前一个代理 URL 指向的服务器返回 404 或 401 错误时,go 命令才会尝试使用当前的代理 URL。现在,如果一个代理 URL 跟在了管道符后面,那么只要在访问前一个服务器时发生了(任何的)错误,go 命令就会马上使用当前的代理 URL。换句话说,新的管道符让我们多了一种容错的选择,即范围更广的容错。合理使用它,可以让我们更快地从可用的代理那里下载到所需的代码包。
顺便说一下,我们现在有了一个新的系统环境变量 GOINSECURE。这个环境变量的存在单纯是为了让我们能够从非 HTTPS 的代码包服务器上下载依赖包(或者模块)。
有关环境变量的更多细节,我就不在这里说了。大家如果想了解的话,可以去参看 Go 语言的相关文档。
语言语法:可重叠的接口方法
我们都知道,Go 语言这些年在语法方面一向很稳定,少有改动,更没有不兼容的变化出现。在 2020 年,Go 语言只做了一项语法改进。这是关于接口声明的,并且完全保证了向后兼容性。我们下面来看一组代码示例。假设,我们有如下两个接口声明:
// MyReader 代表可读的自定义接口。
type MyReader interface {
io.ReadCloser
}
// MyWriter 代表可写的自定义接口。
type MyWriter interface {
io.WriteCloser
}
在 Go 1.14 之前,这两个接口是无法内嵌到同一个接口声明中去的。就像这样:
// MyIO 代表可输入输出的自定义接口。
type MyIO interface {
MyReader
MyWriter
io.Closer
}
这会让 Go 语言的编译器报错。报错的原因是:在同一个接口声明中发现了重复的方法声明。更具体地说,Go 语言标准库中的 io.ReadCloser 接口和 io.WriteCloser 接口都包含了一个名为 Close 的方法,而分别内嵌了这两个接口的 MyReader 和 MyWriter 又已经嵌入到了接口 MyIO 之中。这导致 MyIO 接口里现在存在两个 Close 方法的声明。所以,MyIO 的声明是无效的,并且无法通过编译。
这看上去是合规的,但却不一定合理。因为在很多情况下,我们想做的只是把多个接口合并在一起,而不在乎方法声明是否有重叠。我们一般认为,如果有重叠的方法,那么就当作一个就好了。很可惜,之前的 Go 语言并不这么认为。更重要的是,对于像上面那样深层次的接口内嵌问题,我们排查和解决起来都会很麻烦。有时候,这还会涉及到第三方库。
值得庆幸的是,自 Go 1.14 开始,我们的这个合理诉求终于得到了满足。Go 语言的语法已经认可了上述情况。这将给我们的接口整合工作带来极大的便利。
不过请注意,Go 语言只接受在同一个接口声明中完全重叠的多个方法声明。换句话说,只有这些方法声明在名称和签名上完全一致,它们才能够合而为一。别忘了,接口中方法的签名包括了参数列表和结果列表。如果仅名称相同但签名不同,那么 Go 语言编译器照样会报错。例如:
// MyIO 代表可输入输出的自定义接口。
type MyIO interface {
MyReader
MyWriter
Close()
}
由于这里最后面的方法声明 Close() 与接口 io.ReadCloser 和 io.WriteCloser 中的方法声明 Close() error 不完全一致(请注意结果声明上的差异),所以 Go 语言仍然会报出错误“duplicate method Close”,并使得程序编译不通过。
运行时内部:性能提升
Go 语言每次的版本更新都会包含针对其运行时系统的性能提升。在 2020 年的优化中,有几点值得我们注意:
- goroutine 真正实现了异步的抢占。也就是说,现在即使是不包含任何函数调用的 for 循环也绝不会引起程序的死锁和垃圾回收的延误了。
- defer 语句的执行效率又得到了进一步的提升,额外的开销已几乎为零。所以,我们已经完全可以将 defer 语句用在对性能有严苛要求的应用场景中了。
- 运行时系统内部的内存分配器获得了改进。这使得在系统环境变量 GOMAXPROCS 有较高数值的情况下,内存的申请效率会得到大幅的提升。这间接地让运行时系统的整体性能明显提高。
除了以上这些,Go 语言运行时系统还有一些比较小的改进,比如:panic 函数已经可以正确地打印出我们传入的参数值当中的各种数值了、在将较小的整数值转换为接口值的过程中不再会引起内存分配、针对通道的非阻塞接收操作得到了进一步的优化,等等。
并发编程:一些微调
我们都知道,runtime.Goexit 函数在被调用之后会中止当前的 goroutine 的运行。然而,在嵌套的 panic/recover 处理流程中,程序对它的调用会被忽略掉。虽然这种应用场景非常少见,但终归是一个问题。幸好自 Go 1.14 开始,这个问题被彻底地解决了。
还要注意,如果 runtime.Goexit 函数被主 goroutine 中的代码调用了,那么它并不会终止整个 Go 程序的运行,而只会中止主 goroutine 而已。在这种情况下,其他的 goroutine 会继续运行。如果,在这之后,这些其他的 goroutine 都运行完毕了,那么程序就会崩溃。所以说,我们千万不要在主 goroutine 中调用 runtime.Goexit 函数。
在同步工具方面,现在的 Go 运行时系统会更加关注那些竞争非常激烈的互斥锁。一旦这样的锁被解锁,系统会立即安排 CPU 去运行正在等待该锁的下一个 goroutine(也就是说,它会跳过调度的过程)。这显然可以大大提高此类场景下的互斥锁性能。
并发安全字典 sync.Map 有了一个新方法 LoadAndDelete。从方法名上我们也可以看得出来,这个方法能够在获取某个元素值的同时把它从字典中删除掉。更重要的是,Go 语言会保证这个“获取 + 删除”的操作的原子性。另外,该方法返回的第二个结果值会告知我们,字典先前是否包含与我们传入的健值(key)对应的元素值(element)。如果不包含,那么它返回的第一个结果值就会是 nil。
再来说 context.Context。虽然它并不在 sync 代码包中,但是我们常常用它来做上下文的同步。因此,它也算是一个很重要的同步工具。关于它的调整很简单也很明确,那就是:不再允许使用 nil 作为父级的上下文来创建衍生的 Context 实例了。这一要求显然是合理的,因为所谓的衍生 Context 就应该有实实在在的父级。然而,在 Go 1.15 之前,并没有专门的防卫语句来对此进行前置检查。此项改进涉及到了 context 包中的几个重要函数,如 WithCancel、WithDeadline、WithTimeout 和 WithValue。现在,如果我们传给这些方法的第一个参数值是 nil,那么它们都将会立即抛出包含了对应错误信息的 panic。
标准库:新的散列算法包
标准库中多了一个新的代码包:hash/maphash。这是一个通用的散列算法包,可以将任意的字节序列或者字符串散列成 64 位的整数。从名字上我们也可以看出,它能够帮助我们实现那些基于散列表的数据结构。该包中的 Hash 类型的基本行为如下:
- 此 Hash 类型是开箱即用的。也就是说,我们在实例化它的时候无需额外的初始化工作,仅创建一个它的零值即可使用。
- 不同的 Hash 实例在默认的情况下会有不同的默认种子。因此,这些默认实例为同一个对象计算出的散列值将会不同。
- Hash 实例允许手动设置种子(必须由 MakeSeed 函数产生)。在单一进程中,只要种子相同,Hash 实例为同一个对象计算出的散列值就会相同。不论进行计算的 Hash 实例是一个还是多个,都会如此。
- 被计算的对象的表现形式可以是字节序列,也可以是字符串。只要内容一致,不论它们是以怎样的方式写入 Hash 实例的,计算出的散列值都会相同(大前提是进程和种子都相同)。
- Hash 实例可以被重置。此操作会清空已写入当前 Hash 实例的内容,但并不会改变当前的种子。
总之,hash/maphash.Hash 类型代表着一种基于种子的、可重用、可重置、稳定、通用的散列算法。这种算法的优势是可以有效地避免散列冲突,但它并不是加密安全的。如果你的关注点是加密安全,那么可以考虑使用标准库 crypto 包中的算法包,如 md5、sha256、sha512 等。
单元测试:资源清理
今年,Go 语言还在单元测试方面做了一些改进。
作者认为最实用的一项改进当属 Cleanup 方法的增加。更具体地说,现在 testing.T 类型和 testing.B 类型都有了这个方法。该方法接受一个函数作为其参数值。我们的测试代码可以多次调用 Cleanup 方法,以传入多个函数。这些函数都会被记录在测试程序的内部。当测试即将结束的时候,这些函数就会被一一调用,且越晚传入的函数会越先被调用。
虽然 Cleanup 方法的文档中并没有规定这些被传入的函数应该做什么。但正如其名,它们最应该做的是清理在测试运行的过程中用到的各种资源。
另外,testing.T 类型和 testing.B 类型还各自多了一个名为 TempDir 的方法。这个方法会返回一个临时目录的路径。这样的临时目录是专门为当前的测试实例创建的。并且,这些目录会在测试即将结束的时候被自动地删除掉。因此,我们可以在测试的过程中根据实际需要在这样的目录下创建一些临时的文件,以帮助测试更好的进行。
时区信息:独立性增强
Go 1.15 包含了一个新的代码包 time/tzdata。该包允许将时区数据库嵌入到 Go 程序当中。当我们在程序中添加导入语句(即 import _ "time/tzdata")之后,即使本地系统里不存在时区数据库,当前程序也完全可以正确地查询到时区信息。另外,我们还可以通过在构建程序时追加 -tags timetzdata 来嵌入时区数据库。当然了,天下没有免费的午餐。这两种方法都会使 Go 程序的大小增加大约 800 KB(预计在 Go 1.16 中会降低到 350 KB)。
明年的展望
目前,可以明确的是,Go 语言明年肯定不会有泛型和新的错误处理机制。如果不出意外的话,范型应该会在 2022 年发布的 1.18 版本中出现。而有些可惜的是,错误处理方面的变革可能要等到 Go 2 发布的时候才能够真正的实现。
关于 Go 语言的泛型,大家可以参看 Ian Lance Taylor 和 Robert Griesemer 在 2020 年 11 月 25 日发布的最新设计草案。而对于新的错误处理机制,大家可以去看 Go 2 设计草案中“Error handling”部分。作者就不在这里多说了。
我们下面来看看,将要在 2021 年 2 月发布的 Go 1.16 预计会包括哪些重要的更新。由于 1.16 版本目前还在开发当中,因此以下所讲的内容不一定就是最终的实现。
端口:支持新的组合
大家都知道,苹果公司已经发布了他们自己研发的 CPU,并且已经应用在了自家的入门级电脑当中。然而,很多在 macOS 操作系统上运行的软件却还没有准备好。Go 语言也在其列。
不过,Go 语言打算从 1.16 版本开始支持这种新的“计算架构 + 操作系统”组合,代号为“darwin/arm64”。请注意,在当下的 Go 语言之中其实已经存在了“darwin/arm64”,只不过现在这个代号实际上对应的是“ARM 处理器 +iOS 操作系统”。为了避免混淆,之前的“darwin/arm64”将更名为“ios/arm64”,而 “darwin/arm64”之后将对应于“ARM 处理器 +macOS 操作系统”。
模块与工具:GOPATH 即将下岗
我们在前文说过,Go 语言预计在 1.16 版本将系统环境变量 GO111MODULE 的默认值改为 on。这就意味着,GOPATH 以及以它为中心的程序存储和构建方式终于要向我们挥手告别了。不过,若我们还想让 go 命令以之前的方式运行,那么还可以把该环境变量的值改成 auto。按照惯例,本文作者估计这个环境变量还会再在 Go 语言中留存 1 或 2 个版本。
与之相应的,Go 语言自带的各种标准工具会彻底地站在 go modules 一方。
命令 go install 将会支持安装指定版本号的代码包。更具体地说,我们在输入命令的时候可以这样:“go install golibhub.com/mylib@v1.2.1 ”。在这种情况下,go 命令将会忽略掉相关 go.mod 文件中的 mylib 包条目,即使那里配置的版本号与命令参数中的不同也会如此。顺便说一下,Go 语言官方将会在之后的某个版本中改变 go get 命令的行为,使它只负责下载代码包,而不再自动地进行代码包的构建和安装。也就是说,这个“构建 + 安装”的动作将只由 go install 命令负责。
另外,go build 命令和 go test 命令将不会再对 go.mod 文件进行任何的修改。对它们来说,go.mod 文件将会是只读的。一个相应的行为是,如果这两个命令在执行的过程中发现需要对依赖配置文件进行修改(或者说有必要调整依赖包的配置信息),那么它们将会立即报错。这与之前在输入命令时追加标记 -mod=readonly 的行为是一致的。这时,我们可以使用命令 go mod tidy 或 go get 来做相应的调整。
运行时 API:监测的增强
在 Go 1.16 中,将会出现一个新的用于运行时度量的代码包 runtime/metrics。该代码包旨在引入一种稳定的度量接口,用于从 Go 运行时系统中读取相应的指标数据。它在功能上会取代现有的诸如 runtime.ReadMemStats、debug.GCStats 等 API,并且会更加的通用和高效。
另外,系统环境变量 GODEBUG 将可以接受一个新的选项“inittrace”。当该环境变量的值中包含“inittrace=1”的时候,Go 运行时系统会输出有关于 init 函数的监测信息。其中会包含对应函数的执行时间和内存分配情况。这将非常有利于我们观察各个代码包在初始化方面的性能。
标准库:新的嵌入包
Go 1.16 的标准库中将会出现一个新的代码包 embed。这个包的主要作用是让 go 命令在编译程序的时候向其嵌入指定的外部文件。至于嵌入什么文件,需要我们通过注释指令 //go:embed 来指定,如://go:embed hello.txt 。请注意,在包含了这个注释指令的源码文件中必须要有针对 embed 包的导入语句,如:import "embed" 。
同时,这样的注释指令必须紧挨在单一变量声明语句的上方,且该变量的类型必须是 string、[]byte 或 embed.FS。这样的话,Go 语言就会自动地把我们指定的文件的内容转化为相应的值,并赋给这个变量。
在 //go:embed 的右边,我们可以用空格来分割多个文件路径,或者通过添加多个这样的注释指令来分别指定多个文件。文件的路径可以是相对的,也可以是绝对的。
另外,这里的文件路径还可以包含通配符,如://go:embed image/template/。具体有哪些通配符可用,大家可以去参看 path.Match 函数的文档。
一定要注意,如果需要嵌入多个文件,那么我们就必须把变量的类型声明为 embed.FS。这主要是因为,只有这个类型才能把多个嵌入文件的内容区分开来。通过该类型的方法(Open、ReadDir 和 ReadFile),我们还可以分别拿到代表了某个文件或目录的实体,或者读取其中任何文件的内容。
究其原因,结构体类型 embed.FS 的实例可以代表一个树形的文件系统。也就是说,它可以用来表示一个拥有多个层级的文件目录。另外,embed.FS 类型是 io/fs.FS 接口的一个实现,因此它的实例可以被应用在很多理解统一文件系统的 API 之上。这些 API 散落在 net/http、text/template、html/template 等代码包之中。
请想象一下,如果我们在开发一个带有 Web 页面和静态资源的软件系统,那么这将会给我们带来多么大的便利。让单个的可执行文件包含所有的程序和资源将变为可能。
标准库:新的文件系统包
代码包 io/fs 代表了一种全新的文件系统模型。它可以对应于任意(已支持的)操作系统中的文件系统,但并不局限于此。该包中的核心就是我们已经在前面提及的 FS 接口,以及还未讲到的 File 接口。
简单来说,FS 接口代表了一个文件系统抽象的最小实现要求。其中只有一个方法声明:Open(name string) (File, error) 。而 File 接口则代表了可以在单个文件上进行的一系列操作。请看下面这幅图。
图 7 - io/fs 包中的接口
到目前为止,这个代码包中的接口共有 10 个。其他的大部分接口都会内嵌 FS 接口或 File 接口。也就是说,它们都属于核心接口的扩展。虽然从声明上看 DirEntry 接口和 FileInfo 接口是独立的,但它们都被多个其他的接口所引用。
在仔细阅读上图或者该包的源码之后,你一定会发现,这个模型所代表的文件系统是只读的。也就是说,其中并没有表示“写操作”的方法。
正因为这个模型可以用来表示任何树形结构的资源系统,所以作为统一的模型,它只提供了最基础的抽象。要知道,有的资源系统就是只能读、不能写的。比如,我们前面说过的嵌入 Go 程序的外部文件和目录就必须是只读的。也就是说,它们在嵌入程序之后就不应该再被改变了。
为了适配这个模型,Go 语言标准库中的不少代码包都做了相应的调整,比如:os 包、net/http 包、archive/zip 包,以及 html/template 包和 text/template 包。
这里有一些更具体的例子:
- embed.FS 类型用于表示嵌入的文件或目录。它实现了 io/fs 包中的 FS 接口、ReadDirFS 接口和 ReadFileFS 接口;
- 新的 os.DirFS 函数可以提供由当前的操作系统支持的 io/fs.FS 接口实现;
- 新的 http.FS 函数和 http.FileServer 函数可以把 io/fs.FS 接口的实例包装成 http.Handler;
- 新的 testing/fstest 包专门用于测试相应的文件系统模型,其中还包含了一个基于内存的文件系统抽象 MapFS。
总之,代码包 io/fs 代表了一个全新的、统一的文件系统(或者说资源系统)的模型。这个模型将会作为很多具体应用的底层框架。另外,io/fs 包在以后可能会得到进一步的拓展,说不定还会发展出一些描述“写操作”的扩展接口。作者也希望如此。
总 结
好了,我们现在来稍微总结一下。在 2020 年,Go 语言同样做出了很多改变。这包括已经完全稳定的 go modules、环境变量和标准工具的跟进和增强、语法上的一项重要调整——可重叠的接口方法、运行时系统的性能提升、异步编程和同步工具方面的进一步优化,以及新的散列算法包、新的单元测试辅助方法和独立的时区代码包。这些更新表面上看起来可能并不算大,但 Go 语言内部其实已经做了很多的改变。有的改变是完全的,而有的改变是在为以后的目标做铺垫。
我们对 Go 语言有着很多的期望。但是,大饼还是要一口一口的吃。在 2021 年,Go 语言中的 GOPATH 将会正式宣布下岗。同时,Go 语言也会把一些重要的东西统一起来,比如用于运行时度量的代码包 runtime/metrics,以及代表了新的文件系统模型的代码包 io/fs。随着 io/fs 包而来的,还有便于我们将程序和资源整合成一个单独文件的代码包 embed 和注释指令 //go:embed 。另外,作者也非常期待 Go 语言对“ARM+macOS”组合的支持。
目前可以预计,Go 语言的泛型支持将会比新的错误处理机制更早到来。不过,Go 语言官方在保证向后兼容性的情况下已经对现有的错误处理 API 进行了尽可能的改进。更何况,我们还有几个不错的错误处理包可用,如官方的扩展包 golang.org/x/xerrors,以及已经过时间验证的第三方包 github.com/pkg/errors 和 gopkg.in/errgo.v2。所以当前看来,这个问题早已不那么尖锐了。
作者对 Go 语言的发展仍然是非常乐观的,尤其是在“云原生”大行其道的当下。在数据科学方面,七牛云的 CEO 许式伟正在带头创造对标“Python 语言 +Numpy+Pandas”和(MIT 出品的)Julia 语言的新玩意儿——基于 Go 语言的编程语言 Go+。作者对此还是很看好的。不过,据说(Apple 出品的)Swift 语言也将在这一领域继续发力,可能会出现“Swift Number”之类的东西,同样对标基于 Python 语言的数据科学包。作者相信,到了 2021 年下半年或者 2022 年,这里很可能会出现四足鼎立的态势。总之,有竞争才会有突破,基于 Go 语言的生态环境依然不可限量。