Go 1.22中值得關注的幾個變化,你知道幾個?
美國時間2024年2月6日,正當中國人民洋溢在即將迎來龍年春節的喜慶祥和的氣氛中時,Eli Bendersky[1]代表Go團隊在Go官博發文“Go 1.22 is released![2]”,正式向世界宣告了Go 1.22版本的發布!
注:大家可以從Go官網下載Go 1.22的第一個版本go 1.22.0,也可以在Go playground[3]上選擇Go 1.22版本在線體驗Go 1.22的語法。
記憶中,這似乎是Eli Bendersky首次代表Go團隊撰寫Go版本發布的文章,文章短小且言簡意賅,會讓大家誤以為Go 1.22版本沒有太多的功能點變更,其實不然。讀過我之前寫的“Go 1.22新特性前瞻[4]”一文的童鞋都知道Go 1.22中有很多重要且影響深遠的值得我們關注的變化。在這篇文章中,我們就再來介紹一下這些變化,供大家參考。
0. 插播“舊聞”:Go再次進入Top10,并刷新有史以來的最高排名
TIOBE編程語言排行榜發布2024年2月編程語言排名的時間恰逢中國人民的傳統佳節春節期間,因此它的這次排名發布“淹沒”在了“龍年大吉”的喜慶氣氛當中了。年后開工,大家翻看這條“舊聞”時,才發現在這次排名中,Go再一次回到Top10,位列第8名,刷新了Go打榜一來的歷史最好位次。
單看這一次進入top10似乎沒有什么,因為2023年4月份,Go也躋身過top10,排名第10。但如果從Go打榜以來的歷史曲線來看,如下圖:
我們看到了“翹尾”,我們看到了Go邁過“低谷”后的爬升!這與我在《Go語言第一課專欄》[5]的結課語《和你一起迎接Go的黃金十年》[6]中預判:Go即將迎來自己的黃金十年 愈來愈吻合了!
不過,我在《2023年Go語言盤點:穩中求新,穩中求變[7]》一文中提到過TIOBE index作為世界最知名的編程語言排行榜,卻存在其“不靠譜”的特性,比如這一期排名中,上古時代的編程語言Fortran從去年同期的第24位上升至第11位,僅比PHP落后一位,另一門古老的COBOL語言也從去年同期的第30位上升至第19位,僅僅比大熱的Rust語言落后一位。
因此,對于TIOBE的排名,大家既要了解,也無需過于看重^_^。
言歸正傳,我們來說說Go 1.22版本的變化。
1. 語言變化
Go 1.22對語言語法做了兩處變更,一個是Go 1.21版本[8]中的試驗特性loopvar在Go 1.22中轉正落地;另一個也和for循環有關,那就是for range新增了對整型表達式的支持。兩者相比較,還是第一個變化loopvar帶來的影響更大一些。為什么呢?因此這是Go語言發展歷史上第一次真正的填語義層面的“坑”,而且修改的是一個在Go源碼中最常用的控制結構的執行語義,這很大可能會帶來break change。Go101教程[9]的作者老貘[10]將之成為Go歷史上最大的向后兼容性破壞版本[11]。
注:Go 1.21版本[12]有一個對panic(nil)的語義修正,但我估計很少會有人寫出panic(nil)這樣的代碼。
這次語義修改用一句話表達就是:將經典三段式for循環語句以及for range語句中的用短聲明形式定義的循環變量從整個循環定義和共享一個,變為每個迭代定義一個。
這里借用Go官博文章中那個例子再說明一下這個語義變化:
// go1.22-examples/lang/loopvar/main.go
package main
import (
"fmt"
"time"
)
func main() {
done := make(chan bool)
values := []string{"a", "b", "c"}
for _, v := range values {
go func() {
time.Sleep(time.Second)
fmt.Println(v)
done <- true
}()
}
// wait for all goroutines to complete before exiting
for _ = range values {
<-done
}
}
我們用Go 1.22.0版本之前的版本,比如Go 1.21.0,來運行該示例:
$go run main.go
c
c
c
我們看到:由于v是整個循環中各個迭代共享的一個變量,所以在每個迭代新創建的goroutine中輸出的v都是循環結束后v的最終值c。
如果我們用go 1.22.0來運行上述示例,我們將得到:
// 輸出的值的順序與goroutine調度有關
$go run main.go
b
c
a
注:關于Go 1.22版本之前的for range的坑,我的極客時間專欄《Go語言第一課》[13]專欄有圖文并茂的原理講解,歡迎訂閱閱讀。
那么,loopvar這一語義填“坑”究竟會對你的代碼造成怎樣的影響呢?在Russ Cox關于loopvar語義變更的設計文檔[14]中提到了:只有go.mod中的go version在go 1.22.0及以后的時候才會生效,這是一個漸進式過渡的過程,因此目前無論是開源項目還是商業項目,只要go.mod中的go version還沒有更新為大于等于go 1.22.0,那么for循環依然會保留短聲明定義的變量的原語義,這樣這些項目都不會受到影響。
不過,如果是直接在腳本中通過go run xxx.go形式運行某個go源碼的,且當前工作目錄以及父目錄下沒有go.mod文件的,go 1.22.0會采用新的loopvar語義,這點大家要注意了。
此外,當你將go.mod中的go version升級到go 1.22.0或更高版本時,也要注意語義變更可能帶來的問題。在升級go version之前,可以用Go 1.22版本之前的go vet對項目源碼進行一次靜態分析,對于go vet提示:“loop variable v captured by func literal”的地方務必注意逐個確認。
注:Go 1.22版本中的go vet已經移除了在go version >= 1.22.0時,對“loop variable v captured by func literal”情況進行警告的功能。
關于Go 1.22中for range支持后面接整型表達式的“語法糖”新特性以及函數迭代器的實驗特性[15],這里就不細說了,大家可以看看“Go 1.22新特性前瞻[16]”一文中的說明。
2. 編譯器、運行時與工具鏈
在編譯器、運行時和工具鏈這些方面,Go 1.22的正式版本與“Go 1.22新特性前瞻[17]”一文中使用的Go 1.22rc1版本幾乎沒有差異,這里挑主要內容介紹一下,其他一些內容可以參考前瞻[18]一文。
Go 1.22版本繼續在編譯上優化PGO(profile-guided optimization), 基于PGO的構建可以比以前版本實現更高比例的調用去虛擬化(devirtualize)。在Go 1.22中,官?給出的PGO帶來的性能提升數字是2%~14%,這應該是基于Google內部一些典型的Go程序測算出來的。
注:如果你對PGO優化還不是很了解,可以看看“深入理解Profile Guided Optimization(PGO)[19]”這篇文章。
Go 1.22版本編譯器現在可以更多運?devirtualize和inline。在Go編譯器中,devirtualize是一種編譯優化技術,旨在消除“虛函數”調用的開銷。“虛函數”是指在面向對象編程中,通過基類指針或引用調用的函數。在Go中所謂虛函數調用指的就是通過接口類型變量進行的方法調用。由于是動態調用,基于接口的方法調用需要在運行時進行查找和分派,這可能導致一定的性能損失。
而Go編譯器在進行devirtualize優化時,會嘗試根據程序的上下文信息和類型信息,確定方法調用的具體對象實例。如果編譯器能夠確定調用的具體實例,則會將通過接口的方法調用替換為直接調用具體對象實例的方法,從而消除運行時的開銷,使得通過接口類型變量進行方法調用的性能得到優化提升。
Go 1.22版本中的運行時可以使基于類型的垃圾收集的元數據更接近每個堆對象,從而將Go程序的CPU性能(延遲或吞吐量)提高了1-3%。這一變化還支持通過重復數據刪除冗余元數據,進而將大多數Go程序的內存開銷減少了大約1%。
在工具鏈方面,有三個主要改變這里提一下:
- go work支持vendor
在Go 1.22版本中,通過go work vendor可以將workspace中的依賴放到vendor?錄下,同時在構建時,如果workspace下有vendor?錄,那么默認的構建是go build -mod=vendor,即基于vendor的構建。
- go mod init不再care其他vendor工具的配置文件
go mod init不再嘗試將其他vendor工具(例如Gopkg.lock )的配置文件導入到go module依賴文件(go.mod)中了,也就是說從Go 1.22版本開始,go module出現之前的那些gopath時代的依賴管理工具正式退出并成為歷史了。
- 改進go test -cover的輸出
對于沒有自己的測試文件的包,go test -cover在go 1.22版本之前會輸出:
? mymod/mypack [no test files]
但在Go 1.22版本之后,會報告覆蓋率為0.0%:
mymod/mypack coverage: 0.0% of statements
3. 標準庫
這里列舉一下標準庫值得關注的重大變化,大家可以與前瞻[20]一文相互參考著閱讀。
3.1 math/rand/v2:標準庫的第一個v2版本包
Go 1.22中新增了math/rand/v2包,這里之所以將它列為Go 1.22版本標準庫的?次重要變化,是因為這是標準庫第一次為某個包建?v2版本,按照Russ Cox的說法,這次math/rand/v2包的創建,算是為標準庫中的其他可能的v2包“探探路”,找找落地路徑。關于math/rand/v2包相對于原math/rand包的變化有很多,具體可以參考issue 61716[21]中的設計與討論。
3.2 增強http.ServeMux表達能力
在Go 1.22版本中,http.ServeMux的表達能力得到了大幅提升,從原先只支持靜態路由,到新版本中支持如下一些特性:
- "/index.html"路由將匹配任何主機和方法的路徑"/index.html";
- "GET /static/"將匹配路徑以"/static/"開頭的GET請求;
- "example.com/"可以與任何指向主機為"example.com"的請求匹配;
- "example.com/{$}"會匹配主機為"example.com"、路徑為"/"的請求,即"example.com/";
- "/b/{bucket}/o/{objectname...}"匹配第一段為"b"、第三段為"o"的路徑。名稱"bucket"表示第二段,"objectname"表示路徑的其余部分。
并且新版ServeMux在路由匹配性能方面也不輸眾多開源http路由框架太多,后續建立Go web或api類新項目時,可以考慮首選新版ServeMux來進行路由匹配了,減少對外部的一個依賴。
關于新版http.ServeMux的具體使用方法,其作者Jonathan Amsterdam(也是log/slog[22]的作者)在官博發表了一篇名為“Routing Enhancements for Go 1.22[23]”的文章,大家可以詳細參考。
關于標準庫的其他一些變化,大家可以參考前瞻[24]一文以及更詳細的Go 1.22的發布說明文檔[25]。
4. 小結
綜上,Go 1.22版本對語言、編譯器、工具鏈、運行時和標準庫都有一定程度的改進和創新,遺留代碼通過Go 1.22版本的重新編譯便可以得到一定程度的性能上的自然提升,這也體現了Go語言在穩中求新、穩中求變[26]的特點。
不過這里還要提醒各位Go開發者,在升級Go 1.22版本時務必注意潛在的向后兼容性問題,尤其是loopvar語義帶來的變化影響。
本文涉及的源碼可以在這里[27]下載。
參考資料
[1]
Eli Bendersky: https://github.com/eliben
[2] Go 1.22 is released!: https://go.dev/blog/go1.22
[3] Go playground: https://go.dev/play/
[4] Go 1.22新特性前瞻: https://tonybai.com/2023/12/25/go-1-22-foresight/
[5] 《Go語言第一課專欄》: http://gk.link/a/10AVZ
[6] 《和你一起迎接Go的黃金十年》: https://time.geekbang.org/column/article/486536
[7] 2023年Go語言盤點:穩中求新,穩中求變: https://tonybai.com/2023/12/31/the-2023-review-of-go-programming-language/
[8] Go 1.21版本: https://tonybai.com/2023/08/20/some-changes-in-go-1-21/
[9] Go101教程: https://go101.org/
[10] 老貘: https://github.com/go101
[11] Go歷史上最大的向后兼容性破壞版本: https://twitter.com/go100and1/status/1755598141351677983
[12] Go 1.21版本: https://tonybai.com/2023/08/20/some-changes-in-go-1-21/
[13] 《Go語言第一課》: http://gk.link/a/10AVZ
[14] 關于loopvar語義變更的設計文檔: https://go.googlesource.com/proposal/+/master/design/60078-loopvar.md
[15] 函數迭代器的實驗特性: https://go.dev/wiki/RangefuncExperiment
[16] Go 1.22新特性前瞻: https://tonybai.com/2023/12/25/go-1-22-foresight/
[17] Go 1.22新特性前瞻: https://tonybai.com/2023/12/25/go-1-22-foresight/
[18] 前瞻: https://tonybai.com/2023/12/25/go-1-22-foresight
[19] 深入理解Profile Guided Optimization(PGO): https://andrewwphillips.github.io/blog/pgo.html
[20] 前瞻: https://tonybai.com/2023/12/25/go-1-22-foresight
[21] issue 61716: https://go.dev/issue/61716
[22] log/slog: https://tonybai.com/2022/10/30/first-exploration-of-slog
[23] Routing Enhancements for Go 1.22: https://go.dev/blog/routing-enhancements
[24] 前瞻: https://tonybai.com/2023/12/25/go-1-22-foresight
[25] Go 1.22的發布說明文檔: https://go.dev/doc/go1.22
[26] Go語言在穩中求新、穩中求變: https://tonybai.com/2023/12/31/the-2023-review-of-go-programming-language
[27] 這里: https://github.com/bigwhite/experiments/tree/master/go1.22-examples
[28] Gopher部落知識星球: https://public.zsxq.com/groups/51284458844544
[29] 鏈接地址: https://m.do.co/c/bff6eed92687