Go 1.16 中值得關注的幾個變化

bigwhite-github發表於2021-02-25

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

我的聯絡方式:

更多原創文章乾貨分享,請關注公眾號
  • Go 1.16 中值得關注的幾個變化
  • 加微信實戰群請加微信(註明:實戰群):gocnio

相關文章