Go、向後相容性和 GODEBUG

引言

Go 對向後相容性的重視是其主要優勢之一。然而,有時我們無法保持完全相容性。如果程式碼依賴於有 bug(包括不安全)的行為,那麼修復 bug 就會破壞該程式碼。新特性也可能產生類似的影響:HTTP 客戶端啟用 HTTP/2 功能曾導致連線到存在 bug 的 HTTP/2 實現伺服器的程式出現問題。這些型別的更改是不可避免的,並且Go 1 相容性規則允許。即便如此,Go 仍然提供了一種稱為 GODEBUG 的機制,以減少這些更改對使用較新工具鏈編譯舊程式碼的 Go 開發者造成的影響。

GODEBUG 設定是一個 key=value 對,用於控制 Go 程式的某些部分的執行。環境變數 GODEBUG 可以包含這些設定的逗號分隔列表。例如,如果一個 Go 程式執行在包含以下內容的環境中

GODEBUG=http2client=0,http2server=0

則該 Go 程式將預設在 HTTP 客戶端和 HTTP 伺服器中停用 HTTP/2 的使用。GODEBUG 環境變數中無法識別的設定將被忽略。也可以為給定程式設定預設的 GODEBUG(下面討論)。

在準備任何 Go 1 相容性允許但仍可能破壞某些現有程式的更改時,我們首先設計更改以儘可能保持現有程式正常執行。對於剩餘的程式,我們定義了一個新的 GODEBUG 設定,允許單個程式選擇恢復舊的行為。如果不可行,可能不會新增 GODEBUG 設定,但這應該極其罕見。

為相容性而新增的 GODEBUG 設定將至少維護兩年(四個 Go 版本)。有些設定,例如 http2clienthttp2server,將維護更長時間,甚至無限期。

在可能的情況下,每個 GODEBUG 設定都有一個關聯的 runtime/metrics 計數器,命名為 /godebug/non-default-behavior/<name>:events,用於計算特定程式行為因該設定的非預設值而改變的次數。例如,當設定 GODEBUG=http2client=0 時,/godebug/non-default-behavior/http2client:events 會計算程式配置了多少個未啟用 HTTP/2 支援的 HTTP 傳輸。

預設 GODEBUG 值

當 GODEBUG 設定未在環境變數中列出時,其值來源於三個來源:用於構建程式的 Go 工具鏈的預設值,根據 go.mod 中列出的 Go 版本進行修正,然後被程式中顯式的 //go:debug 行覆蓋。

GODEBUG 歷史提供了每個 Go 工具鏈版本的確切預設值。例如,Go 1.21 引入了 panicnil 設定,控制是否允許 panic(nil);它預設為 panicnil=0,使 panic(nil) 成為執行時錯誤。使用 panicnil=1 可以恢復 Go 1.20 及更早版本的行為。

當編譯聲明瞭舊 Go 版本的工作模組或工作區時,Go 工具鏈會修改其預設設定以儘可能與舊 Go 版本匹配。例如,當 Go 1.21 工具鏈編譯一個程式時,如果工作模組的 go.mod 或工作區的 go.work 指明 go 1.20,那麼程式預設使用 panicnil=1,與 Go 1.20 而非 Go 1.21 匹配。

由於這種設定 GODEBUG 預設值的方法僅在 Go 1.21 中引入,列出早於 Go 1.20 版本的程式會被配置為與 Go 1.20 匹配,而不是更早的版本。

要覆蓋這些預設設定,從 Go 1.23 開始,工作模組的 go.mod 或工作區的 go.work 可以列出一行或多行 godebug

godebug (
    default=go1.21
    panicnil=1
    asynctimerchan=0
)

特殊鍵 default 指示從哪個 Go 版本獲取未指定的設定。這允許將 GODEBUG 預設值與模組中的 Go 語言版本分開設定。在此示例中,程式要求 Go 1.21 語義,然後要求 Go 1.21 之前舊的 panic(nil) 行為和 Go 1.23 新的 asynctimerchan=0 行為。

只有工作模組的 go.mod 會被用來查詢 godebug 指令。所需依賴模組中的任何指令都將被忽略。列出無法識別設定的 godebug 是一個錯誤。(早於 Go 1.23 的工具鏈會拒絕所有 godebug 行,因為它們完全不理解 godebug。)

來自 gogodebug 行的預設設定適用於所有構建的主包。為了更精細的控制,從 Go 1.21 開始,主包的原始檔可以在檔案頂部(在 package 語句之前)包含一個或多個 //go:debug 指令。上例中的 godebug 行可以寫成

//go:debug default=go1.21
//go:debug panicnil=1
//go:debug asynctimerchan=0

從 Go 1.21 開始,Go 工具鏈將帶有無法識別的 GODEBUG 設定的 //go:debug 指令視為無效程式。對於給定設定,具有多行 //go:debug 的程式也被視為無效。(較舊的工具鏈完全忽略 //go:debug 指令。)

將編譯到主包中的預設設定可以透過以下命令報告

go list -f '{{.DefaultGODEBUG}}' my/main/package

僅報告與基本 Go 工具鏈預設設定的不同之處。

測試包時,*_test.go 檔案中的 //go:debug 行被視為測試主包的指令。在任何其他上下文中,工具鏈會忽略 //go:debug 行;go vet 會將此類行報告為位置錯誤。

GODEBUG 歷史

本節記錄了出於相容性原因在每個主要的 Go 版本中引入和移除的 GODEBUG 設定。包或程式可以出於內部除錯目的定義額外的設定;例如,請參閱執行時文件go 命令文件

Go 1.24

Go 1.24 添加了一個新的 fips140 設定,用於控制 Go 加密模組是否在 FIPS 140-3 模式下執行。可能的值有

Go 1.24 將全域性的 math/rand.Seed 更改為無操作 (no-op)。此行為由 randseednop 設定控制。對於 Go 1.24,它預設為 randseednop=1。使用 randseednop=0 會恢復到 Go 1.24 之前的行為。

Go 1.24 為 multipathtcp 設定添加了新值。現在 multipathtcp 的可能值為

對於 Go 1.24,它現在預設為 multipathtcp=“2”,因此預設在監聽器上啟用。使用 multipathtcp=“0” 會恢復到 Go 1.24 之前的行為。

Go 1.24 改變了 go test -json 的行為,將構建錯誤輸出為 JSON 而非文字。這些新的 JSON 事件透過新的 Action 值區分,但仍可能導致 CI 系統出現問題,如果它們對此類事件的處理不夠穩健的話。此行為可以透過 gotestjsonbuildtext 設定控制。使用 gotestjsonbuildtext=1 可以恢復 1.23 的行為。此設定將在未來的版本中移除,最早可能在 Go 1.28。

Go 1.24 更改了 crypto/rsa,要求 RSA 金鑰至少為 1024 位。此行為可以透過 rsa1024min 設定控制。使用 rsa1024min=0 可以恢復 Go 1.23 的行為。

Go 1.24 引入了一種機制,用於在 crypto/subtle 包中啟用平臺特定的資料無關時序 (Data Independent Timing, DIT) 模式。可以使用 dataindependenttiming 設定為整個程式啟用此模式。對於 Go 1.24,它預設為 dataindependenttiming=0。當未設定 dataindependenttiming 時,與 Go 1.23 的預設行為沒有變化。使用 dataindependenttiming=1 會為整個 Go 程式啟用 DIT 模式。啟用後,從 Go 呼叫 C 時將啟用 DIT。啟用後,從 C 呼叫 Go 程式碼將啟用 DIT,如果在進入 Go 程式碼時未啟用 DIT,則在返回 C 之前會停用它。這目前僅影響 arm64 程式。對於所有其他平臺,它是一個無操作 (no-op)。

Go 1.24 移除了 x509sha1 設定。crypto/x509 不再支援驗證使用基於 SHA-1 簽名演算法的證書上的簽名。

Go 1.24 將 x509usepolicies setting. 的預設值從 0 更改為 1。預設情況下,在編組證書時,策略現在取自 Certificate.Policies 欄位,而不是 Certificate.PolicyIdentifiers 欄位。

Go 1.24 預設啟用了後量子金鑰交換機制 X25519MLKEM768。可以使用 tlsmlkem 設定來恢復預設值。Go 1.24 還移除了 X25519Kyber768Draft00 和 Go 1.23 的 tlskyber 設定。

Go 1.24 使 ParsePKCS1PrivateKey 使用並驗證編碼私鑰中的 CRT 引數。此行為可以透過 x509rsacrt 設定控制。使用 x509rsacrt=0 可以恢復 Go 1.23 的行為。

Go 1.23

Go 1.23 將 time 包建立的通道更改為無緩衝(同步)通道,這使得正確使用 Timer.StopTimer.Reset 方法結果變得容易得多。asynctimerchan 設定可以停用此更改。此更改沒有執行時指標,此設定可能會在未來的版本中移除,最早可能在 Go 1.27。

Go 1.23 更改了 os.Lstatos.Stat 報告的重解析點 (reparse points) 的模式位,這可以透過 winsymlink 設定控制。從 Go 1.23 (winsymlink=1) 開始,掛載點 (mount points) 不再設定 os.ModeSymlink,並且不是符號連結、Unix socket 或去重檔案 (dedup files) 的重解析點現在總是設定 os.ModeIrregular。由於這些更改,filepath.EvalSymlinks 不再評估掛載點,這是許多不一致性和 bug 的來源。在早期版本 (winsymlink=0) 中,掛載點被視為符號連結,而其他具有非預設 os.ModeType 位(例如 os.ModeDir)的重解析點未設定 ModeIrregular 位。

Go 1.23 更改了 os.Readlinkfilepath.EvalSymlinks,以避免嘗試將卷 (volumes) 規範化為驅動器磁碟機代號,這並不總是可能的。此行為由 winreadlinkvolume 設定控制。對於 Go 1.23,它預設為 winreadlinkvolume=1。早期版本預設為 winreadlinkvolume=0

Go 1.23 預設啟用了實驗性的後量子金鑰交換機制 X25519Kyber768Draft00。可以使用 tlskyber 設定來恢復預設值。

Go 1.23 更改了 crypto/x509.ParseCertificate 的行為,拒絕負數的序列號。此更改可以使用 x509negativeserial 設定恢復。

Go 1.23 預設重新啟用了 html/template 對 ECMAScript 6 模板字面量的支援。jstmpllitinterp 設定不再有任何效果。

Go 1.23 更改了客戶端和伺服器在未顯式配置時使用的預設 TLS 密碼套件,移除了 3DES 密碼套件。可以使用 tls3des 設定來恢復預設值。

Go 1.23 更改了 tls.X509KeyPairtls.LoadX509KeyPair 的行為,填充返回的 tls.Certificate 的 Leaf 欄位。此行為由 x509keypairleaf 設定控制。對於 Go 1.23,它預設為 x509keypairleaf=1。早期版本預設為 x509keypairleaf=0

Go 1.23 更改了 net/http.ServeContentnet/http.ServeFilenet/http.ServeFS,在提供錯誤時移除 Cache-Control、Content-Encoding、Etag 和 Last-Modified 頭。此行為由 httpservecontentkeepheaders 設定控制。使用 httpservecontentkeepheaders=1 可以恢復 Go 1.23 之前的行為。

Go 1.22

Go 1.22 添加了一個可配置的限制,用於控制 TLS 握手過程中可接受的最大 RSA 金鑰大小,由 tlsmaxrsasize 設定控制。預設值為 tlsmaxrsasize=8192,將 RSA 限制為 8192 位金鑰。為避免拒絕服務攻擊,此設定和預設值已向後移植到 Go 1.19.13、Go 1.20.8 和 Go 1.21.1。

Go 1.22 規定 net/http 客戶端或伺服器讀取的請求或響應中 Content-Length 頭為空是錯誤。此行為由 httplaxcontentlength 設定控制。

Go 1.22 更改了 ServeMux 的行為,以接受擴充套件模式並透過段 (segment) 對模式和請求路徑進行 unescape。此行為可以透過 httpmuxgo121 設定控制。

Go 1.22 向 go/types 添加了 Alias 型別,用於顯式表示類型別名。型別檢查器是否生成 Alias 型別由 gotypesalias 設定控制。對於 Go 1.22,它預設為 gotypesalias=0。對於 Go 1.23,gotypesalias=1 將成為預設值。此設定將在未來的版本中移除,最早可能在 Go 1.27。

Go 1.22 將伺服器和客戶端支援的預設最低 TLS 版本更改為 TLS 1.2。可以使用 tls10server 設定來恢復到 TLS 1.0。

Go 1.22 更改了客戶端和伺服器在未顯式配置時使用的預設 TLS 密碼套件,移除了使用基於 RSA 金鑰交換的密碼套件。可以使用 tlsrsakex 設定來恢復預設值。

Go 1.22 在連線既不支援 TLS 1.3 也不支援 Extended Master Secret(在 Go 1.21 中實現)時停用了 ConnectionState.ExportKeyingMaterial。可以使用 tlsunsafeekm 設定重新啟用它。

Go 1.22 改變了執行時與 Linux 透明大頁 (transparent huge pages) 的互動方式。特別地,一個常見的預設 Linux 核心配置可能導致顯著的記憶體開銷,而 Go 1.22 不再會規避此預設設定。為了在不調整核心設定的情況下解決此問題,可以使用 disablethp 設定為 Go 記憶體停用透明大頁。此行為已向後移植到 Go 1.21.1,但此設定僅在 Go 1.21.6 及更高版本中可用。此設定可能會在未來的版本中移除,受此問題影響的使用者應根據GC 指南中的建議調整其 Linux 配置,或切換到完全停用透明大頁的 Linux 發行版。

Go 1.22 將執行時內部鎖的爭用情況新增到 mutex profile 中。這些鎖的爭用總是報告在 runtime._LostContendedRuntimeLock。可以使用 runtimecontentionstacks 設定啟用執行時鎖的完整堆疊跟蹤。這些堆疊跟蹤具有非標準語義,詳見設定文件。

Go 1.22 添加了一個新的 crypto/x509.Certificate 欄位 Policies,它支援元件大於 31 位的證書策略 OID。預設情況下,此欄位僅在解析時使用(此時它被填充策略 OID),而在編組時則不使用。可以使用 x509usepolicies setting. 來編組這些較大的 OID,而不是使用現有的 PolicyIdentifiers 欄位。

Go 1.21

Go 1.21 規定使用 nil 介面值呼叫 panic 為執行時錯誤,由 panicnil 設定控制。

Go 1.21 規定 html/template action 出現在 ECMAScript 6 模板字面量內部是錯誤,由 jstmpllitinterp 設定控制。此行為已向後移植到 Go 1.19.8+ 和 Go 1.20.3+。

Go 1.21 引入了 MIME 頭部和多部分表單最大數量的限制,分別由 multipartmaxheadersmultipartmaxparts 設定控制。此行為已向後移植到 Go 1.19.8+ 和 Go 1.20.3+。

Go 1.21 添加了對 Multipath TCP 的支援,但只有應用程式顯式請求時才使用。此行為可以透過 multipathtcp 設定控制。

沒有計劃移除這些設定中的任何一個。

Go 1.20

Go 1.20 引入了拒絕 tar 和 zip 歸檔中不安全路徑的支援,由 tarinsecurepath 設定zipinsecurepath 設定控制。它們預設設定為 tarinsecurepath=1zipinsecurepath=1,保留了早期 Go 版本的行為。未來的 Go 版本可能會將預設值更改為 tarinsecurepath=0zipinsecurepath=0

Go 1.20 引入了 math/rand 全域性隨機數生成器的自動種子設定,由 randautoseed 設定控制。

Go 1.20 引入了在證書驗證期間使用的回退根 (fallback roots) 的概念,由 x509usefallbackroots 設定控制。

Go 1.20 從 Go 分發版中移除了預安裝的標準庫 .a 檔案。現在安裝程式會像處理其他模組中的包一樣構建和快取標準庫。installgoroot 設定會恢復預安裝 .a 檔案的安裝和使用。

沒有計劃移除這些設定中的任何一個。

Go 1.19

Go 1.19 規定路徑查詢解析到當前目錄中的二進位制檔案是錯誤,由 execerrdot 設定控制。沒有計劃移除此設定。

Go 1.19 開始在 DNS 請求中傳送 EDNS0 附加頭。據報告這可能導致某些路由器(例如 CenturyLink Zyxel C3000Z)提供的 DNS 伺服器出現問題。這可以透過 netedns0 設定更改。此設定在 Go 1.21.12、Go 1.22.5、Go 1.23 及更高版本中可用。沒有計劃移除此設定。

Go 1.18

Go 1.18 移除了對大多數 X.509 證書中 SHA1 的支援,由 x509sha1 設定控制。此設定已在 Go 1.24 中移除。

Go 1.10

Go 1.10 改變了構建快取的工作方式,並添加了測試快取,以及 gocacheverifygocachehashgocachetest 設定。沒有計劃移除這些設定。

Go 1.6

Go 1.6 引入了對 HTTP/2 的透明支援,由 http2clienthttp2serverhttp2debug 設定控制。沒有計劃移除這些設定。

Go 1.5

Go 1.5 引入了一個純 Go DNS 解析器,由 netdns 設定控制。沒有計劃移除此設定。