Go 在 2019 年發佈了Go 1.12與Go 1.13。Go 1.13 的大部分變化在於工具鏈、運行時和庫的實現。時隔半年,Go 1.14 正式發佈。
和之前的版本一樣,該版本保留了 Go 1 兼容性的承諾,這個版本的大部分更新在工具鏈 、運行時庫的性能提升方面。總的來說,還是在已有的基礎上不斷優化提成,大家期待的泛型還沒有到來,下面一塊看看新的變化吧。重大的更新如下:
Go 命令中的 Module 支持現在可以投入生產
嵌入具有重疊方法集的接口
defer 性能改進
goroutine 支持異步搶佔
工具的變化
time.Timer 定時器性能大幅提升
Go 命令中的 Module 支持現在可以投入生產
現在,可以在 Go 命令中使用 Module 支持,以供生產使用,並且鼓勵所有用戶遷移到 Go Module 以進行依賴項管理。
嵌入具有重疊方法集的接口
Go 1.14 現在允許嵌入具有重疊方法集的接口:來自嵌入式接口的方法允許與 (嵌入) 接口中已存在的方法擁有相同的名稱和簽名。
在 Go 1.14 之前,如下的定義會編譯報錯。
type ReadWriteCloser interface {
io.ReadCloser
io.WriteCloser
}
因爲 io.ReadCloser 和 io.WriteCloser 中 Close 方法重複了。Go 1.14開始允許相同簽名的方法可以內嵌入一個接口中。與以前一樣,接口中顯式聲明的方法必須保持唯一性。
defer 性能改進
Go1.14 提高了 defer 的大多數用法的性能,幾乎 0 開銷!defer 已經可以用於對性能要求很高的場景了。
關於 defer,在Go 1.13 版本已經做了一些的優化,相較於 Go 1.12,defer 大多數用法性能提升了 30%。而 Go 1.14 的此次改進之後更加高效!
goroutine 支持異步搶佔
調度器使用的 G-M-P 模型。下面是相關的概念:
G(Goroutine):goroutine,由關鍵字 go 創建
M(Machine):在 Go 中稱爲 Machine,可以理解爲工作線程
P(Processor):處理器 P 是線程 M 和 Goroutine 之間的中間層(並不是CPU)
M 必須持有 P 才能執行 G 中的代碼,P有自己本地的一個運行隊列,由可運行的 G 組成,Go 語言調度器的工作原理就是處理器P的隊列中選擇隊列頭的 goroutine 放到線程 M 上執行,上圖展示了 線程 M、處理器 P 和 goroutine 的關係。
每個P維護的G可能是不均衡的,調度器還維護了一個全局G隊列,當P執行完本地的G任務後,會嘗試從全局隊列中獲取G任務運行(需要加鎖),當P本地隊列和全局隊列都沒有可運行的任務時,會嘗試偷取其他P中的G到本地隊列運行(任務竊取)。
在 Go 1.1 版本中,調度器還不支持搶佔式調度,只能依靠 goroutine 主動讓出 CPU 資源,存在非常嚴重的調度問題:
單獨的 goroutine 可以一直佔用線程運行,不會切換到其他的 goroutine,造成飢餓問題
垃圾回收需要暫停整個程序(Stop-the-world,STW),如果沒有搶佔可能需要等待幾分鐘的時間,導致整個程序無法工作
Go 1.12 中編譯器在特定時機插入函數,通過函數調用作爲入口觸發搶佔,實現了協作式的搶佔式調度。但是這種需要函數調用主動配合的調度方式存在一些邊緣情況,就比如說下面的例子:
import (
"runtime"
"time"
)
func main() {
runtime.GOMAXPROCS(1)
go func() { //創建一個goroutine並掛起
for {
}
}()
time.Sleep(time.Millisecond) //main goroutine 優先調用了 休眠
println("OK")
}
此時唯一的 P 會轉去執行 for 循環所創建的 goroutine,進而 main goroutine 永遠不會再被調度。換一句話說在Go1.14之前,上邊的代碼永遠不會輸出 OK。這是因爲 Go 1.12 實現的協作式的搶佔式調度是不會使一個沒有主動放棄執行權、且不參與任何函數調用的 goroutine 被搶佔。
Go1.14 通過實現了基於信號的真搶佔式調度解決了上述問題,這是一個非常大的改動,Go 團隊對已有的邏輯進行重構併爲 goroutine 增加新的狀態和字段來支持搶佔。沒有函數調用的循環不再能致使調度程序死鎖或影響 GC。除了 Windows/arm,darwin/arm,js/wasm 和 plan9/* 之外的所有平臺均支持此功能。
實施搶佔的結果是,在包括 Linux 和 macOS 系統在內的 Unix 系統上,使用 Go 1.14 構建的程序將比使用早期版本構建的程序接收更多的信號。這意味着使用諸如 syscall 或 golang.org/x/sys/unix 之類的軟件包的程序將看到更多較慢的系統調用,並出現 EINTR 錯誤。這些程序將必須以某種方式處理那些錯誤,最有可能的循環是再次嘗試系統調用。有關此內容的更多信息,請參見用於 Linux 系統的 man 7 signal 或用於其他系統的類似文檔。
工具的變化
關於Go1.14中對工具的完善,主要說一下 go mod 和 go test,Go官方肯定希望開發者使用官方的包管理工具,Go1.14 完善了很多功能。
go mod 主要做了以下改進:
incompatiable versions:如果模塊的最新版本包含go.mod文件,則除非明確要求或已經要求該版本,否則go get將不再升級到該模塊的不兼容主要版本。直接從版本控制中獲取時,go list還會忽略此模塊的不兼容版本,但如果由代理報告,則可能包括這些版本。
go.mod文件維護:除了
go mod tidy
之外的 go 命令不再刪除 require指令,該指令指定了間接依賴版本,該版本已由主模塊的其他依賴項隱含。除了go mod tidy
之外的 go 命令不再編輯 go.mod 文件,如果更改只是修飾性的。Module下載:在module模式下,go命令支持 SVN 倉庫,go 命令現在包括來自模塊代理和其他HTTP服務器的純文本錯誤消息的摘要。如果錯誤消息是有效的UTF-8,且包含圖形字符和空格,只會顯示錯誤消息。
go test -v 現在將 t.Log 輸出流式傳輸,而不是在所有測試數據結束時輸出。
time.Timer 定時器性能大幅提升
在 Go 1.10 之前的版本中,Go語言使用1個全局的四叉小頂堆維護所有的timer。由time.after,time.Tick,net.Conn.SetDeadline和friends所使用的內部計時器效率更高,鎖爭用更少,上下文切換更少。這是一項性能改進,不會引起任何用戶可見的更改。
這邊具體的改進,大家可以自行了解下,相對比較複雜,筆者正在學習最新的實現,後續專門講這部分內容。
小結
Go 1.14 還有很多其他變更:
WebAssembly的變化
reflect包的變化
很多其他重要的包(math,http等)的改變
Go語言的錯誤處理提案獲得了社區很多人的支持,但是也有很多人反對,結論是:Go已經放棄了這一提案!這些思想還沒有得到充分的發展,尤其考慮到更改語言的實現成本時,所以有關枚舉和不可變類型,Go語言團隊最近也是不給予考慮實現的。
Go1.14 也有一些計劃中但是未完成的工作,Go1.14 嘗試優化頁分配器(page allocator),能夠實現在 GOMAXPROCS 值比較大時,顯著減少鎖競爭。這一改動影響很大,能顯著的提高 Go 並行能力,也會進一步提升 timer 的性能。但是由於實現起來比較複雜,有一些來不及解決的問題,要 delay 到 Go1.15 完成了。
關於 Go 1.14 的詳細發佈日誌,可參見 https://golang.org/doc/go1.14。
參考
go 1.14 https://golang.org/doc/go1.14
關於Go1.14,你一定想知道的性能提升與新特性 https://studygolang.com/articles/26529?fr=sidebar