萬字長文告訴你Go 1.20中值得關(guān)注的幾個變化
美國時間2023年2月1日,唯一尚未退休的Go語言之父Robert Griesemer[1]代表Go核心開發(fā)團隊在Go官博撰文正式發(fā)布了Go 1.20版本[2]。就像Russ Cox在2022 GopherCon大會[3]所說的那樣:**Go2永不會到來,Go 1.x.y將無限延續(xù)[4]**!
注:似乎新興編程語言都喜歡停留在1.x.y上無限延續(xù),譬如已經(jīng)演化到1.67版本的Rust[5]^_^。
在《Go,13周年》[6]之后,Go 1.20新特性在開發(fā)主干凍結(jié)(2022.11)之前,我曾寫過一篇《??Go 1.20新特性前瞻[??7]》,對照著Go 1.20 milestone[8]中內(nèi)容,把我認為的主要特性和大家簡單過了一遍,不過那時Go 1.20畢竟沒有正式發(fā)布,前瞻肯定不夠全面,某些具體的點與正式版本可能也有差異!現(xiàn)在Go 1.20版本正式發(fā)布了,其Release Notes[9]也補充完整了,在這一篇中,我再來系統(tǒng)說說Go 1.20版本中值得關(guān)注的那些變化。對于在前瞻一文[10]中詳細介紹過的特性,這里不會再重復講解了,大家參考前瞻一文中的內(nèi)容即可。而對于其他一些特性,或是前瞻一文中著墨不多的特性,這里會挑重點展開說說。
按照慣例,我們依舊首先來看看Go語法層面都有哪些變化,這可能也是多數(shù)Gopher們最為關(guān)注的變化點。
一. 語法變化
Go秉持“大道至簡”的理念,對Go語法特性向來是“不與時俱進”的。自從Go 1.18大刀闊斧的加入了泛型特性[11]后,Go語法特性就又恢復到了之前的“新三年舊三年,縫縫補補又三年”的節(jié)奏。Go 1.20亦是如此??!Release Notes說Go 1.20版本在語言方面包含了四點變化,但看了變化的內(nèi)容后,我覺得真正的變化只有一個,其他的都是修修補補。
1. 切片到數(shù)組的轉(zhuǎn)換
唯一算是真語法變化的特性是支持切片類型到數(shù)組類型(或數(shù)組類型的指針)的類型轉(zhuǎn)換,這個特性在前瞻一文[12]中系統(tǒng)講過,這里就不贅述了,放個例子大家直觀認知一下就可以了:
有兩點注意一下就好:
- 切片轉(zhuǎn)換為數(shù)組類型的指針,那么該指針將指向切片的底層數(shù)組,就如同上面例子中slice2arrOK的parr變量那樣;
- 轉(zhuǎn)換的數(shù)組類型的長度不能大于原切片的長度(注意是長度而不是切片的容量哦),否則在運行時會拋出panic。
2. 其他的修修補補
- comparable“放寬”了對泛型實參的限制
下面代碼在Go 1.20版本之前,比如Go 1.19版本中會無法通過編譯:
之前,comparable約束下的泛型形參需要支持嚴格可比較(strictly comparable)的類型作為泛型實參,哪些是嚴格可比較的類型呢?Go 1.20的語法規(guī)范做出了進一步澄清:如果一個類型是可比較的,且不是接口類型或由接口類型組成的類型,那么這個類型就是嚴格可比較的類型,包括:
我們看到:例外的就是接口類型了。接口類型不是“嚴格可比較的(strictly comparable)”,但未作為類型形參的接口類型是可比較的(comparable),如果兩個接口類型的動態(tài)類型相同且值相等,那么這兩個接口類型就相等,或兩個接口類型的值均為nil,它們也相等,否則不等。
Go 1.19版本及之前,作為非嚴格比較類型的接口類型是不能作為comparable約束的類型形參的類型實參的,就像上面comparable.go中示例代碼那樣,但Go 1.20版本開始,這一要求被防控,接口類型被允許作為類型實參賦值給comparable約束的類型形參了!不過這么做之前,你也要明確一點,如果像下面這樣兩個接口類型底層類型相同且是不可比較的類型(比如切片),那么代碼將在運行時拋panic:
Go 1.20語言規(guī)范借此機會還進一步澄清了結(jié)構(gòu)體和數(shù)組兩種類型比較實現(xiàn)的規(guī)范:對于結(jié)構(gòu)體類型,Go會按照結(jié)構(gòu)體字段的聲明順序,逐一字段進行比較,直到遇到第一個不相等的字段為止。如果沒有不相等字段,則兩個結(jié)構(gòu)體字段相等;對于數(shù)組類型,Go會按數(shù)組元素的順序,逐一元素進行比較,直到遇到第一個不相等的元素為止。如果沒有不相等的元素,則兩個數(shù)組相等。
- unsafe包繼續(xù)添加“語法糖”
繼Go 1.17版本[13]在unsafe包增加Slice函數(shù)后,Go 1.20版本又增加三個語法糖函數(shù):SliceData、String和StringData:
值得注意的是由于string的不可更改性,String函數(shù)的參數(shù)ptr指向的內(nèi)容以及StringData返回的指針指向的內(nèi)容在String調(diào)用和StringData調(diào)用后不允許修改,但實際情況是怎么樣的呢?
我們看到:unsafe.String函數(shù)調(diào)用后,如果我們修改了傳入的指針指向的內(nèi)容,那么該改動會影響到后續(xù)返回的string內(nèi)容!但StringData返回的指針所指向的內(nèi)容一旦被修改,就會導致運行時的段錯誤,從而程序崩潰!
二. 工具鏈
1. Go安裝包“瘦身”
這些年,Go發(fā)布版的安裝包“體格”是越來越壯了,動輒100多MB的壓縮包,以go.dev/dl頁面上的go1.xy.linux-amd64.tar.gz為例,我們看看從Go 1.15版本到Go 1.19版本的“體格”變化趨勢:
如果按此趨勢,Go 1.20勢必要上到150MB以上。但Go團隊找到了“瘦身”方法,那就是:從Go 1.20開始發(fā)行版的安裝包不再為GOROOT中的軟件包提供預編譯的.a文件了[14],這樣我們得到的瘦身后的Go 1.20版本的size為95MB!相較于Go 1.19,Go 1.20的安裝包“瘦”了三分之一。安裝包解壓后這種體現(xiàn)更為明顯:
我們看到:Go 1.20占用的磁盤空間僅為Go 1.19版本的一半多一點而已。并且,Go 1.20版本中,GOROOT下的源碼將像其他用戶包那樣在構(gòu)建后被緩存到本機cache中。此外,go install也不會為GOROOT下的軟件包安裝.a文件。
2. 編譯器
1) PGO(profile-guided optimization)
Go 1.20編譯器的一個最大的變更點是引入了PGO優(yōu)化技術(shù)預覽版,這個在前瞻一文中也有對PGO技術(shù)的簡單介紹[15]。說白了點,PGO技術(shù)就是在原有compiler優(yōu)化技術(shù)的基礎(chǔ)上,針對程序在生產(chǎn)環(huán)境運行中的熱點關(guān)鍵路徑再進行一輪優(yōu)化,并且針對熱點代碼執(zhí)行路徑,編譯器會放開一些限制,比如Go決定是否對函數(shù)進行內(nèi)聯(lián)優(yōu)化的復雜度上限默認值是80[16],但對于PGO指示的關(guān)鍵熱點路徑,即便函數(shù)復雜性超過80很多,也可能會被inline優(yōu)化掉。
之前持續(xù)性能剖析工具開發(fā)商Polar Signals曾發(fā)布一篇文章《Exploring Go's Profile-Guided Optimizations》[17],專門探討了PGO技術(shù)可能帶來的優(yōu)化效果,文章中借助了Go項目中自帶的測試示例,這里也基于這個示例帶大家重現(xiàn)一下。
我們使用的例子在Go 1.20源碼/安裝包的$GOROOT/src/cmd/compile/internal/test/testdata/pgo/inline路徑下:
我們首先執(zhí)行一下inline目錄下的測試,并生成用于測試的可執(zhí)行文件以及對應(yīng)的cpu profile文件供后續(xù)PGO優(yōu)化使用:
接下來,我們對比一下不使用PGO和使用PGO優(yōu)化,Go編譯器在內(nèi)聯(lián)優(yōu)化上的區(qū)別:
上面diff命令中為Go test命令傳入-run=none -tags="" -gcflags="-m -m"是為了僅編譯源文件,而不執(zhí)行任何測試。
我們看到,相較于未使用PGO優(yōu)化的結(jié)果,PGO優(yōu)化后的結(jié)果多了兩個inline函數(shù),這兩個可以被inline的函數(shù),一個的復雜度開銷為106,一個是312,都超出了默認的80,但仍然可以被inline。
我們來看看PGO的實際優(yōu)化效果,我們分為在無PGO優(yōu)化與有PGO優(yōu)化下執(zhí)行100次benchmark,再用benchstat工具對比兩次的結(jié)果:
注:benchstat的安裝方法:$go install golang.org/x/perf/cmd/benchstat@latest
我們看到,在我的機器上(ubuntu 20.04 linux kerenel 5.4.0-132),PGO針對這個測試的優(yōu)化效果并不明顯(僅僅有0.24%的提升),Polar Signals原文中的提升幅度也不大,僅為1.05%。
Go官方Release Notes中提到benchmark提升效果為3%~4%,同時官方也提到了,這個僅僅是PGO初始技術(shù)預覽版,后續(xù)會加強對PGO優(yōu)化的投入,直至對多數(shù)程序產(chǎn)生較為明顯的優(yōu)化效果。個人覺得目前PGO尚處于早期,不建議在生產(chǎn)中使用。
Go官方也增加針對PGO的ref頁面[18],大家重點看看其中的FAQ,你會有更多收獲!
2) 構(gòu)建速度
Go 1.18泛型落地后,Go編譯器的編譯速度出現(xiàn)了回退(幅度15%),Go 1.19編譯速度也沒有提升。雖然編譯速度回退后依然可以“秒殺”競爭對手,但對于以編譯速度快著稱的Go來說,這個問題必須修復。Go 1.20做到了這一點,讓Go編譯器的編譯速度重新回歸到了Go 1.17的水準!相對Go 1.19提升10%左右。
我使用github.com/reviewdog/reviewdog這個庫實測了一下,分別使用go 1.17.1、go 1.18.6、go 1.19.1和Go 1.20對這個module進行g(shù)o build -a構(gòu)建(之前將依賴包都下載本地,排除掉go get環(huán)節(jié)的影響),結(jié)果如下:
雖然不能十分精確,但總體上反映出各個版本的編譯速度水準以及Go 1.20相對于Go 1.18和Go 1.19版本的提升。我們看到Go 1.20與Go 1.17版本在一個水平線上,甚至要超過Go 1.17(但可能僅限于我這個個例)。
3) 允許在泛型函數(shù)/方法中進行類型聲明
Go 1.20版本之前下面代碼是無法通過Go編譯器的編譯的:
Go 1.20改進了語言前端的實現(xiàn)[19],通過unified IR實現(xiàn)了對在泛型函數(shù)/方法中進行類型聲明(包括定義type alias)的支持。
同時,Go 1.20在spec[20]中還明確了哪些使用了遞歸方式聲明的類型形參列表是不合法的[21]:
4) 構(gòu)建自舉源碼的Go編譯器的版本選擇
Go從Go 1.5版本開始實現(xiàn)自舉,即使用Go實現(xiàn)Go,那么自舉后的Go項目是誰來編譯的呢?最初對應(yīng)編譯Go 1.5版本的Go編譯器版本為Go 1.4。
以前從源碼構(gòu)建Go發(fā)行版,當未設(shè)置GOROOT_BOOTSTRAP時,編譯腳本會默認使用Go 1.4,但如果有更高版本的Go編譯器存在,會使用更高版本的編譯器。
Go 1.18和Go 1.19會首先尋找是否有g(shù)o 1.17版本,如果沒有再使用go 1.4。
Go 1.20會尋找當前Go 1.17的最后一個版本Go 1.17.13,如果沒有,則使用Go 1.4。
將來,Go核心團隊計劃一年升級一次構(gòu)建自舉源碼的Go編譯器的版本,例如:Go 1.22版本將使用Go 1.20版本的編譯器。
5) cgo
Go命令現(xiàn)在在沒有C工具鏈的系統(tǒng)上會默認禁用了cgo。更具體來說,當CGO_ENABLED環(huán)境變量未設(shè)置,CC環(huán)境變量未設(shè)置以及PATH環(huán)境變量中沒有找到默認的C編譯器(通常是clang或gcc)時,CGO_ENABLED會被默認設(shè)置為0。
3. 其他工具
1) 支持采集應(yīng)用執(zhí)行的代碼蓋率
在前瞻一文中,我提到過Go 1.20將對代碼覆蓋率的支持擴展到了應(yīng)用整體層面,而不再僅僅是unit test。這里使用一個例子來看一下,究竟如何采集應(yīng)用代碼的執(zhí)行覆蓋率。我們以gitlab.com/esr/loccount這個代碼統(tǒng)計工具為例,先修改一下Makefile,在go build后面加上-cover選項,然后編譯loccount,并對其自身進行代碼統(tǒng)計:
上面執(zhí)行l(wèi)occount之前,我們建立了一個mycovdata目錄,并設(shè)置GOCOVERDIR的值為mycovdata目錄的路徑。在這樣的上下文下,執(zhí)行l(wèi)occount后,mycovdata目錄下會生成一些覆蓋率統(tǒng)計數(shù)據(jù)文件:
怎么查看loccount的執(zhí)行覆蓋率呢?我們使用go tool covdata來查看:
當然, covdata子命令還支持其他一些功能,大家可以自行查看manual挖掘。
2) vet
Go 1.20版本中,go工具鏈的vet子命令增加了兩個十分實用的檢測:
- 對loopclosure這一檢測策略進行了增強
具體可參見https://github.com/golang/tools/tree/master/go/analysis/passes/loopclosure代碼
- 增加對2006-02-01的時間格式的檢查
注意我們使用time.Format或Parse時,最常使用的是2006-01-02這樣的格式,即ISO 8601標準的時間格式,但一些代碼中總是出現(xiàn)2006-02-01,十分容易導致錯誤。這個版本中,go vet將會對此種情況進行檢查。
三. 運行時與標準庫
1. 運行時(runtime)
Go 1.20運行時的調(diào)整并不大,僅對GC的內(nèi)部數(shù)據(jù)結(jié)構(gòu)進行了微調(diào),這個調(diào)整可以獲得最多2%的內(nèi)存開銷下降以及cpu性能提升。
2. 標準庫
標準庫肯定是變化最多的那部分。前瞻一文中對下面變化也做了詳細介紹,這里不贅述了,大家可以翻看那篇文章細讀:
- 支持wrap multiple errors
- time包新增DateTime、DateOnly和TimeOnly三個layout格式常量
- 新增arena包 ... ...
標準庫變化很多,這里不能一一羅列,再補充一些我認為重要的,其他的變化大家可以到Go 1.20 Release Notes[22]去看:
1) arena包
前瞻一文已經(jīng)對arena包做了簡要描述,對于arena包的使用以及最佳適用場合的探索還在進行中。著名持續(xù)性能剖析工具pyroscope[23]的官方博客文章《Go 1.20 arenas實踐:arena vs. 傳統(tǒng)內(nèi)存管理》[24]對于arena實驗特性的使用給出了幾點好的建議,比如:
- 只在關(guān)鍵的代碼路徑中使用arena,不要到處使用它們
- 在使用arena之前和之后對你的代碼進行profiling,以確保你在能提供最大好處的地方添加arena。
- 密切關(guān)注arena上創(chuàng)建的對象的生命周期。確保你不會把它們泄露給你程序中的其他組件,因為那里的對象可能會超過arena的壽命。
- 使用defer a.Free()來確保你不會忘記釋放內(nèi)存。
- 如果你想在arena被釋放后使用對象,使用arena.Clone()將其克隆回heap中。
pyroscope的開發(fā)人員認為arena是一個強大的工具,也支持標準庫中保留arena這個特性,但也建議將arena和reflect、unsafe、cgo等一樣納入“不推薦”使用的包行列。這點我也是贊同的。我也在考慮如何基于arena改進我們產(chǎn)品的協(xié)議解析器的性能,有成果后,我也會將實踐過程分享出來的。
2) 新增crypto/ecdh包
密碼學包(crypto)的主要maintainer Filippo Valsorda[25]從google離職后,成為了一名專職開源項目維護者[26]。這似乎讓其更有精力和動力對crypto包進行更好的規(guī)劃、設(shè)計和實現(xiàn)了。crypto/ecdh包就是在他的提議下加入到Go標準庫中的[27]。
相對于標準庫之前存在的crypto/elliptic等包,crypto/ecdh包的API更為高級,Go官方推薦使用ecdh的高級API,這樣大家以后可以不必再與低級的密碼學函數(shù)斗爭了。
3) HTTP ResponseController
以前HTTP handler的超時都是http服務(wù)器全局指定一個的:包括ReadTimeout和WriteTimeout。但有些時候,如果能在某個請求范圍內(nèi)支持這些超時(以及可能的其他選項)將非常有用。Damien Neil就創(chuàng)建了這個增加ResponseController的提案[28],下面是一個在HandlerFunc中使用ResponseController的例子:
4) context包增加WithCancelCause函數(shù)
context包新增了一個WithCancelCause函數(shù),與WithCancel不同,通過WithCancelCause返回的Context,我們可以得到cancel的原因,比如下面示例:
我們看到通過context.Cause可以得到Context在cancel時傳入的錯誤原因。
四. 移植性
Go對新cpu體系結(jié)構(gòu)和OS的支持向來是走在前面的。Go 1.20還新增了對freebsd在risc-v上的實驗性支持,其環(huán)境變量為GOOS=freebsd, GOARCH=riscv64。但Go 1.20也將成為對下面平臺提供支持的最后一個Go版本:
- Windows 7, 8, Server 2008和Server 2012
- MacOS 10.13 High Sierra和10.14 (我的安裝了10.14的mac os又要在go 1.21不被支持了^_^)
近期Go團隊又有了新提案:支持WASI(GOOS=wasi GOARCH=wasm)[29],WASI是啥,它是WebAssembly一套與引擎無關(guān)(engine-indepent)的、面向非Web系統(tǒng)的WASM API標準,是WebAssembly脫離瀏覽器的必經(jīng)之路!一旦生成滿足WASI的WASM程序,該程序就可以在任何支持WASI或兼容的runtime上運行。不出意外,該提案將在Go 1.21或Go 1.22版本落地。
本文中的示例代碼可以在這里[30]下載。
Gopher Daily(Gopher每日新聞)歸檔倉庫 - https://github.com/bigwhite/gopherdaily
- 微博(暫不可用):https://weibo.com/bigwhite20xx
- 微博2:https://weibo.com/u/6484441286
- 博客:tonybai.com
- github: https://github.com/bigwhite
參考資料
[1] Robert Griesemer: https://github.com/griesemer
[2] Go官博撰文正式發(fā)布了Go 1.20版本: https://go.dev/blog/go1.20
[3] Russ Cox在2022 GopherCon大會: https://www.youtube.com/watch?v=v24wrd3RwGo
[4] Go2永不會到來,Go 1.x.y將無限延續(xù): https://tonybai.com/2022/12/29/the-2022-review-of-go-programming-language
[5] 演化到1.67版本的Rust: https://www.rust-lang.org
[6] 《Go,13周年》: https://tonybai.com/2022/11/11/go-opensource-13-years/
[7] Go 1.20新特性前瞻: https://tonybai.com/2022/11/17/go-1-20-foresight
[8] Go 1.20 milestone: https://github.com/golang/go/milestone/250
[9] Release Notes: https://go.dev/blog/go1.20
[10] 前瞻一文: https://tonybai.com/2022/11/17/go-1-20-foresight
[11] Go 1.18大刀闊斧的加入了泛型特性: https://tonybai.com/2022/04/20/some-changes-in-go-1-18
[12] 前瞻一文: https://tonybai.com/2022/11/17/go-1-20-foresight
[13] Go 1.17版本: https://tonybai.com/2021/08/17/some-changes-in-go-1-17
[14] 從Go 1.20開始發(fā)行版的安裝包不再為GOROOT中的軟件包提供預編譯的.a文件了: https://github.com/golang/go/issues/47257
[15] 對PGO技術(shù)的簡單介紹: https://tonybai.com/2022/11/17/go-1-20-foresight
[16] Go決定是否對函數(shù)進行內(nèi)聯(lián)優(yōu)化的復雜度上限默認值是80: https://tonybai.com/2022/10/17/understand-go-inlining-optimisations-by-example
[17] 《Exploring Go's Profile-Guided Optimizations》: https://www.polarsignals.com/blog/posts/2022/09/exploring-go-profile-guided-optimizations/
[18] PGO的ref頁面: https://go.dev/doc/pgo
[19] Go 1.20改進了語言前端的實現(xiàn): https://github.com/golang/go/issues/47631
[20] spec: https://go.dev/ref/spec#Type_parameter_declarations
[21] 哪些使用了遞歸方式聲明的類型形參列表是不合法的: https://github.com/golang/go/issues/40882
[22] Go 1.20 Release Notes: https://go.dev/doc/go1.20
[23] pyroscope: https://pyroscope.io/
[24] 《Go 1.20 arenas實踐:arena vs. 傳統(tǒng)內(nèi)存管理》: https://pyroscope.io/blog/go-1-20-memory-arenas/
[25] Filippo Valsorda: https://filippo.io/
[26] 成為了一名專職開源項目維護者: https://words.filippo.io/full-time-maintainer/
[27] crypto/ecdh包就是在他的提議下加入到Go標準庫中的: https://github.com/golang/go/issues/52221
[28] 增加ResponseController的提案: https://github.com/golang/go/issues/54136
[29] 支持WASI(GOOS=wasi GOARCH=wasm): https://github.com/golang/go/issues/58141
[30] 這里: https://github.com/bigwhite/experiments/blob/master/go1.20-examples
[31] “Gopher部落”知識星球: https://wx.zsxq.com/dweb2/index/group/51284458844544
本文轉(zhuǎn)載自微信公眾號「 白明的贊賞賬戶」,可以通過以下二維碼關(guān)注。轉(zhuǎn)載本文請聯(lián)系白明的贊賞賬戶公眾號。