Go 1.15 將帶來什麼
Go 1.15 是 Go 程式語言的第 16 個主要版本,預計將於 8 月 1 日推出。這次的更新幅度比以往小,但許多重大變革都發生在幕後或工具鏈中:例如,全新的連結器將加快建置速度並縮小執行檔的大小。此外,語言執行階段的效能有所提升、支援的架構有所調整,標準函式庫也有部分更新。整體而言,這應該會是一次紮實的升級。
自Go 1.0 的發布以來,Go 團隊在每個版本中都持續為工具鏈與標準函式庫帶來改進,但在語言本身的變動上始終保持保守。許多其他語言每個版本都會推出重要的語言新功能,但 Go 自 1.0 之後在各個版本中只加入了少數幾個小幅度的調整。
這是刻意的設計選擇:自 1.0 版以來,團隊的重點一直是穩定性與簡潔性。Go 1 相容性承諾保證,所有為 Go 1.0 撰寫的程式,在後續所有的 1.x 版本中都能不需修改、持續正確執行。Go 開發者通常認為這是好事——他們的程式可以持續「正常運作」,而且通常會變得越來越快。
在即將推出的 1.15 版中,如預期般幾乎沒有對語言規格的更動;改進主要集中在工具鏈、編譯器效能以及標準函式庫上。如同技術負責人 Russ Cox 所指出的,核心開發者們考量到疫情影響,計畫在 1.15 版中採取更為保守的策略:
我們不知道未來幾個月會有多艱難,所以讓我們保守一點,不要在最後一刻提交那些需要除錯的細微變更,給自己帶來不必要的壓力。把這些變更留到下一個週期的開始,讓它們有充分的時間進行測試。
[...] Go 1.15 將會是一個比以往規模更小的版本,這樣也很好。
Go 1.15 已於 5 月 1 日進入功能凍結,Go 團隊計畫在 8 月 1 日發布正式版本,維持一貫的六個月發布週期。
Go 的開發模式與大多數開源語言相當不同。這個語言由 Google 設計,且多數核心開發者都在 Google 任職(因此後續開發實際上是由 Google 贊助)。這個語言採用寬鬆的 BSD 風格授權,開發過程完全公開,一般性討論都在 golang-dev 郵件論壇上進行。變更或新功能的提案與討論則在 GitHub 儲存庫的 issues 中進行,而程式碼審查則透過對 Gerrit 程式碼變更(稱為「changelists」或「CLs」)的留言來完成。
全新的連結器
1.15 版中最大的一項工具鏈變革,就是完全重寫的連結器。由 Go 核心貢獻者 Austin Clements 於 2019 年 9 月撰寫的全新連結器設計文件,詳細說明了重寫的動機以及將帶來的改進。新的連結器有三項主要的結構性變更:
- 將部分工作從連結器移至編譯器:這讓平行化成為可能,因為編譯可以在多個 CPU(或多台機器)上平行執行,但連結步驟在建置的最後幾乎都必須依序執行。此外,編譯器的結果會被 Go 工具鏈快取。
- 改進關鍵的資料結構,主要是盡量避免使用字串。目前的連結器使用一個以字串為索引的龐大符號表;新設計則透過符號編號技術,盡可能避免使用字串。
- 避免一次將所有輸入的物件檔載入記憶體:這讓新連結器在處理大型程式時使用更少的記憶體,整體配置的記憶體也更少(目前的連結器有超過 20% 的時間都花在垃圾回收上)。
隨著原始連結器的作者 Ken Thompson 退休,可維護性也成了必須考量的問題。如 Clements 所說:
原始的連結器也比現在簡單得多,其實作可以完全裝在一位圖靈獎得主的腦海中,因此幾乎沒有抽象化或模組化的設計。不幸的是,隨著連結器不斷成長與演進,它依然維持著缺乏結構的狀態,而我們唯一的圖靈獎得主已經退休了。
鑑於這是一項影響深遠的長期變革,相關工作是在一個分支(dev.link)上進行,只有在穩定的時間點才會合併到 master 分支。負責新連結器開發的 Than McIntosh 描述了 1.15 版已完成的部分:設計文件中大多數的結構性改進已經完成,包括新的物件檔格式與更緊湊的符號表示方式。建置速度已經比 1.14 更快、使用的記憶體也更少,但部分功能(例如採用 DWARF 5 除錯格式)則要等到 1.16 版才會推出。
Clements 進一步說明了平行化方面的工作,以及這項工程如何逐步分階段導入:
我們 [...] 在過程中也做了許多其他改進,例如將關鍵階段平行化,以及移除大量不必要的 I/O 同步。為了最好地承接過去在連結器上的所有成果,我們將這次轉換以「波前」的方式進行,透過一個將新表示法轉換為舊表示法的階段,並在連結器中不斷將這個階段往後推。我們還沒有完成:這個轉換階段目前仍然存在,只是發生的時機與實際作用會因平台而異。對於 amd64 ELF 平台來說,它發生得相當晚,做的事也相對較少。對於其他平台,則還沒有推得那麼後面、需要處理的事也更多,因此目前的效益還沒那麼大。無論如何,1.16 版還有更多值得期待的地方。
目前,連結器在連結的最後階段仍會將輸出轉換回舊的記憶體內表示法。可以預期在未來的 Go 版本中,這些最後的步驟將會被移入新連結器中,轉換階段也會被完全移除,進一步縮短連結時間並降低記憶體使用量。
更小的執行檔
與此相關的是多項可縮小編譯後執行檔大小的改進。如 Brad Fitzpatrick 所展示的,新連結器能消除更多未使用的程式碼,使他那個(相當刻意的)測試程式從 Go 1.14 的 8.2MB 縮小到 1.15 的 3.9MB。對於更貼近實際的程式,執行檔大小則減少了3.5%甚至高達 22%。我自己運行的一個網頁伺服器程式,就從 21.4MB 降至 20.3MB,減少了 4.9%。
造成體積縮小的主要原因,是新連結器對未使用程式碼的消除,以及幾項針對性的改進,例如 Clements 的 CL 230544,它減少了執行檔中包含的堆疊與暫存器對應表的數量。這些對應表原本供 Go 的垃圾回收器(GC)用來判斷哪些物件仍在使用,但現在只需要在呼叫點提供,而不需針對每一道指令都提供。這項變更讓 go 執行檔的大小減少了 5.7%,同時也大幅加快了編譯與連結的速度。
由於 Go 具備在執行階段檢查型別的能力(透過 reflect 套件),Go 的執行檔中包含了大量的型別資訊。由 Cherry Zhang 提交的CL 231397則改為只有在符號被轉換為介面時,才會在輸出中包含其型別資訊(只有轉換為介面的值才能搭配反射使用)。這項變更讓 hello-world 程式的大小減少了 7.2%。
還有一些其他針對執行檔大小的小幅改進,例如 Brad Fitzpatrick 的 CL 228111,如果只使用 TLS 客戶端與伺服器端其中之一,就避免在輸出中同時包含兩者的程式碼,讓一個TLS 撥號 hello world程式的大小減少了 3.2%。
效能改進
Go 1.15 帶來了許多小幅的效能改進,其中較值得注意的兩項來自多產的非 Google 貢獻者 Josh Bleecher Snyder。CL 216401 在將小整數轉換為介面值時避免配置記憶體,讓編譯至組譯碼的時間改善了 2%。將值轉換為介面值類似於其他語言中的「boxing」;這項最佳化在精神上類似於 Python 的小整數快取,不過由於靜態型別的關係,在 Go 中發生的頻率要低得多。
Snyder 的第二項改進是位於編譯器與執行階段內部的 CL 226367,它讓編譯器能為垃圾回收器的 write barrier 呼叫使用更多的 x86 暫存器。Go 使用 write barrier(有點類似鎖)來在 GC 與使用者程式碼並行執行時,維持堆積上的資料完整性(這篇關於 Go GC 的詳細分析有更多資訊)。這項改進讓執行檔略微變小,編譯時間也改善了 1%。
Michael Knysze 重新設計了記憶體配置器中的「mcentral」資料結構以降低鎖競爭,大幅提升了大型區塊記憶體配置的吞吐量。新的配置程式碼經過重新設計後,對於 12KB 或更大的區塊,速度提升了一倍以上。
工具鏈與移植版本
Go 的「modules」功能(Go 的依賴套件管理系統)最早在 Go 1.11 中推出,並在 1.13 中加入了對模組鏡像或「proxy」的支援。1.15 版則新增了對備援 proxy的支援,讓 go 工具在下載模組原始碼時,若第一個位址失敗,可以改用次要主機。備援設定是透過 GOMODCACHE 環境變數新增的「|」分隔符號來指定。
Go 1.15 移除了兩個較舊的移植版本:darwin/386 與 darwin/arm,它們為 macOS 及其他 Apple 作業系統提供 32 位元執行檔。Fitzpatrick 指出,macOS Catalina 已不再支援執行 32 位元應用程式,因此移除這些移植版本將有助於釋放 macOS 建置機器的資源,同時讓編譯器本身也略微縮小。這些移植版本已在 Go 1.14 版中宣告棄用,並將在 Go 1.15 中移除。
另一方面,linux/arm64 移植版本被升級為「first class port」,這意味著 linux/arm64 的建置若失敗,將會阻擋版本的發布;Go 團隊也會為其提供官方執行檔與安裝文件。如 Fitzpatrick 所指出的,Linux 64 位元 Arm 的重要性如今至少已與 32 位元 Arm 相當,而後者早已是 first-class 移植版本。
在 Windows 上,Go 1.15 現在會產生位址空間配置隨機化(ASLR)預設啟用的執行檔。ASLR 透過位置無關碼在啟動時隨機化各個資料區域的位址,讓攻擊者更難預測目標位址並製造記憶體損毀攻擊。
標準函式庫新增內容
Go 的標準函式庫規模龐大且相當穩定;在 Go 1.15 中僅加入了相對次要的功能。
標準函式庫中的 testing 套件相當精簡——Go 的理念是避免為了撰寫測試與斷言而使用特定領域語言,而是直接用開發者已經熟悉的 Go 來撰寫。不過,核心開發者認為建立暫存目錄的需求足夠常見,因此核准新增了 TempDir() 方法,它會為當前的測試延遲建立一個暫存目錄,並在測試結束時自動刪除。
net/url 套件新增了新的 URL.Redacted() 方法,它會以字串形式回傳 URL,但將密碼隱去(替換為 xxxxx)。像 https://username:[email protected]/ 這類帶有密碼的 URL 雖然在瀏覽器中已不常見,但在腳本與工具中仍出乎意料地普遍。Redacted() 可用於更安全地記錄 URL,符合 RFC 3986 中建議不要將 : 之後的內容以明文顯示的規範。
新的 time/tzdata 套件已新增,可讓執行檔內嵌一份靜態的時區資料庫副本。由於它會讓執行檔增加約 800KB,因此採用 opt-in 方式:可透過匯入 time/tzdata 套件,或使用 timetzdata 建置標籤來編譯以啟用。內嵌的資料庫能在某些系統(特別是 Windows)上讓時區資料庫的存取更一致、更可靠,在 Docker 容器與 Go playground 這類虛擬化環境中也可能很有用。
結語
Go 使用 GitHub issues 來追蹤所有的錯誤與功能請求,因此你可以瀏覽 Go 1.15 里程碑中已關閉的 issues 清單,進一步探索這個版本包含的內容。距離 1.15 正式版發布還有超過 2 個月,但你可以使用 gotip 工具輕鬆以最新版本測試自己的程式碼,或等待預定於 6 月 1 日發布的二進位 beta 版本。現在發現的錯誤,幾乎肯定會在 1.15 正式版發布前獲得修正。
隨機一篇部落格
留言
登入後參與討論