原创分享 Go 1.16 中值得关注的几个变化

bigwhite-github · 2021年02月25日 · 718 次阅读
本帖已被设为精华帖!

img{512x368}

辛丑牛年初七开工大吉的日子 (2021.2.18),Go 核心开发团队为中国 Gopher 们献上了大礼 - Go 1.16 版本正式发布了!国内 Gopher 可以在Go 中国官网上下载到 Go 1.16 在各个平台的安装包:

img{512x368}

2020 年双 12,Go 1.16 进入 freeze 状态,即不再接受新 feature,仅 fix bug、编写文档和接受安全更新等,那时我曾写过一篇名为《Go 1.16 新功能特性不完全前瞻》的文章。当时 Go 1.16 的发布说明尚处于早期草稿阶段,要了解 Go 1.16 功能特性都有哪些变化,只能结合当时的 release note 以及从Go 1.16 里程碑中的 issue 列表中挖掘。

如今 Go 1.16 版本正式发布了,和当时相比,Go 1.16 又有哪些变化呢?在这篇文章中,我们就来一起详细分析一下 Go 1.16 中那些值得关注的重要变化!

一. 语言规范

如果你是 Go 语言新手,想必你一定很期待一个大版本的发布会带来许多让人激动人心的语言特性。但是 Go 语言在这方面肯定会让你 “失望” 的。伴随着 Go 1.0 版本一起发布的Go1 兼容性承诺给 Go 语言的规范加了一个 “框框”,从 Go 1.0 到Go 1.15版本,Go 语言对语言规范的变更屈指可数,因此资深 Gopher 在阅读 Go 版本的 release notes 时总是很自然的略过这一章节,因为这一章节通常都是如下面这样的描述:

img{512x368}

这就是Go 的设计哲学:简单!绝不轻易向语言中添加新语法元素增加语言的复杂性。除非是那些社区呼声很高并且是 Go 核心团队认可的。我们也可以将 Go 从 1.0 到 Go 1.16 这段时间称为 “Go 憋大招” 的阶段,因为就在 Go 团队发布 1.16 版本之前不久,Go 泛型提案正式被 Go 核心团队接受 (Accepted):

img{512x368}

这意味着什么呢?这意味着在 2022 年 2 月份 (Go 1.18),Gopher 们将迎来 Go 有史以来最大一次语言语法变更并且这种变更依然是符合 Go1 兼容性承诺的,这将避免 Go 社区出现 Python3 给 Python 社区带去的那种 “割裂”。不过就像《“能力越大,责任越大” - Go 语言之父详解将于 Go 1.18 发布的 Go 泛型》一文中 Go 语言之父Robert Griesemer所说的那样:泛型引入了抽象,但滥用抽象而没有解决实际问题将带来不必要的复杂性,请三思而后行! 离泛型的落地还有一年时间,就让我们耐心等待吧!

二. Go 对各平台/OS 支持的变更

Go 语言具有良好的可移植性,对各主流平台和 OS 的支持十分全面和及时,Go 官博曾发布过一篇文章,简要列出了自 Go1 以来对各主流平台和 OS 的支持情况:

  • Go1(2012 年 3 月)支持原始系统 (译注:上面提到的两种操作系统和三种架构) 以及 64 位和 32 位 x86 上的 FreeBSD、NetBSD 和 OpenBSD,以及 32 位 x86 上的 Plan9。
  • Go 1.3(2014 年 6 月)增加了对 64 位 x86 上 Solaris 的支持。
  • Go 1.4(2014 年 12 月)增加了对 32 位 ARM 上 Android 和 64 位 x86 上 Plan9 的支持。
  • Go 1.5(2015 年 8 月)增加了对 64 位 ARM 和 64 位 PowerPC 上的 Linux 以及 32 位和 64 位 ARM 上的 iOS 的支持。
  • Go 1.6(2016 年 2 月)增加了对 64 位 MIPS 上的 Linux,以及 32 位 x86 上的 Android 的支持。它还增加了 32 位 ARM 上的 Linux 官方二进制下载,主要用于 RaspberryPi 系统。
  • Go 1.7(2016 年 8 月)增加了对的 z 系统(S390x)上 Linux 和 32 位 x86 上 Plan9 的支持。
  • Go 1.8(2017 年 2 月)增加了对 32 位 MIPS 上 Linux 的支持,并且它增加了 64 位 PowerPC 和 z 系统上 Linux 的官方二进制下载。
  • Go 1.9(2017 年 8 月)增加了对 64 位 ARM 上 Linux 的官方二进制下载。
  • Go 1.12(2018 年 2 月)增加了对 32 位 ARM 上 Windows10 IoT Core 的支持,如 RaspberryPi3。它还增加了对 64 位 PowerPC 上 AIX 的支持。
  • Go 1.14(2019 年 2 月)增加了对 64 位 RISC-V 上 Linux 的支持。

Go 1.7 版本中新增的go tool dist list命令还可以帮助我们快速了解各个版本究竟支持哪些平台以及 OS 的组合。下面是 Go 1.16 版本该命令的输出:

$go tool dist list
aix/ppc64
android/386
android/amd64
android/arm
android/arm64
darwin/amd64
darwin/arm64
dragonfly/amd64
freebsd/386
freebsd/amd64
freebsd/arm
freebsd/arm64
illumos/amd64
ios/amd64
ios/arm64
js/wasm
linux/386
linux/amd64
linux/arm
linux/arm64
linux/mips
linux/mips64
linux/mips64le
linux/mipsle
linux/ppc64
linux/ppc64le
linux/riscv64
linux/s390x
netbsd/386
netbsd/amd64
netbsd/arm
netbsd/arm64
openbsd/386
openbsd/amd64
openbsd/arm
openbsd/arm64
openbsd/mips64
plan9/386
plan9/amd64
plan9/arm
solaris/amd64
windows/386
windows/amd64
windows/arm

通常我不太会过多关注每次 Go 版本发布时关于可移植性方面的内容,这次将可移植性单独作为章节主要是因为 Go 1.16 发布之前的Apple M1 芯片事件

img{512x368}

苹果公司再次放弃 Intel x86 芯片而改用自造的基于 Arm64 的 M1 芯片引发业界激烈争论。但现实是搭载 Arm64 M1 芯片的苹果笔记本已经大量上市,对于编程语言开发团队来说,能做的只有尽快支持这一平台。因此,Go 团队给出了在 Go 1.16 版本中增加对 Mac M1 的原生支持。

在 Go 1.16 版本之前,Go 也支持 darwin/arm64 的组合,但那更多是为了构建在 iOS 上运行的 Go 应用 (利用gomobile)。

Go 1.16 做了进一步的细分:将 darwin/arm64 组合改为 apple M1 专用;而构建在 iOS 上运行的 Go 应用则使用 ios/arm64。同时,Go 1.16 还增加了 ios/amd64 组合用于支持在 MacOS(amd64) 上运行的iOS 模拟器中运行 Go 应用

另外还值得一提的是在 OpenBSD 上,Go 应用的系统调用需要通过 libc 发起,而不能再绕过 libc 而直接使用汇编指令了,这是出于对未来 OpenBSD 的一些兼容性要求考虑才做出的决定。

三. Go module-aware 模式成为默认!

在泛型落地前,Go module 依旧是这些年 Go 语言改进的重点 (虽不是语言规范特性)。在 Go 1.16 版本中,Go module-aware 模式成为了默认模式 (另一种则是传统的 gopath 模式)。module-aware 模式成为默认意味着什么呢?意味着 GO111MODULE 的值默认为 on 了。

自从 Go 1.11 加入 go module,不同 go 版本在 GO111MODULE 为不同值的情况下开启的构建模式几经变化,上一次 go module-aware 模式的行为有较大变更还是在Go 1.13 版本中。这里将 Go 1.13 版本之前、Go 1.13 版本以及 Go 1.16 版本在 GO111MODULE 为不同值的情况下的行为做一下对比,这样我们可以更好的理解 go 1.16 中 module-aware 模式下的行为特性,下面我们就来做一下比对:

GO111MODULE < Go 1.13 Go 1.13 Go 1.16
on 任何路径下都开启 module-aware 模式 任何路径下都开启 module-aware 模式 【默认值】:任何路径下都开启 module-aware 模式
auto 【默认值】:使用 GOPATH mode 还是 module-aware mode,取决于要构建的源码目录所在位置以及是否包含 go.mod 文件。如果要构建的源码目录不在以 GOPATH/src 为根的目录体系下,且包含 go.mod 文件 (两个条件缺一不可),那么使用 module-aware mode;否则使用传统的 GOPATH mode。 【默认值】:只要当前目录或父目录下有 go.mod 文件时,就开启 module-aware 模式,无论源码目录是否在 GOPATH 外面 只有当前目录或父目录下有 go.mod 文件时,就开启 module-aware 模式,无论源码目录是否在 GOPATH 外面
off gopath 模式 gopath 模式 gopath 模式

我们看到在 Go 1.16 模式下,依然可以回归到 gopath 模式。但 Go 核心团队已经决定拒绝“继续保留 GOPATH mode” 的提案,并计划在 Go 1.17 版本中彻底取消 gopath mode,仅保留 go module-aware mode:

img{512x368}

虽然目前仍有项目没有转换到 go module 下,但根据调查,大多数项目已经选择拥抱 go module 并完成了转换工作,因此笔者认为即便 Go 1.17 真的取消了 GOPATH mode,对整个 Go 社区的影响也不会太大了。

Go 1.16 中,go module 机制还有其他几个变化,这里逐一来看一下:

1. go build/run 命令不再自动更新 go.mod 和 go.sum 了

为了能更清晰看出 Go 1.16 与之前版本的差异,我们准备了一个小程序:

// github.com/bigwhite/experiments/blob/master/go1.16-examples/go-modules/helloworld/go.mod
module github.com/bigwhite/helloworld

go 1.16

// github.com/bigwhite/experiments/blob/master/go1.16-examples/go-modules/helloworld/helloworld.go 
package main

import "github.com/sirupsen/logrus"

func main() {
    logrus.Println("Hello, World")
}

我们使用go 1.15 版本构建一下该程序:

$go build
go: finding module for package github.com/sirupsen/logrus
go: downloading github.com/sirupsen/logrus v1.8.0
go: found github.com/sirupsen/logrus in github.com/sirupsen/logrus v1.8.0

$cat go.mod
module github.com/bigwhite/helloworld

go 1.16

require github.com/sirupsen/logrus v1.8.0

$cat go.sum
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/magefile/mage v1.10.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sirupsen/logrus v1.8.0 h1:nfhvjKcUMhBMVqbKHJlk5RPrrfYr/NMo3692g0dwfWU=
github.com/sirupsen/logrus v1.8.0/go.mod h1:4GuYW9TZmE769R5STWrRakJc4UqQ3+QQ95fyz7ENv1A=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=

在 Go 1.15 版本中,go build 会自动分析源码中的依赖,如果 go.mod 中没有对该依赖的 require,则会自动添加 require,同时会将 go.sum 中将相关包 (特定版本) 的校验信息写入。

我们将上述 helloworld 恢复到初始状态,再用 go 1.16 来 build 一次:

$go build
helloworld.go:3:8: no required module provides package github.com/sirupsen/logrus; to add it:
    go get github.com/sirupsen/logrus

我们看到 go build 没有成功,而是给出错误:go.mod 中没有对 logrus 的 require,并给出添加对 logrus 的 require 的方法 (go get github.com/sirupsen/logrus)。

我们就按照 go build 给出的提示执行 go get:

$go get github.com/sirupsen/logrus
go: downloading github.com/magefile/mage v1.10.0
go get: added github.com/sirupsen/logrus v1.8.0


$cat go.mod
module github.com/bigwhite/helloworld

go 1.16

require github.com/sirupsen/logrus v1.8.0 // indirect


$cat go.sum
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/magefile/mage v1.10.0 h1:3HiXzCUY12kh9bIuyXShaVe529fJfyqoVM42o/uom2g=
github.com/magefile/mage v1.10.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sirupsen/logrus v1.8.0 h1:nfhvjKcUMhBMVqbKHJlk5RPrrfYr/NMo3692g0dwfWU=
github.com/sirupsen/logrus v1.8.0/go.mod h1:4GuYW9TZmE769R5STWrRakJc4UqQ3+QQ95fyz7ENv1A=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=

$go build 
//ok

我们看到 go build 并不会向 go 1.15 及之前版本那样做出有 “副作用” 的动作:自动修改 go.mod 和 go.sum,而是提示开发人员显式通过 go get 来添加缺少的包/module,即便是依赖包 major 版本升级亦是如此。

从自动更新 go.mod,到通过提供-mod=readonly 选项来避免自动更新 go.mod,再到 Go 1.16 的禁止自动更新 go.mod,笔者认为这个变化是 Go 不喜 “隐式转型” 的一种延续,即尽量不支持任何可能让开发者产生疑惑或 surprise 的隐式行为(就像隐式转型),取而代之的是要用一种显式的方式去完成 (就像必须显式转型那样)。

我们也看到在 go 1.16 中,添加或更新 go.mod 中的依赖,只有显式使用 go get。go mod tidy 依旧会执行对 go.mod 的清理,即也可以修改 go.mod。

2. 推荐使用 go install 安装 Go 可执行文件

在 gopath mode 下,go install 基本 “隐身” 了,它能做的事情基本都被 go get“越俎代庖” 了。在 go module 时代初期,go install 更是没有了地位。但 Go 团队现在想逐步恢复 go install 的角色:安装 Go 可执行文件!在 Go 1.16 中,当 go install 后面的包携带特定版本号时,go install 将忽略当前 go.mod 中的依赖信息而直接编译安装可执行文件:

// go install回将gopls v0.6.5安装到GOBIN下
$go install golang.org/x/tools/gopls@v0.6.5

并且后续,Go 团队会让 go get 将专注于分析依赖,并获取 go 包/module,更新 go.mod/go.sum,而不再具有安装可执行 Go 程序的行为能力,这样 go get 和 go install 就会各司其职,Gopher 们也不会再被两者的重叠行为所迷惑了。现在如果不想 go get 编译安装,可使用 go get -d。

3. 作废 module 的特定版本

《如何作废一个已发布的 Go module 版本,我来告诉你!》一文中,我曾详细探讨了 Go 引入 module 后如何作废一个已发布的 go module 版本。当时已经知晓 Go 1.16 会在 go.mod 中增加retract 指示符,因此也给出了在 Go 1.16 下 retract 一个 module 版本的原理和例子 (基于当时的 go tip)。

Go 1.16 正式版在工具的输出提示方面做了进一步的优化,让开发人员体验更为友好。我们还是以一个简单的例子来看看在 Go 1.16 中作废一个 module 版本的过程吧。

在我的 bitbucket 账户下有一个名为 m2 的 Go module(https://bitbucket.org/bigwhite/m2/v1.0.0:),当前它的版本为

// bitbucket.org/bigwhite/m2
$cat go.mod
module bitbucket.org/bigwhite/m2

go 1.15

$cat m2.go
package m2

import "fmt"

func M2() {
    fmt.Println("This is m2.M2 - v1.0.0")
}

我们在本地建立一个 m2 的消费者:

// github.com/bigwhite/experiments/blob/master/go1.16-examples/go-modules/retract

$cat go.mod
module github.com/bigwhite/retractdemo

go 1.16

$cat main.go
package main

import "bitbucket.org/bigwhite/m2"

func main() {
    m2.M2()
}

运行这个消费者:

$go run main.go
main.go:3:8: no required module provides package bitbucket.org/bigwhite/m2; to add it:
    go get bitbucket.org/bigwhite/m2

由于上面提到的原因,go run 不会隐式修改 go.mod,因此我们需要手工 go get m2:

$go get bitbucket.org/bigwhite/m2
go: downloading bitbucket.org/bigwhite/m2 v1.0.0
go get: added bitbucket.org/bigwhite/m2 v1.0.0

再来运行消费者,我们将看到以下运行成功的结果:

$go run main.go
This is m2.M2 - v1.0.0

现在 m2 的作者对 m2 打了小补丁,版本升级到了 v1.0.1。这时消费者通过 go list 命令可以看到 m2 的最新版本 (前提:go proxy server 上已经 cache 了最新的 v1.0.1):

$go list -m -u all
github.com/bigwhite/retractdemo
bitbucket.org/bigwhite/m2 v1.0.0 [v1.0.1]

消费者可以通过 go get 将对 m2 的依赖升级到最新的 v1.0.1:

$go get bitbucket.org/bigwhite/m2@v1.0.1

go get: upgraded bitbucket.org/bigwhite/m2 v1.0.0 => v1.0.1
$go run main.go
This is m2.M2 - v1.0.1

m2 作者收到 issue,有人指出 v1.0.1 版本有安全漏洞,m2 作者确认了该漏洞,但此时 v1.0.1 版已经发布并被缓存到各大 go proxy server 上,已经无法撤回。m2 作者便想到了 Go 1.16 中引入的 retract 指示符,于是它在 m2 的 go.mod 用 retract 指示符做了如下更新:

$cat go.mod
module bitbucket.org/bigwhite/m2

// 存在安全漏洞
retract v1.0.1

go 1.15

并将此次更新作为 v1.0.2 发布了出去!

之后,当消费者使用 go list 查看 m2 是否有最新更新时,便会看到 retract 提示:(前提:go proxy server 上已经 cache 了最新的 v1.0.2)

$go list -m -u all                      
github.com/bigwhite/retractdemo
bitbucket.org/bigwhite/m2 v1.0.1 (retracted) [v1.0.2]

执行 go get 会收到带有更详尽信息的 retract 提示和问题解决建议:

$go get .
go: warning: bitbucket.org/bigwhite/m2@v1.0.1: retracted by module author: 存在安全漏洞
go: to switch to the latest unretracted version, run:
    go get bitbucket.org/bigwhite/m2@latest                                                          

于是消费者按照提示执行 go get bitbucket.org/bigwhite/m2@latest:

$go get bitbucket.org/bigwhite/m2@latest
go get: upgraded bitbucket.org/bigwhite/m2 v1.0.1 => v1.0.2

$cat go.mod
module github.com/bigwhite/retractdemo

go 1.16

require bitbucket.org/bigwhite/m2 v1.0.2

$go run main.go
This is m2.M2 - v1.0.2

到此,retract 的使命终于完成了!

4. 引入 GOVCS 环境变量,控制 module 源码获取所使用的版本控制工具

出于安全考虑,Go 1.16 引入 GOVCS 环境变量,用于在 go 命令直接从代码托管站点获取源码时对所使用的版本控制工具进行约束,如果是从 go proxy server 获取源码,那么 GOVCS 将不起作用,因为 go 工具与 go proxy server 之间使用的是GOPROXY 协议

GOVCS 的默认值为 public:git|hg,private:all,即对所有公共 module 允许采用 git 或 hg 获取源码,而对私有 module 则不限制版本控制工具的使用。

如果要允许使用所有工具,可像下面这样设置 GOVCS:

GOVCS=*:all

如果要禁止使用任何版本控制工具去直接获取源码(不通过 go proxy),那么可以像下面这样设置 GOVCS:

GOVCS=*:off

5. 有关 go module 的文档更新

自打Go 1.14 版本宣布 go module 生产可用后,Go 核心团队在说服和帮助 Go 社区全面拥抱 go module 的方面不可谓不努力。在文档方面亦是如此,最初有关 go module 的文档仅局限于 go build 命令相关以及有关 go module 的 wiki。随着 go module 日益成熟,go.mod 格式的日益稳定,Go 团队在 1.16 版本中还将 go module 相关文档升级到 go reference 的层次,与 go language ref 等并列:

img{512x368}

我们看到有关 go module 的 ref 文档包括:

官方还编写了详细的 Go module 日常开发时的使用方法,包括:开发与发布 module、module 发布与版本管理工作流、升级 major 号等。

img{512x368}

建议每个 gopher 都要将这些文档仔细阅读一遍,以更为深入了解和使用 go module

四. 编译器与运行时

1. runtime/metrics 包

《Go 1.16 新功能特性不完全前瞻》一文中,我们提到过:Go 1.16 新增了 runtime/metrics 包,以替代 runtime.ReadMemStats 和 debug.ReadGCStats 输出 runtime 的各种度量数据,这个包更通用稳定,性能也更好。限于篇幅这里不展开,后续可能会以单独的文章讲解这个新包。

2. GODEBUG 环境变量支持跟踪包 init 函数的消耗

GODEBUG=inittrace=1 这个特性也保留在了 Go 1.16 正式版当中了。当 GODEBUG 环境变量包含 inittrace=1 时,Go 运行时将会报告各个源代码文件中的 init 函数的执行时间和内存开辟消耗情况。我们用上面的 helloworld 示例 (github.com/bigwhite/experiments/blob/master/go1.16-examples/go-modules/helloworld) 来看看该特性的效果:

$go build
$GODEBUG=inittrace=1 ./helloworld 
init internal/bytealg @0.006 ms, 0 ms clock, 0 bytes, 0 allocs
init runtime @0.037 ms, 0.031 ms clock, 0 bytes, 0 allocs
init errors @0.29 ms, 0.005 ms clock, 0 bytes, 0 allocs
init math @0.31 ms, 0 ms clock, 0 bytes, 0 allocs
init strconv @0.33 ms, 0.002 ms clock, 32 bytes, 2 allocs
init sync @0.35 ms, 0.003 ms clock, 16 bytes, 1 allocs
init unicode @0.37 ms, 0.10 ms clock, 24568 bytes, 30 allocs
init reflect @0.49 ms, 0.002 ms clock, 0 bytes, 0 allocs
init io @0.51 ms, 0.003 ms clock, 144 bytes, 9 allocs
init internal/oserror @0.53 ms, 0 ms clock, 80 bytes, 5 allocs
init syscall @0.55 ms, 0.010 ms clock, 752 bytes, 2 allocs
init time @0.58 ms, 0.010 ms clock, 384 bytes, 8 allocs
init path @0.60 ms, 0 ms clock, 16 bytes, 1 allocs
init io/fs @0.62 ms, 0.002 ms clock, 16 bytes, 1 allocs
init internal/poll @0.63 ms, 0.001 ms clock, 64 bytes, 4 allocs
init os @0.65 ms, 0.089 ms clock, 4472 bytes, 20 allocs
init fmt @0.77 ms, 0.006 ms clock, 32 bytes, 2 allocs
init bytes @0.84 ms, 0.004 ms clock, 48 bytes, 3 allocs
init context @0.87 ms, 0 ms clock, 128 bytes, 4 allocs
init encoding/binary @0.89 ms, 0.002 ms clock, 16 bytes, 1 allocs
init encoding/base64 @0.90 ms, 0.015 ms clock, 1408 bytes, 4 allocs
init encoding/json @0.93 ms, 0.002 ms clock, 32 bytes, 2 allocs
init log @0.95 ms, 0 ms clock, 80 bytes, 1 allocs
init golang.org/x/sys/unix @0.96 ms, 0.002 ms clock, 48 bytes, 1 allocs
init bufio @0.98 ms, 0 ms clock, 176 bytes, 11 allocs
init github.com/sirupsen/logrus @0.99 ms, 0.009 ms clock, 312 bytes, 5 allocs
INFO[0000] Hello, World                             

以下面这行为例:

init fmt @0.77 ms, 0.006 ms clock, 32 bytes, 2 allocs
  • 0.77ms 表示的是自从程序启动后到 fmt 包 init 执行所过去的时间 (以 ms 为单位)
  • 0.006 ms clock 表示 fmt 包 init 函数执行的时间 (以 ms 为单位)
  • 312 bytes 表示 fmt 包 init 函数在 heap 上分配的内存大小;
  • 5 allocs 表示的是 fmt 包 init 函数在 heap 上执行内存分配操作的次数。

3. Go runtime 默认使用 MADV_DONTNEED

Go 1.15 版本时,我们可以通过 GODEBUG=madvdontneed=1 让 Go runtime 使用 MADV_DONTNEED 替代 MADV_FREE 达到更积极的将不用的内存释放给 OS 的效果 (如果使用 MADV_FREE,只有 OS 内存压力很大时,才会真正回收内存),这将使得通过 top 查看到的常驻系统内存 (RSS 或 RES) 指标更实时也更真实反映当前 Go 进程对 os 内存的实际占用情况 (仅使用 linux)。

在 Go 1.16 版本中,Go runtime 将 MADV_DONTNEED 作为默认值了,我们可以用一个小例子来对比一下这种变化:

// github.com/bigwhite/experiments/blob/master/go1.16-examples/runtime/memalloc.go 
package main

import "time"

func allocMem() []byte {
    b := make([]byte, 1024*1024*1) //1M
    return b
}

func main() {
    for i := 0; i < 100000; i++ {
        _ = allocMem()
        time.Sleep(500 * time.Millisecond)
    }
}

我们在 linux 上使用 go 1.16 版本编译该程序,考虑到优化和 inline 的作用,我们在编译时关闭优化和内联:

$go build -gcflags "-l -N" memalloc.go 

接下来,我们分两次运行该程序,并使用 top 监控其 RES 指标值:

$./memalloc
$ top -p 9273
  PID USER      PR  NI    VIRT    RES    SHR S %CPU %MEM     TIME+ COMMAND
 9273 root      20   0  704264   5840    856 S  0.0  0.3   0:00.03 memalloc
 9273 root      20   0  704264   3728    856 S  0.0  0.2   0:00.05 memalloc 
 ... ...

$GODEBUG=madvdontneed=0 ./memalloc
$ top -p 9415

  PID USER      PR  NI    VIRT    RES    SHR S %CPU %MEM     TIME+ COMMAND
 9415 root      20   0  704264   5624    856 S  0.0  0.3   0:00.03 memalloc   
 9415 root      20   0  704264   5624    856 S  0.0  0.3   0:00.05 memalloc   

我们看到默认运行的 memalloc(开启 MADV_DONTNEED),RES 很积极的变化,当上一次显示 5840,下一秒内存就被归还给 OS,RES 变为 3728。而关闭 MADV_DONTNEED(GODEBUG=madvdontneed=0)的 memalloc,OS 就会很 lazy 的回收内存,RES 一直显示 5624 这个值。

4. Go 链接器的进一步进行现代化改造

新一代 Go 链接器的更新计划从 Go 1.15 版本开始,在 Go 1.15 版本链接器的性能、资源占用、最终二进制文件大小等方面都有了一定幅度的优化提升。Go 1.16 版本延续了这一势头:相比于 Go 1.15,官方宣称 (在 linux 上) 性能有 20%-25% 的提升,资源占用下降 5%-15%。更为直观的是编译出的二进制文件的 size,我实测了一下文件大小下降 10% 以上:

-rwxr-xr-x   1 tonybai  staff    22M  2 21 23:03 my-large-app-demo*
-rwxr-xr-x   1 tonybai  staff    25M  2 21 23:02 my-large-app-demo-go1.15*

并且和 Go 1.15 的链接器优化仅针对 amd64 平台和基于 ELF 格式的 OS 不同,这次的链接器优化已经扩展到所有平台和 os 组合上

五. 标准库

1. io/fs 包

Go 1.16 标准库新增 io/fs 包,并定义了一个 fs.File 接口用于表示一个只读文件树 (tree of file) 的抽象。之所以要加入 io/fs 包并新增 fs.File 接口源于对嵌入静态资源文件 (embed static asset) 的实现需求。虽说实现 embed 功能特性是直接原因,但 io/fs 的加入也不是 “临时起意”,早在很多年前的 godoc 实现时,对一个抽象的文件系统接口的需求就已经被提了出来并给出了实现:

最终这份实现以 godoc 工具的vfs 包的形式一直长期存在着。虽然它的实现有些复杂,抽象程度不够,但却对io/fs 包的设计有着重要的参考价值。同时也部分弥补了Rob Pike 老爷子当年没有将 os.File 设计为 interface 的遗憾Ian Lance Taylor 2013 年提出的增加 VFS 层的想法也一并得以实现。

io/fs 包的两个最重要的接口如下:

// $GOROOT/src/io/fs/fs.go

// An FS provides access to a hierarchical file system.
//
// The FS interface is the minimum implementation required of the file system.
// A file system may implement additional interfaces,
// such as ReadFileFS, to provide additional or optimized functionality.
type FS interface {
        // Open opens the named file.
        //
        // When Open returns an error, it should be of type *PathError
        // with the Op field set to "open", the Path field set to name,
        // and the Err field describing the problem.
        //
        // Open should reject attempts to open names that do not satisfy
        // ValidPath(name), returning a *PathError with Err set to
        // ErrInvalid or ErrNotExist.
        Open(name string) (File, error)
}

// A File provides access to a single file.
// The File interface is the minimum implementation required of the file.
// A file may implement additional interfaces, such as
// ReadDirFile, ReaderAt, or Seeker, to provide additional or optimized functionality.
type File interface {
        Stat() (FileInfo, error)
        Read([]byte) (int, error)
        Close() error
}

FS 接口代表虚拟文件系统的最小抽象,File 接口则是虚拟文件的最小抽象,我们可以基于这两个接口进行扩展以及对接现有的一些实现。io/fs 包也给出了一些扩展 FS 的 “样例”:

这两个接口的设计也是 “Go 秉持定义小接口惯例” 的延续 (更多关于这方面的内容,可以参考我的专栏文章《定义小接口是 Go 惯例》)。

io/fs 包的加入也契合了 Go 社区对 vfs 的需求,在 Go 团队决定加入 io/fs 并提交实现后,社区做出了积极的反应,在 github 上我们能看到好多为各类对象提供针对 io/fs.FS 接口实现的项目:

io/fs.FS 和 File 接口在后续 Go 演进过程中会像 io.Writer 和 io.Reader 一样成为 Gopher 们在操作类文件树时最爱的接口。

2. embed 包

《Go 1.16 新功能特性不完全前瞻》一文中我们曾重点说了 Go 1.16 将支持在 Go 二进制文件中嵌入静态文件并给出了一个在 webserver 中嵌入文本文件的例子:

// github.com/bigwhite/experiments/blob/master/go1.16-examples/stdlib/embed/webserver/hello.txt 
hello, go 1.16

// github.com/bigwhite/experiments/blob/master/go1.16-examples/stdlib/embed/webserver/main.go
package main

import (
         _  "embed"
    "net/http"
)

//go:embed hello.txt
var s string

func main() {
    http.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte(s))
    }))
    http.ListenAndServe(":8080", nil)
}

我们看到在这个例子,通过//go:embed hello.txt,我们可以轻易地将 hello.txt 的内容存储在包级变量 s 中,而 s 将作为每个 http request 的应答返回给客户端。

在 Go 二进制文件中嵌入静态资源文件是 Go 核心团队对社区广泛需求的积极回应。在 go 1.16 以前,Go 社区开源的类嵌入静态文件的项目不下十多个,在 Russ Cox关于 embed 的设计草案中,他就列了十多个:

  • github.com/jteeuwen/go-bindata(主流实现)
  • github.com/alecthomas/gobundle
  • github.com/GeertJohan/go.rice
  • github.com/go-playground/statics
  • github.com/gobuffalo/packr
  • github.com/knadh/stuffbin
  • github.com/mjibson/esc
  • github.com/omeid/go-resources
  • github.com/phogolabs/parcello
  • github.com/pyros2097/go-embed
  • github.com/rakyll/statik
  • github.com/shurcooL/vfsgen
  • github.com/UnnoTed/fileb0x
  • github.com/wlbr/templify
  • perkeep.org/pkg/fileembed

Go1.16 原生支持嵌入并且给出一种开发者体验良好的实现方案,这对 Go 社区是一种极大的鼓励,也是 Go 团队重视社区声音的重要表现。

笔者认为 embed 机制是 Go 1.16 中玩法最多的一种机制,也是极具新玩法挖掘潜力的机制。在 embed 加入 Go tip 不久,很多 Gopher 就已经 “脑洞大开”:

有通过 embed 嵌入版本号的:

// github.com/bigwhite/experiments/blob/master/go1.16-examples/stdlib/embed/version/main.go
package main

import (
    _ "embed"
    "fmt"
    "strings"
)

var (
    Version string = strings.TrimSpace(version)
    //go:embed version.txt
    version string
)

func main() {
    fmt.Printf("Version %q\n", Version)
}

// github.com/bigwhite/experiments/blob/master/go1.16-examples/stdlib/embed/version/version.txt
v1.0.1

有通过 embed 打印自身源码的:

// github.com/bigwhite/experiments/blob/master/go1.16-examples/stdlib/embed/printself/main.go
package main

import (
        _ "embed"
        "fmt"
)

//go:embed main.go
var src string

func main() {
        fmt.Print(src)
}

更是有将一个完整的、复杂的带有 js 支持的 web 站点直接嵌入到 go 二进制文件中的示例,鉴于篇幅,这里就不一一列举了。

Go 擅长于 Web 服务,而 embed 机制的引入粗略来看,可以大大简化 web 服务中资源文件的部署,估计这也是之前社区青睐各种静态资源文件嵌入项目的原因。embed 估计也会成为 Go 1.16 中最被 gopher 们喜爱的功能特性。

不过 embed 机制的实现目前有如下一些局限:

  • 仅支持在包级变量前使用//go:embed 指示符,还不支持在函数/方法内的局部变量上应用 embed 指示符(当然我们可以通过将包级变量赋值给局部变量来过渡一下);
  • 使用//go:embed 指示符的包必须以空导入的方式导入 embed 包,二者是成对出现的,缺一不可;

3. net 包的变化

在 Go 1.16 之前,我们检测在一个已关闭的网络上进行 I/O 操作或在 I/O 完成前网络被关闭的情况,只能通过匹配字符串"use of closed network connection"的方式来进行。之前的版本没有针对这个错误定义 “哨兵错误变量”(更多关于哨兵错误变量的内容,可以参考我的专栏文章《别笑!这就是 Go 的错误处理哲学》),Go 1.16 增加了 ErrClosed 这个 “哨兵错误变量”,我们可以通过 errors.Is(err, net.ErrClosed) 来检测是否是上述错误情况。

六. 小结

从 Go 1.16 版本变更的功能特性中,我看到了 Go 团队更加重视社区的声音,这也是 Go 团队一直持续努力的目标。在最新的 Go proposal review meeting 的结论中,我们还看到了这样的一个proposal被 accept:

要知道这个 proposal 的提议是将在 Go 1.18 才会落地的泛型实现分支 merge 到 Go 项目 master 分支,也就是说在 Go 1.17 中就会包含 “不会发布的” 泛型部分实现,这在之前是不可能实现的 (之前,新 proposal 必须有原型实现的分支,实现并经过社区测试与 Go 核心委员会评估后才会在特定版本 merge 到 master 分支)。虽说泛型的开发有其特殊情况,但能被 accept,这恰证明了 Go 社区的声音在 Go 核心团队日益受到重视。

如果你还没有升级到 Go 1.16,那么现在正是时候

本文中涉及的代码可以在这里下载。https://github.com/bigwhite/experiments/tree/master/go1.16-examples


“Gopher 部落” 知识星球正式转正(从试运营星球变成了正式星球)!“gopher 部落” 旨在打造一个精品 Go 学习和进阶社群!高品质首发 Go 技术文章,“三天” 首发阅读权,每年两期 Go 语言发展现状分析,每天提前 1 小时阅读到新鲜的 Gopher 日报,网课、技术专栏、图书内容前瞻,六小时内必答保证等满足你关于 Go 语言生态的所有需求!部落目前虽小,但持续力很强。在 2021 年上半年,部落将策划两个专题系列分享,并且是部落独享哦:

  • Go 技术书籍的书摘和读书体会系列
  • Go 与 eBPF 系列

考虑到部落尚处于推广期,这里仍然为大家准备了新人优惠券,虽然优惠幅度有所下降,但依然物超所值,早到早享哦!

Go 技术专栏 “改善 Go 语⾔编程质量的 50 个有效实践” 正在慕课网火热热销中!本专栏主要满足广大 gopher 关于 Go 语言进阶的需求,围绕如何写出地道且高质量 Go 代码给出 50 条有效实践建议,上线后收到一致好评!欢迎大家订阅!目前该技术专栏正在新春促销!关注我的个人公众号 “iamtonybai”,发送 “go 专栏活动” 即可获取专栏专属优惠码,可在订阅专栏时抵扣 20 元哦 (2021.2 月末前有效)。

Gopher Daily(Gopher 每日新闻) 归档仓库 - https://github.com/bigwhite/gopherdaily

我的联系方式:

更多原创文章干货分享,请关注公众号
  • 加微信实战群请加微信(注明:实战群):gocnio
astaxie 将本帖设为了精华贴 02月25日 14:28
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册