常見問題 (FAQ)

起源

這個專案的目的是什麼?

Go 語言在 2007 年誕生時,程式設計世界與今天截然不同。生產軟體通常用 C++ 或 Java 編寫,GitHub 尚不存在,大多數計算機尚未多處理器化,除了 Visual Studio 和 Eclipse 之外,幾乎沒有其他 IDE 或高階工具可用,更不用說在網際網路上免費獲得。

與此同時,我們對使用現有語言及其關聯的構建系統構建大型軟體專案所需的過度複雜性感到沮喪。自 C、C++ 和 Java 等語言首次開發以來,計算機的速度已經大大加快,但程式設計行為本身並沒有取得同樣大的進步。此外,很明顯多處理器正在普及,但大多數語言在高效安全地程式設計方面提供的幫助很少。

我們決定退一步思考,隨著技術的發展,未來幾年主導軟體工程的主要問題是什麼,以及一門新語言如何幫助解決這些問題。例如,多核 CPU 的興起表明語言應該為某種併發或並行提供一流的支援。為了在大規模併發程式中使資源管理易於處理,需要垃圾回收,或者至少某種安全的自動記憶體管理。

這些考慮導致了一系列討論,Go 語言由此而生,首先是一組想法和期望,然後是一種語言。一個首要目標是 Go 語言透過啟用工具、自動化程式碼格式化等日常任務以及消除處理大型程式碼庫的障礙,更好地幫助工作程式設計師。

關於 Go 語言目標的更詳細描述以及它們如何實現或至少接近實現,請參閱文章《Google 的 Go 語言:服務於軟體工程的語言設計》。

這個專案的歷史是什麼?

Robert Griesemer、Rob Pike 和 Ken Thompson 於 2007 年 9 月 21 日在白板上開始草擬一門新語言的目標。幾天之內,目標就確定為一個行動計劃,並且對它會是什麼樣有了一個大致的瞭解。設計工作與無關的工作並行進行。到 2008 年 1 月,Ken 開始著手開發一個編譯器來探索想法;它生成 C 程式碼作為輸出。到年中,該語言已成為一個全職專案,並且已經穩定到可以嘗試生產編譯器。2008 年 5 月,Ian Taylor 獨立地開始使用草案規範開發 Go 的 GCC 前端。Russ Cox 於 2008 年底加入,並幫助將語言和庫從原型變為現實。

Go 於 2009 年 11 月 10 日成為一個公共開源專案。社群中無數人貢獻了想法、討論和程式碼。

現在全世界有數百萬 Go 程式設計師——Gopher——而且每天都在增加。Go 的成功遠遠超出了我們的預期。

Gopher 吉祥物的起源是什麼?

吉祥物和標誌由Renée French設計,她也設計了 Plan 9 的兔子Glenda。一篇關於 Gopher 的部落格文章解釋了它是如何從她多年前為WFMU T 恤設計所使用的一個派生出來的。標誌和吉祥物受知識共享署名 4.0 許可協議的保護。

Gopher 有一個模型圖,說明了它的特徵以及如何正確地表現它們。模型圖首次在 Renée 於 2016 年 Gophercon 上的一次演講中展示。它具有獨特的特徵;它是 Go Gopher,而不是任何一隻普通的 Gopher。

這門語言叫 Go 還是 Golang?

這門語言叫 Go。“golang”這個名稱的出現是因為網站最初是 golang.org。(當時還沒有 .dev 域名。)不過,許多人使用 golang 這個名稱,它作為一個標籤很方便。例如,該語言的社交媒體標籤是“#golang”。無論如何,該語言的名稱就是 Go。

附註:雖然官方標誌有兩個大寫字母,但語言名稱寫為 Go,而不是 GO。

你們為什麼建立一門新語言?

Go 語言的誕生源於我們對在 Google 所做工作中使用現有語言和環境感到沮喪。程式設計變得太困難了,而語言的選擇是部分原因。人們必須選擇高效的編譯、高效的執行或易於程式設計;這三者不能在同一種主流語言中同時獲得。能夠做到這一點的程式設計師透過轉向 Python 和 JavaScript 等動態型別語言,而不是 C++ 或(程度較輕的)Java,來選擇易用性而非安全性與效率。

我們並非唯一有此擔憂的人。在程式語言領域經歷了多年平靜之後,Go 是幾門新語言(Rust、Elixir、Swift 等)中的第一批,這些語言使程式語言開發再次成為一個活躍的、幾乎主流的領域。

Go 語言透過嘗試將解釋型、動態型別語言的程式設計便利性與靜態型別、編譯型語言的效率和安全性結合起來,解決了這些問題。它還旨在更好地適應當前硬體,支援網路和多核計算。最後,使用 Go 的目標是 快速:在單臺計算機上構建一個大型可執行檔案最多隻需幾秒鐘。實現這些目標促使我們重新思考現有語言中的一些程式設計方法,從而產生了:一種組合而非分層的型別系統;對併發和垃圾回收的支援;嚴格的依賴規範;等等。這些不能透過庫或工具很好地處理;因此需要一門新語言。

文章《Google 的 Go 語言》討論了 Go 語言設計的背景和動機,並提供了本 FAQ 中許多答案的更詳細資訊。

Go 的祖先是誰?

Go 主要屬於 C 家族(基本語法),並從 Pascal/Modula/Oberon 家族(宣告、包)中獲得了大量輸入,還借鑑了受 Tony Hoare 的 CSP 啟發的語言,如 Newsqueak 和 Limbo(併發)的一些想法。然而,它是一門全新的語言。在各個方面,這門語言的設計都考慮了程式設計師的工作方式,以及如何使程式設計(至少是我們所做的程式設計)更有效,這意味著更有趣。

設計的指導原則是什麼?

Go 語言設計時,Java 和 C++ 是編寫伺服器最常用的語言,至少在 Google 是這樣。我們覺得這些語言需要太多的簿記和重複。一些程式設計師透過轉向更動態、更流暢的語言,如 Python,來應對,但代價是效率和型別安全。我們認為應該可以在一門語言中同時擁有效率、安全性和流暢性。

Go 嘗試在兩種意義上減少“打字”的量。在整個設計過程中,我們一直努力減少混亂和複雜性。沒有前向宣告,也沒有標頭檔案;所有內容都只宣告一次。初始化具有表現力、自動化且易於使用。語法簡潔,關鍵字少。透過使用 := 宣告並初始化構造的簡單型別推導,減少了重複(foo.Foo* myFoo = new(foo.Foo))。也許最根本的是,沒有型別層次結構:型別就是型別,它們不必宣告它們之間的關係。這些簡化使 Go 語言既富有表現力又易於理解,同時又不犧牲生產力。

另一個重要原則是保持概念的正交性。方法可以為任何型別實現;結構體表示資料,而介面表示抽象;等等。正交性使理解事物組合時發生的情況變得更容易。

用法

Google 內部使用 Go 嗎?

是的。Go 在 Google 內部廣泛用於生產環境。一個例子是 Google 的下載伺服器 dl.google.com,它提供 Chrome 二進位制檔案和其他大型可安裝檔案,例如 apt-get 包。

Go 並不是 Google 唯一使用的語言,遠非如此,但它在許多領域都是一門關鍵語言,包括站點可靠性工程 (SRE) 和大規模資料處理。它也是執行 Google Cloud 的軟體的關鍵組成部分。

還有哪些公司使用 Go?

Go 的使用在全球範圍內不斷增長,尤其是在雲計算領域,但絕不僅限於此。一些用 Go 編寫的主要雲基礎設施專案是 Docker 和 Kubernetes,但還有很多。

然而,不僅僅是雲,正如您可以在 go.dev 網站上的公司列表以及一些成功案例中看到的那樣。此外,Go Wiki 還包含一個定期更新的頁面,列出了許多使用 Go 的公司。

Wiki 還有一個頁面,連結到更多關於使用該語言的公司和專案的成功案例

在同一個地址空間中同時使用 C 和 Go 是可能的,但它不是一個自然的選擇,並且可能需要特殊的介面軟體。此外,將 C 與 Go 程式碼連結會放棄 Go 提供的記憶體安全和堆疊管理特性。有時,使用 C 庫來解決問題是絕對必要的,但這樣做總是會引入純 Go 程式碼中不存在的風險因素,因此請謹慎行事。

如果您確實需要在 Go 中使用 C,如何操作取決於 Go 編譯器的實現。Go 團隊在 Google 支援的 Go 工具鏈中的“標準”編譯器稱為 gc。此外,還有基於 GCC 的編譯器 (gccgo) 和基於 LLVM 的編譯器 (gollvm),以及越來越多服務於不同目的的特殊編譯器列表,有時實現語言子集,例如TinyGo

Gc 使用與 C 不同的呼叫約定和連結器,因此不能直接從 C 程式呼叫,反之亦然。cgo 程式提供了“外部函式介面”的機制,允許從 Go 程式碼安全呼叫 C 庫。SWIG 將此功能擴充套件到 C++ 庫。

您還可以將 cgo 和 SWIG 與 gccgogollvm 一起使用。由於它們使用傳統的 ABI,因此在非常小心的情況下,也可以將這些編譯器的程式碼直接與 GCC/LLVM 編譯的 C 或 C++ 程式連結。但是,安全地這樣做需要了解所有相關語言的呼叫約定,以及在從 Go 呼叫 C 或 C++ 時對堆疊限制的關注。

Go 支援哪些 IDE?

Go 專案不包含自定義 IDE,但語言和庫的設計使其易於分析原始碼。因此,大多數知名的編輯器和 IDE 都很好地支援 Go,無論是直接支援還是透過外掛支援。

Go 團隊還支援 LSP 協議的 Go 語言伺服器,名為 gopls。支援 LSP 的工具可以使用 gopls 整合特定於語言的支援。

提供良好 Go 支援的知名 IDE 和編輯器列表包括 Emacs、Vim、VSCode、Atom、Eclipse、Sublime、IntelliJ(透過名為 GoLand 的自定義變體)等等。您的首選環境很可能是一個用於 Go 程式設計的生產力環境。

Go 支援 Google 的協議緩衝區嗎?

一個獨立的開源專案提供了必要的編譯器外掛和庫。它可以在github.com/golang/protobuf/找到。

設計

Go 有執行時嗎?

Go 有一個龐大的執行時庫,通常簡稱為 執行時,它是每個 Go 程式的一部分。這個庫實現了垃圾回收、併發、棧管理以及 Go 語言的其他關鍵特性。雖然它對語言更核心,但 Go 的執行時類似於 C 庫 libc

然而,重要的是要理解,Go 的執行時不包括虛擬機器,例如 Java 執行時提供的虛擬機器。Go 程式是提前編譯成本機機器碼(或 JavaScript 或 WebAssembly,對於某些變體實現)。因此,儘管該術語通常用於描述程式執行的虛擬環境,但在 Go 中,“執行時”一詞只是為提供關鍵語言服務的庫的名稱。

Unicode 識別符號是怎麼回事?

在設計 Go 時,我們希望確保它不會過於以 ASCII 為中心,這意味著要將識別符號的範圍從 7 位 ASCII 的限制擴充套件開來。Go 的規則——識別符號字元必須是 Unicode 定義的字母或數字——簡單易懂且易於實現,但也有其限制。例如,組合字元被設計排除在外,這排除了某些語言,如天城文。

這條規則還有一個不幸的後果。由於匯出的識別符號必須以大寫字母開頭,因此由某些語言中的字元建立的識別符號,根據定義,無法匯出。目前唯一的解決方案是使用類似 X日本語 的東西,這顯然不能令人滿意。

自語言的早期版本以來,人們一直在深入思考如何最好地擴充套件識別符號空間以適應使用其他母語的程式設計師。具體應該怎麼做仍然是一個活躍的討論話題,並且語言的未來版本可能會對識別符號的定義更加寬鬆。例如,它可能會採納 Unicode 組織關於識別符號的建議中的一些想法。無論發生什麼,都必須在相容的情況下完成,同時保留(或者可能擴充套件)字母大小寫決定識別符號可見性的方式,這仍然是 Go 語言我們最喜歡的特性之一。

目前,我們有一個簡單的規則,將來可以擴充套件而不會破壞程式,這個規則避免了由於允許歧義識別符號的規則而肯定會出現的錯誤。

Go 為什麼沒有特性 X?

每種語言都包含新穎的特性,並省略了某人最喜歡的特性。Go 的設計著眼於程式設計的愉悅性、編譯速度、概念的正交性以及支援併發和垃圾回收等特性的需求。您最喜歡的特性可能缺失,因為它不適合,因為它影響了編譯速度或設計的清晰度,或者因為它會使基本系統模型過於複雜。

如果 Go 缺少特性 X 讓您感到困擾,請原諒我們,並研究 Go 所擁有的特性。您可能會發現它們以有趣的方式彌補了 X 的缺失。

Go 是什麼時候獲得泛型型別的?

Go 1.18 版本為語言添加了型別引數。這允許一種多型或泛型程式設計形式。有關詳細資訊,請參閱語言規範提案

Go 最初發布時為什麼沒有泛型型別?

Go 旨在成為一種編寫伺服器程式的語言,易於長期維護。(有關更多背景資訊,請參閱這篇文章。)設計集中於可伸縮性、可讀性和併發性等方面。當時,多型程式設計似乎對語言目標並非必不可少,因此最初為了簡單起見而省略了。

泛型很方便,但它們會增加型別系統和執行時的複雜性。我們花了一些時間才開發出一種設計,我們認為它的價值與複雜性成正比。

Go 為什麼沒有異常?

我們認為將異常與控制結構(如 try-catch-finally 慣用法)耦合會導致程式碼變得複雜。它還傾向於鼓勵程式設計師將太多普通錯誤(例如檔案開啟失敗)標記為異常。

Go 採取了不同的方法。對於普通的錯誤處理,Go 的多值返回使其易於報告錯誤,而不會使返回值過載。規範的錯誤型別,加上 Go 的其他特性,使錯誤處理變得愉快,但與其他語言截然不同。

Go 還有一些內建函式可以發出訊號並從真正異常情況中恢復。恢復機制僅作為函式在錯誤後狀態被拆除的一部分執行,這足以處理災難,但不需要額外的控制結構,並且如果使用得當,可以產生清晰的錯誤處理程式碼。

有關詳細資訊,請參閱《Defer、Panic 和 Recover》一文。此外,錯誤是值這篇部落格文章描述了一種在 Go 中乾淨處理錯誤的方法,透過演示由於錯誤只是值,Go 的全部功能可以用於錯誤處理。

Go 為什麼沒有斷言?

Go 不提供斷言。它們無疑很方便,但我們的經驗是程式設計師將它們用作柺杖,以避免考慮正確的錯誤處理和報告。正確的錯誤處理意味著伺服器在非致命錯誤後繼續執行而不是崩潰。正確的錯誤報告意味著錯誤是直接而切中要害的,使程式設計師不必解釋大量的崩潰跟蹤。當看到錯誤的程式設計師不熟悉程式碼時,精確的錯誤尤為重要。

我們理解這是一個爭議點。Go 語言和庫中的許多內容都與現代實踐不同,僅僅是因為我們認為有時嘗試不同的方法是值得的。

為什麼要基於 CSP 的思想構建併發?

併發和多執行緒程式設計隨著時間的推移獲得了難以掌握的聲譽。我們認為這部分是由於複雜的設計,如pthreads,部分是由於過度強調互斥鎖、條件變數和記憶體屏障等低階細節。更高級別的介面可以實現更簡單的程式碼,即使底層仍然存在互斥鎖等。

提供高階併發語言支援最成功的模型之一來自 Hoare 的通訊順序程序(CSP)。Occam 和 Erlang 是兩種源於 CSP 的著名語言。Go 的併發原語來自家族樹的不同部分,其主要貢獻是作為一流物件的通道的強大概念。使用幾種早期語言的經驗表明,CSP 模型非常適合過程語言框架。

為什麼是 Goroutine 而不是執行緒?

Goroutine 是使併發易於使用的一部分。這個想法已經存在了一段時間,即將獨立執行的函式——協程——複用到一組執行緒上。當協程阻塞時,例如透過呼叫阻塞系統呼叫,執行時會自動將同一作業系統執行緒上的其他協程移動到另一個可執行的執行緒上,這樣它們就不會被阻塞。程式設計師看不到這一切,這就是重點。我們稱之為 Goroutine 的結果可以非常便宜:它們除了堆疊的記憶體(只有幾千位元組)之外,幾乎沒有額外的開銷。

為了使堆疊小,Go 的執行時使用可調整大小的、有界堆疊。一個新建立的 Goroutine 會獲得幾千位元組,這幾乎總是足夠的。當不夠時,執行時會自動增長(和收縮)用於儲存堆疊的記憶體,從而允許許多 Goroutine 佔用適量的記憶體。CPU 開銷平均每次函式呼叫大約三個便宜的指令。在同一個地址空間中建立數十萬個 Goroutine 是可行的。如果 Goroutine 只是執行緒,系統資源很快就會耗盡。

為什麼 map 操作沒有定義為原子操作?

經過長時間討論,我們決定 map 的典型使用不需要從多個 Goroutine 進行安全訪問,並且在需要的情況下,map 可能已經是某個更大資料結構或計算的一部分,該資料結構或計算已經同步。因此,要求所有 map 操作都獲取互斥鎖會減慢大多數程式的執行速度,並且幾乎不會增加安全性。然而,這不是一個容易的決定,因為它意味著不受控制的 map 訪問可能會使程式崩潰。

該語言不排除原子 map 更新。當需要時,例如託管不受信任的程式時,實現可以互鎖 map 訪問。

Map 訪問只有在發生更新時才不安全。只要所有 Goroutine 都只是讀取——在 map 中查詢元素,包括使用 for range 迴圈遍歷它——並且不透過向元素賦值或執行刪除來更改 map,它們就可以安全地併發訪問 map 而無需同步。

為了幫助正確使用 map,該語言的一些實現包含一個特殊檢查,當 map 被併發執行不安全地修改時,它會在執行時自動報告。此外,sync 庫中有一種名為 sync.Map 的型別,它適用於某些使用模式,例如靜態快取,儘管它不適合作為內建 map 型別的通用替代品。

你們會接受我的語言更改嗎?

人們經常提出對語言的改進建議——郵件列表中包含了大量此類討論的歷史記錄——但其中很少有被接受的更改。

儘管 Go 是一個開源專案,但該語言和庫受到相容性承諾的保護,該承諾禁止會破壞現有程式的更改,至少在原始碼級別是這樣(程式可能需要偶爾重新編譯以保持最新)。如果您的提案違反 Go 1 規範,我們甚至不會考慮這個想法,無論其優點如何。Go 的未來主要版本可能與 Go 1 不相容,但關於該主題的討論才剛剛開始,有一點是肯定的:在此過程中引入的不相容性將非常少。此外,相容性承諾鼓勵我們為舊程式提供一條自動向前發展的路徑,以適應這種情況的發生。

即使您的提案與 Go 1 規範相容,它也可能不符合 Go 的設計目標。文章《Google 的 Go 語言:服務於軟體工程的語言設計》解釋了 Go 的起源和其設計背後的動機。

型別

Go 是一門面向物件的語言嗎?

是也不是。雖然 Go 擁有型別和方法,並允許面向物件的程式設計風格,但它沒有型別層次結構。Go 中的“介面”概念提供了一種不同的方法,我們相信它易於使用,並且在某些方面更通用。還有一些方法可以將型別嵌入到其他型別中,以提供類似於(但不完全相同)子類化的功能。此外,Go 中的方法比 C++ 或 Java 中的更通用:它們可以為任何型別的資料定義,甚至是像普通“未裝箱”整數這樣的內建型別。它們不限於結構體(類)。

此外,缺乏型別層次結構使得 Go 中的“物件”比 C++ 或 Java 等語言中的“物件”感覺更輕量級。

如何實現方法的動態排程?

實現動態排程方法的唯一方法是透過介面。結構體或任何其他具體型別上的方法總是靜態解析的。

為什麼沒有型別繼承?

面向物件程式設計,至少在最著名的語言中,涉及過多關於型別之間關係的討論,而這些關係通常可以自動推匯出來。Go 採取了不同的方法。

Go 不需要程式設計師提前宣告兩種型別之間存在關係,而是型別自動滿足任何指定其方法子集的介面。除了減少簿記之外,這種方法還具有真正的優勢。型別可以一次滿足多個介面,而沒有傳統多重繼承的複雜性。介面可以非常輕量級——一個擁有一個甚至零個方法的介面可以表達一個有用的概念。如果出現新想法或為了測試,可以在事後新增介面——而無需註釋原始型別。由於型別和介面之間沒有明確的關係,因此沒有要管理或討論的型別層次結構。

可以使用這些思想來構建類似於型別安全的 Unix 管道。例如,看看 fmt.Fprintf 如何將格式化列印輸出到任何輸出,而不僅僅是檔案,或者 bufio 包如何完全獨立於檔案 I/O,或者 image 包如何生成壓縮影像檔案。所有這些思想都源於一個單一介面 (io.Writer),它表示一個單一方法 (Write)。而這只是冰山一角。Go 的介面對程式的結構方式產生了深遠的影響。

這需要一些時間來適應,但這種隱式型別依賴風格是 Go 最具生產力的地方之一。

為什麼 len 是函式而不是方法?

我們討論過這個問題,但最終認為將 len 和其相關函式實現為函式在實踐中沒問題,並且沒有使基本型別的介面(Go 型別意義上的介面)問題複雜化。

Go 為什麼不支援方法和運算子過載?

如果方法排程不需要同時進行型別匹配,那麼它就簡化了。與其他語言的經驗告訴我們,擁有各種同名但不同簽名的方法偶爾有用,但在實踐中也可能令人困惑和脆弱。僅透過名稱匹配並要求型別一致性是 Go 型別系統中的一個主要簡化決策。

關於運算子過載,它似乎更多是一種便利而不是絕對要求。同樣,沒有它事情會更簡單。

Go 為什麼沒有“implements”宣告?

Go 型別透過實現介面的方法來實現介面,僅此而已。這個特性允許在不修改現有程式碼的情況下定義和使用介面。它實現了一種結構化型別,促進了關注點分離並提高了程式碼重用,並使得更容易在程式碼開發過程中出現的模式上進行構建。介面的語義是 Go 靈活、輕量級感覺的主要原因之一。

有關更多詳細資訊,請參閱關於型別繼承的問題

如何保證我的型別滿足介面?

你可以要求編譯器檢查型別 T 是否實現了介面 I,方法是嘗試使用 T 的零值或指向 T 的指標進行賦值,視情況而定:

type T struct{}
var _ I = T{}       // Verify that T implements I.
var _ I = (*T)(nil) // Verify that *T implements I.

如果 T(或 *T,相應地)沒有實現 I,那麼錯誤將在編譯時被捕獲。

如果你希望介面的使用者明確宣告他們實現了它,你可以給介面的方法集新增一個具有描述性名稱的方法。例如:

type Fooer interface {
    Foo()
    ImplementsFooer()
}

然後,型別必須實現 ImplementsFooer 方法才能成為 Fooer,這明確地記錄了這一事實,並在 go doc 的輸出中宣佈了它。

type Bar struct{}
func (b Bar) ImplementsFooer() {}
func (b Bar) Foo() {}

大多數程式碼不使用此類約束,因為它們限制了介面概念的實用性。但是,有時它們是解決相似介面之間歧義所必需的。

為什麼型別 T 不滿足 Equal 介面?

考慮這個簡單的介面,它表示一個可以與另一個值進行比較的物件:

type Equaler interface {
    Equal(Equaler) bool
}

以及這個型別 T

type T int
func (t T) Equal(u T) bool { return t == u } // does not satisfy Equaler

與某些多型型別系統中的類似情況不同,T 不實現 EqualerT.Equal 的引數型別是 T,而不是字面意義上所需的型別 Equaler

在 Go 中,型別系統不會提升 Equal 的引數;這是程式設計師的責任,如實現了 Equaler 的型別 T2 所示:

type T2 int
func (t T2) Equal(u Equaler) bool { return t == u.(T2) }  // satisfies Equaler

然而,這甚至不像其他型別系統,因為在 Go 中,任何滿足 Equaler 的型別都可以作為引數傳遞給 T2.Equal,並且在執行時我們必須檢查該引數是否為 T2 型別。某些語言會安排在編譯時做出這種保證。

一個相關的例子是另一種情況:

type Opener interface {
   Open() Reader
}

func (t T3) Open() *os.File

在 Go 中,T3 不滿足 Opener,儘管它在其他語言中可能滿足。

雖然 Go 的型別系統在這種情況下為程式設計師做的工作確實較少,但缺少子型別使得關於介面滿足的規則非常容易說明:函式的名稱和簽名是否與介面完全相同?Go 的規則也很容易高效實現。我們認為這些好處抵消了缺乏自動型別提升的缺點。

我可以將 []T 轉換為 []interface{} 嗎?

不能直接轉換。語言規範禁止這樣做,因為這兩種型別在記憶體中沒有相同的表示。有必要將元素逐個複製到目標切片中。此示例將 int 切片轉換為 interface{} 切片:

t := []int{1, 2, 3, 4}
s := make([]interface{}, len(t))
for i, v := range t {
    s[i] = v
}

如果 T1 和 T2 具有相同的底層型別,我可以將 []T1 轉換為 []T2 嗎?

這段程式碼的最後一行無法編譯。

type T1 int
type T2 int
var t1 T1
var x = T2(t1) // OK
var st1 []T1
var sx = ([]T2)(st1) // NOT OK

在 Go 中,型別與方法緊密關聯,每個命名型別都有一個(可能為空的)方法集。一般規則是您可以更改正在轉換的型別的名稱(從而可能更改其方法集),但不能更改複合型別的元素的名稱(和方法集)。Go 要求您明確指定型別轉換。

為什麼我的 nil 錯誤值不等於 nil?

在底層,介面由兩個元素實現:一個型別 T 和一個值 VV 是一個具體值,例如 intstruct 或指標,它本身永遠不是介面,並且具有型別 T。例如,如果我們將 int 值 3 儲存在一個介面中,則生成的介面值在示意圖上具有 (T=int, V=3)。值 V 也被稱為介面的 動態 值,因為給定的介面變數在程式執行期間可能持有不同的值 V(和相應的型別 T)。

介面值僅在 VT 都未設定時才為 nil,(T=nil, V 未設定)。特別是,nil 介面將始終持有一個 nil 型別。如果我們將型別為 *intnil 指標儲存在介面值中,則內部型別將是 *int,無論指標的值如何:(T=*int, V=nil)。因此,即使 內部的指標值 V nil,這樣的介面值也將是非 nil 的。

這種情況可能令人困惑,並且在將 nil 值儲存在介面值(例如 error 返回值)中時會出現:

func returnsError() error {
    var p *MyError = nil
    if bad() {
        p = ErrBad
    }
    return p // Will always return a non-nil error.
}

如果一切順利,函式返回一個 nil p,因此返回值為一個持有 (T=*MyError, V=nil) 的 error 介面值。這意味著如果呼叫者將返回的錯誤與 nil 進行比較,即使沒有發生任何壞事,它也總是看起來像發生了錯誤。要向呼叫者返回一個正確的 nil error,函式必須返回一個顯式的 nil

func returnsError() error {
    if bad() {
        return ErrBad
    }
    return nil
}

對於返回錯誤的函式,最好始終在其簽名中使用 error 型別(如我們上面所做),而不是具體型別(如 *MyError),以幫助保證錯誤正確建立。例如,os.Open 返回一個 error,即使它不為 nil,它也始終是具體型別 *os.PathError

無論何時使用介面,都可能出現與此處描述的類似情況。請記住,如果介面中儲存了任何具體值,則介面將不是 nil。有關更多資訊,請參閱《反射定律》。

為什麼零大小型別行為異常?

Go 支援零大小型別,例如沒有欄位的結構體 (struct{}) 或沒有元素的陣列 ([0]byte)。零大小型別中不能儲存任何值,但這些型別在不需要值時有時很有用,例如在 map[int]struct{} 中或具有方法但沒有值的型別中。

具有零大小型別的不同變數可能位於記憶體中的相同位置。這是安全的,因為這些變數中不能儲存任何值。

此外,該語言不保證指向兩個不同零大小變數的指標是否會比較相等。這種比較甚至可能在程式的某個點返回 true,然後在不同的點返回 false,具體取決於程式的編譯和執行方式。

零大小型別的一個單獨問題是,指向零大小結構體欄位的指標不得與記憶體中不同物件的指標重疊。這可能會導致垃圾回收器混淆。這意味著,如果結構體中的最後一個欄位是零大小的,則結構體將進行填充,以確保指向最後一個欄位的指標不與緊隨結構體之後的記憶體重疊。因此,這個程式:

func main() {
    type S struct {
        f1 byte
        f2 struct{}
    }
    fmt.Println(unsafe.Sizeof(S{}))
}

在大多數 Go 實現中會列印 2,而不是 1

為什麼沒有像 C 那樣的未標記聯合體?

未標記聯合會違反 Go 的記憶體安全保證。

Go 為什麼沒有變體型別?

變體型別,也稱為代數型別,提供了一種指定值可能採用一組其他型別中某一種(但僅限於這些型別)的方式。系統程式設計中的一個常見示例是指定錯誤是網路錯誤、安全錯誤還是應用程式錯誤,並允許呼叫者透過檢查錯誤的型別來區分問題的來源。另一個示例是語法樹,其中每個節點可以是不同的型別:宣告、語句、賦值等。

我們考慮過將變體型別新增到 Go 中,但經過討論後決定將其排除,因為它們與介面以令人困惑的方式重疊。如果變體型別的元素本身是介面,會發生什麼?

此外,變體型別所解決的一些問題已經由語言涵蓋。錯誤示例使用介面值來儲存錯誤和型別 switch 來區分情況很容易表達。語法樹示例也可以實現,儘管不夠優雅。

Go 為什麼沒有協變結果型別?

協變結果型別意味著像下面這樣的介面:

type Copyable interface {
    Copy() interface{}
}

會被方法滿足:

func (v Value) Copy() Value

因為 Value 實現了空介面。在 Go 中,方法型別必須完全匹配,因此 Value 不實現 Copyable。Go 將型別的功能——它的方法——與型別的實現分開。如果兩個方法返回不同的型別,它們做的事情就不一樣。想要協變結果型別的程式設計師通常試圖透過介面來表達型別層次結構。在 Go 中,介面和實現之間有清晰的分離更自然。

Go 為什麼不提供隱式數值轉換?

C 語言中數值型別之間自動轉換的便利性被其造成的困惑所抵消。表示式何時是無符號的?值有多大?它溢位嗎?結果是否可移植,獨立於其執行的機器?它還使編譯器複雜化;C 的“常規算術轉換”不容易實現,並且在不同架構上不一致。出於可移植性考慮,我們決定以程式碼中一些顯式轉換的代價,使事情清晰明瞭。然而,Go 中常量的定義——任意精度值,沒有符號和大小注解——大大改善了這種情況。

一個相關的細節是,與 C 不同,intint64 是不同的型別,即使 int 是 64 位型別。int 型別是通用的;如果您關心整數包含多少位,Go 鼓勵您明確說明。

Go 中的常量是如何工作的?

儘管 Go 對不同數值型別變數之間的轉換要求嚴格,但語言中的常量要靈活得多。字面常量,例如 233.14159math.Pi,佔據了一種理想的數字空間,具有任意精度,沒有溢位或下溢。例如,math.Pi 的值在原始碼中指定為 63 位小數,涉及該值的常量表達式保持的精度超出了 float64 所能容納的範圍。只有當常量或常量表達式被賦值給一個變數——程式中的一個記憶體位置——它才會成為一個具有通常浮點特性和精度的“計算機”數字。

此外,由於它們只是數字,而不是型別值,Go 中的常量可以比變數更自由地使用,從而緩解了嚴格轉換規則帶來的一些不便。可以編寫如下表達式:

sqrt2 := math.Sqrt(2)

編譯器不會抱怨,因為理想數字 2 可以安全準確地轉換為 float64 以呼叫 math.Sqrt

一篇題為《常量》的部落格文章更詳細地探討了此主題。

為什麼 map 是內建的?

原因與字串相同:它們是如此強大和重要的資料結構,以至於提供一個優秀的實現並支援語法會使程式設計更加愉快。我們相信 Go 的 map 實現足夠強大,可以滿足絕大多數用途。如果特定應用程式可以從自定義實現中受益,則可以編寫一個,但它在語法上不會那麼方便;這似乎是一個合理的權衡。

為什麼 map 不允許切片作為鍵?

Map 查詢需要一個相等運算子,而切片不實現此運算子。它們不實現相等,因為相等在這種型別上沒有很好的定義;存在多種考慮因素,包括淺層與深層比較、指標與值比較、如何處理遞迴型別等等。我們可能會重新審視這個問題——為切片實現相等不會使任何現有程式失效——但如果沒有明確的切片相等含義,現在將其排除在外會更簡單。

結構體和陣列定義了相等,因此它們可以用作 map 鍵。

為什麼 map、切片和通道是引用,而陣列是值?

關於這個話題有很多歷史。早期,map 和通道在語法上是指標,並且無法宣告或使用非指標例項。此外,我們還在為陣列如何工作而苦惱。最終,我們決定嚴格區分指標和值使語言更難使用。將這些型別更改為充當相關共享資料結構的引用解決了這些問題。此更改為語言增加了一些令人遺憾的複雜性,但對可用性產生了巨大影響:當它引入時,Go 成為一種更具生產力、更舒適的語言。

編寫程式碼

庫是如何文件化的?

要從命令列訪問文件,go 工具有一個 doc 子命令,它為宣告、檔案、包等提供文字介面。

全域性包發現頁面 pkg.go.dev/pkg/ 執行一個伺服器,該伺服器從網路上任何地方的 Go 原始碼中提取包文件,並將其作為 HTML 提供,其中包含指向宣告和相關元素的連結。這是瞭解現有 Go 庫最簡單的方法。

在專案早期,有一個類似的程式 godoc,也可以執行它來提取本地機器上檔案的文件;pkg.go.dev/pkg/ 本質上是它的一個後代。另一個後代是 pkgsite 命令,它像 godoc 一樣可以在本地執行,儘管它尚未整合到 go doc 顯示的結果中。

Go 有程式設計風格指南嗎?

沒有明確的風格指南,儘管確實存在可識別的“Go 風格”。

Go 已經建立了一些約定來指導命名、佈局和檔案組織方面的決策。文件《Effective Go》包含一些關於這些主題的建議。更直接地說,gofmt 程式是一個美化列印器,其目的是強制執行佈局規則;它取代了通常允許解釋的“應該做和不應該做”的彙編。倉庫中的所有 Go 程式碼,以及開源世界中的絕大多數 Go 程式碼,都經過了 gofmt 處理。

題為《Go 程式碼審查評論》的文件是關於 Go 慣用法的細節的非常短的文章集合,這些細節經常被程式設計師忽略。它是 Go 專案程式碼審查人員的便捷參考。

我如何向 Go 庫提交補丁?

庫的原始碼在倉庫的 src 目錄下。如果您想進行重大更改,請在開始之前在郵件列表上進行討論。

有關如何操作的更多資訊,請參閱文件《為 Go 專案做貢獻》。

為什麼 “go get” 克隆倉庫時使用 HTTPS?

公司通常只允許在標準 TCP 埠 80 (HTTP) 和 443 (HTTPS) 上進行出站流量,而阻止其他埠(包括 TCP 埠 9418 (git) 和 TCP 埠 22 (SSH))上的出站流量。當使用 HTTPS 而不是 HTTP 時,git 預設強制執行證書驗證,從而提供針對中間人、竊聽和篡改攻擊的保護。因此,go get 命令為了安全起見使用 HTTPS。

Git 可以配置為透過 HTTPS 進行身份驗證或使用 SSH 代替 HTTPS。要透過 HTTPS 進行身份驗證,您可以向 git 查詢的 $HOME/.netrc 檔案新增一行:

machine github.com login *USERNAME* password *APIKEY*

對於 GitHub 賬戶,密碼可以是個人訪問令牌

Git 還可以配置為對匹配給定字首的 URL 使用 SSH 代替 HTTPS。例如,要對所有 GitHub 訪問使用 SSH,請將這些行新增到您的 ~/.gitconfig

[url "ssh://git@github.com/"]
    insteadOf = https://github.com/

在使用私有模組,但對依賴項使用公共模組代理時,您可能需要設定 GOPRIVATE。有關詳細資訊和其他設定,請參閱私有模組

我應該如何使用“go get”管理包版本?

Go 工具鏈內建了一個用於管理版本化相關包集合的系統,稱為 模組。模組在 Go 1.11 中引入,並自 1.14 起已準備好投入生產使用。

要使用模組建立專案,請執行 go mod init。此命令會建立一個 go.mod 檔案,用於跟蹤依賴項版本。

go mod init example/project

要新增、升級或降級依賴項,請執行 go get

go get golang.org/x/text@v0.3.5

有關入門的更多資訊,請參閱教程:建立模組

有關使用模組管理依賴項的指南,請參閱開發模組

模組中的包在演進過程中應保持向後相容性,遵循匯入相容性規則

如果舊包和新包具有相同的匯入路徑,則
新包必須與舊包向後相容。

Go 1 相容性指南是一個很好的參考:不要刪除匯出的名稱,鼓勵使用帶標籤的複合字面量等等。如果需要不同的功能,請新增一個新名稱,而不是更改舊名稱。

模組使用語義版本控制和語義匯入版本控制對此進行編碼。如果需要破壞相容性,請釋出一個新主版本模組。主版本 2 及更高版本的模組需要將其路徑(例如 /v2)作為其主版本字尾。這保留了匯入相容性規則:模組不同主版本中的包具有不同的路徑。

指標和分配

函式引數何時透過值傳遞?

和所有 C 家族語言一樣,Go 中的所有內容都是按值傳遞的。也就是說,函式總是獲取所傳遞內容的副本,就像有一個賦值語句將值賦給引數一樣。例如,將 int 值傳遞給函式會建立 int 的副本,而傳遞指標值會建立指標的副本,但不會複製它所指向的資料。(有關此如何影響方法接收器的討論,請參閱後面一節。)

map 和 slice 值表現得像指標:它們是描述符,包含指向底層 map 或 slice 資料的指標。複製 map 或 slice 值不會複製它所指向的資料。複製介面值會複製儲存在介面值中的內容。如果介面值包含一個結構體,複製介面值會複製該結構體。如果介面值包含一個指標,複製介面值會複製該指標,但同樣不會複製它所指向的資料。

請注意,此討論是關於操作的語義。實際實現可能會應用最佳化以避免複製,只要這些最佳化不改變語義。

我什麼時候應該使用指向介面的指標?

幾乎從不。指向介面值的指標僅在涉及為延遲評估而偽裝介面值型別的罕見、棘手情況下才會出現。

將指向介面值的指標傳遞給期望介面的函式是一個常見的錯誤。編譯器會抱怨這個錯誤,但情況仍然可能令人困惑,因為有時需要指標來滿足介面。關鍵是,儘管指向具體型別的指標可以滿足介面,但除了一個例外,指向介面的指標永遠不能滿足介面

考慮變數宣告,

var w io.Writer

列印函式 fmt.Fprintf 將滿足 io.Writer 的值作為其第一個引數——實現了規範 Write 方法的東西。因此我們可以這樣寫:

fmt.Fprintf(w, "hello, world\n")

然而,如果我們傳遞 w 的地址,程式將無法編譯。

fmt.Fprintf(&w, "hello, world\n") // Compile-time error.

唯一的例外是任何值,即使是指向介面的指標,都可以賦值給空介面型別(interface{})的變數。即便如此,如果該值是指向介面的指標,那幾乎肯定是錯誤的;結果可能令人困惑。

我應該在值上定義方法還是在指標上定義方法?

func (s *MyStruct) pointerMethod() { } // method on pointer
func (s MyStruct)  valueMethod()   { } // method on value

對於不習慣指標的程式設計師來說,這兩個示例之間的區別可能令人困惑,但情況實際上非常簡單。當在一個型別上定義方法時,接收者(上述示例中的 s)的行為與它是方法引數時完全相同。因此,將接收者定義為值還是指標,與函式引數應該是一個值還是一個指標是同一個問題。有幾個考慮因素。

首先,也是最重要的一點,方法是否需要修改接收者?如果需要,接收者 必須 是一個指標。(切片和對映表現得像引用,所以它們的情況稍微微妙一些,但例如要在方法中更改切片的長度,接收者仍然必須是一個指標。)在上面的示例中,如果 pointerMethod 修改了 s 的欄位,呼叫者將看到這些更改,但 valueMethod 是使用呼叫者引數的副本呼叫的(這是傳遞值的定義),因此它所做的更改將對呼叫者不可見。

順便說一句,在 Java 中,方法接收者總是指標,儘管它們的指標性質在某種程度上被掩蓋了(最近的發展正在將值接收者引入 Java)。Go 中的值接收者是不尋常的。

其次是效率的考慮。如果接收者很大,例如一個大的 struct,那麼使用指標接收者可能更划算。

接下來是一致性。如果型別的某些方法必須具有指標接收者,那麼其餘方法也應該如此,以便無論如何使用型別,方法集都保持一致。有關詳細資訊,請參閱方法集一節。

對於基本型別、切片和小型 struct 等型別,值接收者非常便宜,因此除非方法的語義需要指標,否則值接收者是高效且清晰的。

new 和 make 有什麼區別?

簡而言之:new 分配記憶體,而 make 初始化切片、對映和通道型別。

有關更多詳細資訊,請參閱《Effective Go》的相關部分。

在 64 位機器上,int 的大小是多少?

intuint 的大小是實現特定的,但在給定平臺上彼此相同。為了可移植性,依賴特定大小值的程式碼應使用顯式大小型別,例如 int64。在 32 位機器上,編譯器預設使用 32 位整數,而在 64 位機器上,整數為 64 位。(從歷史上看,情況並非總是如此。)

另一方面,浮點標量和複數型別總是指定大小的(沒有 floatcomplex 基本型別),因為程式設計師在使用浮點數時應該注意精度。浮點常量(無型別)的預設型別是 float64。因此 foo := 3.0 聲明瞭一個型別為 float64 的變數 foo。對於由(無型別)常量初始化的 float32 變數,必須在變數宣告中顯式指定變數型別:

var foo float32 = 3.0

或者,必須透過轉換給常量一個型別,例如 foo := float32(3.0)

我如何知道變數是分配在堆上還是棧上?

從正確性的角度來看,您不需要知道。Go 中的每個變數都存在,只要有對它的引用。實現選擇的儲存位置與語言的語義無關。

儲存位置確實對編寫高效程式有影響。如果可能,Go 編譯器會將區域性於函式的變數分配在該函式的棧幀中。但是,如果編譯器無法證明在函式返回後變數未被引用,那麼編譯器必須將變數分配在垃圾回收堆上,以避免懸空指標錯誤。此外,如果區域性變數非常大,將其儲存在堆上可能比儲存在棧上更合理。

在當前的編譯器中,如果一個變數的地址被獲取,那麼這個變數就是堆分配的候選者。然而,一個基本的 逃逸分析 會識別出一些情況,在這種情況下,這些變數不會在函式返回後繼續存在,並且可以駐留在棧上。

為什麼我的 Go 程序使用這麼多虛擬記憶體?

Go 記憶體分配器預留了很大一塊虛擬記憶體作為分配的區域。此虛擬記憶體對特定的 Go 程序是本地的;此預留不會剝奪其他程序的記憶體。

要查詢分配給 Go 程序的實際記憶體量,請使用 Unix top 命令並檢視 RES (Linux) 或 RSIZE (macOS) 列。

併發

哪些操作是原子的?互斥鎖呢?

Go 中操作的原子性描述可以在Go 記憶體模型文件中找到。

低階同步和原子原語在 syncsync/atomic 包中可用。這些包適用於簡單的任務,例如增加引用計數或保證小規模的互斥。

對於更高級別的操作,例如併發伺服器之間的協調,更高級別的技術可以產生更好的程式,Go 透過其 goroutine 和通道支援這種方法。例如,您可以構建程式,以便在任何時候只有一個 goroutine 負責特定資料。這種方法由最初的Go 箴言概括:

不要透過共享記憶體來通訊。相反,透過通訊來共享記憶體。

有關此概念的詳細討論,請參閱《透過通訊共享記憶體》的程式碼演練及其相關文章

大型併發程式可能會借鑑這兩種工具包。

為什麼我的程式在更多 CPU 的情況下沒有執行得更快?

程式在更多 CPU 的情況下是否執行得更快,取決於它所解決的問題。Go 語言提供了併發原語,例如 goroutine 和通道,但併發只有在底層問題本質上是並行時才能實現並行。本質上是順序的問題無法透過增加更多 CPU 來加速,而那些可以分解成可以並行執行的部分的問題則可以加速,有時甚至可以顯著加速。

有時增加更多的 CPU 會減慢程式的速度。在實際操作中,花費更多時間同步或通訊而不是進行有用計算的程式在使用多個作業系統執行緒時可能會出現效能下降。這是因為執行緒之間傳遞資料涉及上下文切換,這會產生顯著的開銷,而且這種開銷會隨著 CPU 的增加而增加。例如,Go 規範中的素數篩示例雖然啟動了許多 goroutine,但沒有顯著的並行性;增加執行緒(CPU)數量更有可能使其變慢而不是加快。

有關此主題的更多詳細資訊,請參閱題為《併發不是並行》的演講。

如何控制 CPU 數量?

可同時執行 Goroutine 的 CPU 數量由 GOMAXPROCS shell 環境變數控制,其預設值是可用的 CPU 核心數。因此,具有並行執行潛力的程式在多 CPU 機器上應預設實現並行執行。要更改要使用的並行 CPU 數量,請設定環境變數或使用執行時包中同名函式來配置執行時支援以利用不同數量的執行緒。將其設定為 1 消除了真正並行的可能性,強制獨立的 Goroutine 輪流執行。

執行時可以分配比 GOMAXPROCS 值更多的執行緒來處理多個未決的 I/O 請求。GOMAXPROCS 僅影響可以實際同時執行的 goroutine 數量;任意更多的 goroutine 可能會阻塞在系統呼叫中。

Go 的 goroutine 排程程式在平衡 goroutine 和執行緒方面表現良好,甚至可以搶佔 goroutine 的執行,以確保同一執行緒上的其他 goroutine 不會飢餓。但是,它並不完美。如果您遇到效能問題,根據應用程式設定 GOMAXPROCS 可能會有所幫助。

為什麼沒有 goroutine ID?

Goroutine 沒有名稱;它們只是匿名的工作者。它們不向程式設計師暴露任何唯一的識別符號、名稱或資料結構。有些人對此感到驚訝,期望 go 語句能返回一些可以在以後用於訪問和控制 Goroutine 的專案。

Goroutine 匿名化的根本原因是,在編寫併發程式碼時,Go 語言的全部功能都可以使用。相比之下,當執行緒和 Goroutine 被命名時所產生的用法模式可能會限制使用它們的庫所能做的事情。

以下是困難的說明。一旦命名了一個 goroutine 並圍繞它構建了一個模型,它就變得特殊,並且人們傾向於將所有計算都與該 goroutine 相關聯,而忽略了使用多個(可能是共享的)goroutine 進行處理的可能性。如果 net/http 包將每個請求的狀態與一個 goroutine 相關聯,那麼客戶端將無法在處理請求時使用更多的 goroutine。

此外,使用圖形系統等庫的經驗表明,要求所有處理都在“主執行緒”上進行的這種方法在併發語言中部署時可能多麼笨拙和限制。特殊執行緒或 goroutine 的存在本身就迫使程式設計師扭曲程式,以避免因無意中在錯誤的執行緒上操作而導致的崩潰和其他問題。

對於那些特定 goroutine 確實特殊的情況,語言提供了諸如通道之類的功能,可以以靈活的方式與其互動。

函式和方法

為什麼 T 和 *T 有不同的方法集?

Go 規範所述,型別 T 的方法集包含所有接收者型別為 T 的方法,而相應的指標型別 *T 的方法集包含所有接收者為 *TT 的方法。這意味著 *T 的方法集包含 T 的方法集,但反之不然。

這種區別的產生是因為如果一個介面值包含一個指標 *T,方法呼叫可以透過解引用指標來獲取一個值,但是如果一個介面值包含一個值 T,方法呼叫就沒有安全的方法來獲取一個指標。(這樣做將允許方法修改介面內部值的內容,這是語言規範不允許的。)

即使在編譯器可以獲取值的地址以傳遞給方法的情況下,如果方法修改了該值,則更改將在呼叫者中丟失。

例如,如果以下程式碼有效:

var buf bytes.Buffer
io.Copy(buf, os.Stdin)

它會將標準輸入複製到 buf 的一個 副本 中,而不是 buf 本身。這幾乎從來都不是期望的行為,因此被語言禁止。

作為 Goroutine 執行的閉包會發生什麼?

由於迴圈變數的工作方式,在 Go 1.22 版本之前(請參閱本節末尾的更新),在併發中使用閉包時可能會出現一些混淆。考慮以下程式:

func main() {
    done := make(chan bool)

    values := []string{"a", "b", "c"}
    for _, v := range values {
        go func() {
            fmt.Println(v)
            done <- true
        }()
    }

    // wait for all goroutines to complete before exiting
    for _ = range values {
        <-done
    }
}

人們可能會錯誤地期望輸出為 a, b, c。但您可能會看到的是 c, c, c。這是因為迴圈的每次迭代都使用變數 v 的相同例項,因此每個閉包都共享該單個變數。當閉包執行時,它會列印 fmt.Println 執行時 v 的值,但 v 可能自 goroutine 啟動以來已被修改。為了幫助在這些問題發生之前檢測它們,請執行 go vet

為了在每次啟動閉包時將 v 的當前值繫結到每個閉包,必須修改內部迴圈以在每次迭代中建立一個新變數。一種方法是將變數作為引數傳遞給閉包:

    for _, v := range values {
        go func(u string) {
            fmt.Println(u)
            done <- true
        }(v)
    }

在此示例中,v 的值作為引數傳遞給匿名函式。然後,該值可在函式內部作為變數 u 訪問。

更簡單的方法是直接建立一個新變數,使用一種可能看起來很奇怪但在 Go 中執行良好的宣告樣式:

    for _, v := range values {
        v := v // create a new 'v'.
        go func() {
            fmt.Println(v)
            done <- true
        }()
    }

這種語言行為,即不為每次迭代定義一個新變數,事後被認為是一個錯誤,並在 Go 1.22 中得到了解決,Go 1.22 確實為每次迭代建立了一個新變數,消除了這個問題。

控制流

Go 為什麼沒有 ?: 運算子?

Go 中沒有三元測試操作。您可以使用以下方法實現相同的結果:

if expr {
    n = trueVal
} else {
    n = falseVal
}

Go 中沒有 ?: 的原因是語言設計者發現該操作經常被用來建立難以理解的複雜表示式。if-else 形式雖然更長,但無疑更清晰。一種語言只需要一種條件控制流結構。

型別引數

Go 為什麼有型別引數?

型別引數允許所謂的泛型程式設計,其中函式和資料結構是根據稍後在使用這些函式和資料結構時指定的型別來定義的。例如,它們使得編寫一個函式成為可能,該函式返回任何有序型別的兩個值中的最小值,而無需為每種可能的型別編寫單獨的版本。有關更深入的解釋和示例,請參閱博文 Why Generics?

Go 中泛型是如何實現的?

編譯器可以選擇是單獨編譯每個例項化,還是將類似的例項化編譯為單個實現。單個實現方法類似於帶有介面引數的函式。不同的編譯器會對不同的情況做出不同的選擇。標準 Go 編譯器通常為每個具有相同形狀的型別引數發出一個例項化,其中形狀由型別的屬性(例如它包含的大小和指標的位置)確定。未來的版本可能會試驗編譯時間、執行時效率和程式碼大小之間的權衡。

Go 中的泛型與其他語言中的泛型有何不同?

所有語言的基本功能都相似:可以使用稍後指定的型別編寫型別和函式。即便如此,仍然存在一些差異。

  • Java

    在 Java 中,編譯器在編譯時檢查泛型型別,但在執行時刪除型別。這稱為型別擦除。例如,編譯時稱為 List<Integer> 的 Java 型別在執行時將變為非泛型型別 List。這意味著,例如,在使用 Java 形式的型別反射時,無法區分型別為 List<Integer> 的值和型別為 List<Float> 的值。在 Go 中,泛型型別的反射資訊包括完整的編譯時型別資訊。

    Java 使用型別萬用字元,例如 List<? extends Number>List<? super Number> 來實現泛型協變和逆變。Go 沒有這些概念,這使得 Go 中的泛型型別更簡單。

  • C++

    傳統上,C++ 模板不對型別引數強制執行任何約束,儘管 C++20 透過 概念 支援可選約束。在 Go 中,所有型別引數都必須有約束。C++20 概念表示為必須使用型別引數編譯的小程式碼片段。Go 約束是定義所有允許的型別引數集的介面型別。

    C++ 支援模板超程式設計;Go 不支援。實際上,所有 C++ 編譯器在例項化模板時都會編譯每個模板;如上所述,Go 可以並且確實對不同的例項化使用不同的方法。

  • Rust

    Rust 版本中的約束稱為特徵邊界。在 Rust 中,特徵邊界和型別之間的關聯必須明確定義,無論是在定義特徵邊界的 crate 中還是在定義型別的 crate 中。在 Go 中,型別引數隱式滿足約束,就像 Go 型別隱式實現介面型別一樣。Rust 標準庫定義了用於比較或加法等操作的標準特徵;Go 標準庫不定義,因為這些可以透過介面型別在使用者程式碼中表達。唯一的例外是 Go 的 comparable 預定義介面,它捕獲了型別系統中無法表達的屬性。

  • Python

    Python 不是一種靜態型別語言,因此可以合理地說所有 Python 函式預設都是泛型的:它們總是可以使用任何型別的值呼叫,並且任何型別錯誤都在執行時檢測。

Go 為什麼使用方括號作為型別引數列表?

Java 和 C++ 使用尖括號作為型別引數列表,如 Java List<Integer> 和 C++ std::vector<int>。但是,Go 無法使用此選項,因為它會導致語法問題:在解析函式內部的程式碼時,例如 v := F<T>,在看到 < 時,不清楚我們看到的是例項化還是使用 < 運算子的表示式。這在沒有型別資訊的情況下很難解決。

例如,考慮以下語句:

    a, b = w < x, y > (z)

在沒有型別資訊的情況下,無法確定賦值的右側是兩個表示式(w < xy > z),還是返回兩個結果值的泛型函式例項化和呼叫((w<x, y>)(z))。

Go 的一個關鍵設計決策是,在沒有型別資訊的情況下可以進行解析,這在使用尖括號表示泛型時似乎是不可能的。

Go 在使用方括號方面並非獨一無二或原創;還有其他語言,如 Scala,也使用方括號表示泛型程式碼。

Go 為什麼不支援帶有型別引數的方法?

Go 允許泛型型別擁有方法,但除了接收器之外,這些方法的引數不能使用引數化型別。我們預計 Go 永遠不會新增泛型方法。

問題是如何實現它們。具體來說,考慮檢查介面中的值是否實現了帶有附加方法的另一個介面。例如,考慮此型別,一個空結構體,帶有一個泛型 Nop 方法,該方法返回其引數,對於任何可能的型別:

type Empty struct{}

func (Empty) Nop[T any](x T) T {
    return x
}

現在假設一個 Empty 值儲存在 any 中並傳遞給檢查其功能的其他程式碼:

func TryNops(x any) {
    if x, ok := x.(interface{ Nop(string) string }); ok {
        fmt.Printf("string %s\n", x.Nop("hello"))
    }
    if x, ok := x.(interface{ Nop(int) int }); ok {
        fmt.Printf("int %d\n", x.Nop(42))
    }
    if x, ok := x.(interface{ Nop(io.Reader) io.Reader }); ok {
        data, err := io.ReadAll(x.Nop(strings.NewReader("hello world")))
        fmt.Printf("reader %q %v\n", data, err)
    }
}

如果 x 是一個 Empty,這段程式碼如何工作?看起來 x 必須滿足所有三個測試,以及任何其他型別的所有其他形式。

當呼叫這些方法時,會執行哪些程式碼?對於非泛型方法,編譯器會為所有方法實現生成程式碼並將其連結到最終程式中。但對於泛型方法,可能會有無限數量的方法實現,因此需要不同的策略。

有四種選擇:

  1. 在連結時,列出所有可能的動態介面檢查,然後查詢滿足這些檢查但缺少已編譯方法的型別,然後重新呼叫編譯器以新增這些方法。

    這將使構建速度顯著變慢,因為需要在連結後停止並重復一些編譯。這尤其會減慢增量構建。更糟的是,新編譯的方法程式碼本身可能具有新的動態介面檢查,並且該過程必須重複。可以構造一些例子,其中該過程甚至從未完成。

  2. 實現某種 JIT,在執行時編譯所需的方法程式碼。

    Go 因其純粹的預先編譯的簡單性和可預測的效能而受益匪淺。我們不願為了實現一個語言特性而承擔 JIT 的複雜性。

  3. 為每個泛型方法安排發出一個緩慢的 fallback,該方法使用一個函式表來處理型別引數上的每個可能的語言操作,然後將該 fallback 實現用於動態測試。

    這種方法將使由意外型別引數化的泛型方法比由編譯時觀察到的型別引數化的相同方法慢得多。這將使效能變得不可預測。

  4. 定義泛型方法根本不能用於滿足介面。

    介面是 Go 程式設計的重要組成部分。從設計的角度來看,不允許泛型方法滿足介面是不可接受的。

這些選擇都不是好的,所以我們選擇了“以上都不是”。

代替帶有型別引數的方法,使用帶有型別引數的頂級函式,或將型別引數新增到接收器型別。

有關更多詳細資訊,包括更多示例,請參閱提案

為什麼我不能為引數化型別的接收器使用更具體的型別?

泛型型別的方法宣告是用包含型別引數名稱的接收器編寫的。也許是因為在呼叫站點指定型別的語法相似,有些人認為這提供了一種透過在接收器中命名特定型別(例如 string)來為某些型別引數定製方法的方式:

type S[T any] struct { f T }

func (s S[string]) Add(t string) string {
    return s.f + t
}

這會失敗,因為編譯器將單詞 string 視為方法中型別引數的名稱。編譯器錯誤訊息將類似於“operator + not defined on s.f (variable of type string)”。這可能會令人困惑,因為 + 運算子在預宣告型別 string 上執行良好,但此宣告已為該方法覆蓋了 string 的定義,並且該運算子不適用於該不相關的 string 版本。像這樣覆蓋預宣告的名稱是有效的,但這樣做很奇怪,而且通常是一個錯誤。

為什麼編譯器不能推斷我程式中的型別引數?

在許多情況下,程式設計師可以很容易地看到泛型型別或函式的型別引數必須是什麼,但語言不允許編譯器推斷它。型別推斷有意受到限制,以確保永遠不會對推斷出哪種型別產生任何混淆。使用其他語言的經驗表明,意外的型別推斷在閱讀和除錯程式時可能會導致相當大的混亂。總是可以指定在呼叫中使用的顯式型別引數。將來可能會支援新形式的推斷,只要規則保持簡單明瞭。

包和測試

如何建立多檔案包?

將包的所有原始檔單獨放在一個目錄中。原始檔可以隨意引用不同檔案中的項;無需前向宣告或標頭檔案。

除了分成多個檔案之外,該包的編譯和測試方式與單檔案包相同。

如何編寫單元測試?

在與包原始檔相同的目錄中建立一個以 _test.go 結尾的新檔案。在該檔案中,import "testing" 並編寫形式為

func TestFoo(t *testing.T) {
    ...
}

在該目錄中執行 go test。該指令碼會找到 Test 函式,構建一個測試二進位制檔案,並執行它。

有關更多詳細資訊,請參閱 How to Write Go Code 文件、testing 包和 go test 子命令。

我最喜歡的測試輔助函式在哪裡?

Go 的標準 testing 包使編寫單元測試變得容易,但它缺少其他語言測試框架中提供的功能,例如斷言函式。本文件的早期部分解釋了 Go 為什麼沒有斷言,同樣的論點也適用於在測試中使用 assert。正確的錯誤處理意味著在一個測試失敗後允許其他測試執行,以便除錯失敗的人能夠全面瞭解問題所在。對於一個測試來說,報告 isPrime 對於 2、3、5 和 7(或 2、4、8 和 16)給出錯誤答案比報告 isPrime 對於 2 給出錯誤答案因此不再執行任何測試更有用。觸發測試失敗的程式設計師可能不熟悉失敗的程式碼。現在投入時間編寫好的錯誤訊息,在測試中斷時會得到回報。

一個相關點是,測試框架傾向於發展成自己的迷你語言,帶有條件、控制和列印機制,但 Go 已經擁有所有這些功能;為什麼要重新建立它們?我們寧願用 Go 編寫測試;要學習的語言少了一種,並且這種方法使測試直接且易於理解。

如果編寫好的錯誤所需的額外程式碼量看起來重複且令人望而生畏,那麼如果測試是表驅動的,透過迭代資料結構中定義的一系列輸入和輸出來進行,測試可能會更好地工作(Go 對資料結構字面量有出色的支援)。然後,編寫好的測試和好的錯誤訊息的工作將分攤到許多測試用例中。Go 標準庫中充滿了說明性示例,例如 fmt 包的格式化測試

為什麼標準庫中沒有 X

標準庫的目的是支援執行時庫,連線到作業系統,並提供許多 Go 程式所需的關鍵功能,例如格式化 I/O 和網路。它還包含對 Web 程式設計很重要的元素,包括加密和對 HTTP、JSON 和 XML 等標準的支援。

沒有明確的包含標準,因為在很長一段時間內,這是 唯一 的 Go 庫。然而,現在有定義了新增內容的標準。

標準庫中新增的內容很少,並且納入標準很高。包含在標準庫中的程式碼承擔著巨大的持續維護成本(通常由原作者以外的人承擔),受 Go 1 相容性承諾 的約束(阻止修復 API 中的任何缺陷),並受 Go 釋出計劃 的約束,阻止錯誤修復快速提供給使用者。

大多數新程式碼應該位於標準庫之外,並透過 go 工具go get 命令訪問。此類程式碼可以有自己的維護者、釋出週期和相容性保證。使用者可以在 pkg.go.dev 上查詢包並閱讀其文件。

儘管標準庫中有些部分確實不屬於,例如 log/syslog,但由於 Go 1 相容性承諾,我們繼續維護庫中的所有內容。但我們鼓勵大多數新程式碼存在於其他地方。

實施

用於構建編譯器的編譯器技術是什麼?

Go 有幾個生產編譯器,還有一些正在為各種平臺開發中。

預設編譯器 gc 隨 Go 發行版一起提供,作為對 go 命令的支援的一部分。Gc 最初是用 C 編寫的,因為載入程式很困難——您需要一個 Go 編譯器來設定 Go 環境。但情況已經發展,自 Go 1.5 版本以來,編譯器已成為一個 Go 程式。編譯器使用自動翻譯工具從 C 轉換為 Go,如本設計文件演講所述。因此,編譯器現在是“自託管”的,這意味著我們需要面對載入程式問題。解決方案是已經有一個正常的 Go 安裝,就像人們通常有一個正常的 C 安裝一樣。如何從原始碼啟動新的 Go 環境的故事在這裡這裡描述。

Gc 用 Go 編寫,帶有一個遞迴下降解析器,並使用一個自定義載入器(也用 Go 編寫,但基於 Plan 9 載入器)來生成 ELF/Mach-O/PE 二進位制檔案。

Gccgo 編譯器是一個用 C++ 編寫的前端,帶有一個遞迴下降解析器,並與標準 GCC 後端耦合。一個實驗性的 LLVM 後端 正在使用相同的前端。

專案開始時,我們考慮為 gc 使用 LLVM,但認為它太大太慢,無法滿足我們的效能目標。事後看來更重要的是,從 LLVM 開始會更難引入 Go 所需的一些 ABI 和相關更改,例如堆疊管理,但這些更改不是標準 C 設定的一部分。

Go 被證明是一種很好的語言,可以用來實現 Go 編譯器,儘管這並非其最初的目標。從一開始就不自託管使得 Go 的設計能夠專注於其最初的用例,即網路伺服器。如果我們早早決定 Go 應該自行編譯,我們最終可能會得到一種更針對編譯器構造的語言,這是一個值得稱讚的目標,但不是我們最初的目標。

儘管 gc 有自己的實現,但 go/parser 包中提供了原生的詞法分析器和解析器,並且還有一個原生的型別檢查器gc 編譯器使用這些庫的變體。

執行時支援是如何實現的?

同樣由於引導問題,執行時程式碼最初主要用 C 編寫(帶有一點彙編),但後來已翻譯成 Go(除了一些彙編部分)。Gccgo 的執行時支援使用 glibcgccgo 編譯器使用一種稱為分段堆疊的技術實現 goroutine,該技術由對 gold 連結器的最新修改支援。Gollvm 類似地建立在相應的 LLVM 基礎設施上。

為什麼我的簡單程式會生成這麼大的二進位制檔案?

gc 工具鏈中的連結器預設建立靜態連結的二進位制檔案。因此,所有 Go 二進位制檔案都包含 Go 執行時,以及支援動態型別檢查、反射甚至 panic 時堆疊跟蹤所需的執行時型別資訊。

一個簡單的 C 語言“hello, world”程式,在 Linux 上使用 gcc 靜態編譯和連結,大約是 750 KB,包括 printf 的實現。一個使用 fmt.Printf 的等效 Go 程式重達幾兆位元組,但這包括更強大的執行時支援以及型別和除錯資訊。

使用 gc 編譯的 Go 程式可以使用 -ldflags=-w 標誌進行連結,以停用 DWARF 生成,從而從二進位制檔案中刪除除錯資訊,但不會損失任何其他功能。這可以顯著減小二進位制檔案的大小。

我能停止這些關於我的未使用變數/匯入的抱怨嗎?

存在未使用的變數可能表示錯誤,而未使用的匯入只會減慢編譯速度,隨著程式程式碼和程式設計師的積累,這種影響會變得很大。因此,Go 拒絕編譯帶有未使用變數或匯入的程式,以短期便利換取長期構建速度和程式清晰度。

不過,在開發程式碼時,暫時建立這些情況是很常見的,並且在程式編譯之前必須將其刪除可能會很煩人。

有些人要求提供編譯器選項來關閉這些檢查或至少將其減少到警告。但是,尚未新增此類選項,因為編譯器選項不應影響語言的語義,並且因為 Go 編譯器不報告警告,只報告阻止編譯的錯誤。

沒有警告有兩個原因。首先,如果值得抱怨,那就值得在程式碼中修復。(反之,如果它不值得修復,那就不值得提及。)其次,讓編譯器生成警告會鼓勵實現對可能導致編譯嘈雜的弱情況發出警告,從而掩蓋應該修復的真正錯誤。

不過,解決這種情況很簡單。使用空白識別符號讓未使用的東西在您開發時保留。

import "unused"

// This declaration marks the import as used by referencing an
// item from the package.
var _ = unused.Item  // TODO: Delete before committing!

func main() {
    debugData := debug.Profile()
    _ = debugData // Used only during debugging.
    ....
}

如今,大多數 Go 程式設計師都使用一個工具,goimports,它會自動重寫 Go 原始檔以使其具有正確的匯入,從而在實踐中消除了未使用匯入的問題。這個程式可以很容易地連線到大多數編輯器和 IDE,以便在編寫 Go 原始檔時自動執行。此功能也內建在 gopls 中,如上文所述

為什麼我的病毒掃描軟體認為我的 Go 發行版或編譯的二進位制檔案被感染了?

這種情況很常見,尤其是在 Windows 機器上,而且幾乎總是誤報。商業病毒掃描程式常常被 Go 二進位制檔案的結構所迷惑,因為它們不像其他語言編譯的二進位制檔案那樣常見。

如果您剛剛安裝了 Go 發行版,並且系統報告它已感染,那肯定是一個錯誤。要真正徹底,您可以透過將校驗和與下載頁面上的校驗和進行比較來驗證下載。

無論如何,如果您認為報告有誤,請向您的病毒掃描程式供應商報告錯誤。也許隨著時間的推移,病毒掃描程式可以學會理解 Go 程式。

效能

Go 在基準 X 上為什麼表現不佳?

Go 的設計目標之一是使可比較程式的效能接近 C 語言,但在某些基準測試中,它的表現卻相當糟糕,包括 golang.org/x/exp/shootout 中的幾個。最慢的依賴於 Go 中沒有可比較效能版本的庫。例如,pidigits.go 依賴於多精度數學包,而 C 版本(與 Go 不同)使用 GMP(用最佳化彙編編寫)。依賴於正則表示式的基準測試(例如 regex-dna.go)實際上是在比較 Go 的原生 regexp 包與成熟、高度最佳化的正則表示式庫(如 PCRE)。

基準測試遊戲透過大量的調優才能獲勝,而大多數基準測試的 Go 版本都需要關注。如果您測量真正可比較的 C 和 Go 程式(reverse-complement.go 就是一個例子),您會發現這兩種語言在原始效能上比這套測試所顯示的要接近得多。

儘管如此,仍有改進空間。編譯器很好,但可以更好,許多庫需要大量的效能工作,垃圾回收器還不夠快。(即使它更快,注意不要產生不必要的垃圾也會產生巨大影響。)

無論如何,Go 通常可以非常有競爭力。隨著語言和工具的發展,許多程式的效能都有了顯著提高。請參閱有關剖析 Go 程式的部落格文章,瞭解一個資訊豐富的示例。它很舊,但仍然包含有用的資訊。

與 C 的變化

為什麼語法與 C 如此不同?

除了宣告語法之外,差異並不大,並且源於兩個願望。首先,語法應該輕巧,沒有太多強制性的關鍵字、重複或神秘之處。其次,語言被設計成易於分析,並且可以在沒有符號表的情況下進行解析。這使得構建偵錯程式、依賴分析器、自動化文件提取器、IDE 外掛等工具變得容易得多。C 及其後代在這方面臭名昭著地困難。

為什麼宣告是反向的?

只有當你習慣了 C 語言時,它們才是反向的。在 C 語言中,變數的宣告就像表示其型別的表示式,這是一個不錯的想法,但型別和表示式的語法混合得不太好,結果可能會令人困惑;考慮函式指標。Go 大部分將表示式和型別語法分開,這簡化了事情(使用字首 * 表示指標是一個例外,它證明了規則)。在 C 語言中,宣告

    int* a, b;

宣告 a 是指標,但 b 不是;在 Go 語言中

    var a, b *int

宣告兩者都是指標。這更清晰、更規則。此外,:= 短宣告形式認為完整變數宣告應該與 := 呈現相同的順序,因此

    var a uint64 = 1

具有與以下相同的效果

    a := uint64(1)

解析也透過為型別提供不同於表示式語法的獨立語法而簡化;funcchan 等關鍵字使事情清晰明瞭。

有關更多詳細資訊,請參閱有關Go 的宣告語法的文章。

為什麼沒有指標算術?

安全。沒有指標算術,就可以建立一種永遠不會錯誤地成功派生非法地址的語言。編譯器和硬體技術已經發展到使用陣列索引的迴圈可以與使用指標算術的迴圈一樣高效的程度。此外,缺乏指標算術可以簡化垃圾收集器的實現。

為什麼 ++-- 是語句而不是表示式?為什麼是字尾而不是字首?

沒有指標算術,字首和字尾遞增運算子的便利性就會降低。透過將它們完全從表示式層次結構中刪除,表示式語法得到簡化,並且圍繞 ++-- 的求值順序(考慮 f(i++)p[i] = q[++i])的混亂問題也隨之消除。這種簡化意義重大。至於字尾 vs. 字首,兩者都可以很好地工作,但字尾版本更傳統;對字首的堅持是隨著 STL 而出現的,STL 是為一種語言而設計的庫,而該語言的名稱中諷刺性地包含一個字尾遞增。

為什麼有大括號卻沒有分號?為什麼我不能把開括號放在下一行?

Go 使用大括號進行語句分組,這種語法對於使用過 C 家族中任何語言的程式設計師來說都很熟悉。然而,分號是為解析器而不是為人準備的,我們希望儘可能地消除它們。為了實現這個目標,Go 借鑑了 BCPL 的一個技巧:分隔語句的分號在形式語法中,但由詞法分析器在任何可能作為語句結尾的行末自動注入,無需前瞻。這在實踐中效果很好,但其結果是強制了一種大括號樣式。例如,函式的開括號不能單獨出現在一行上。

有人認為詞法分析器應該進行前瞻以允許大括號出現在下一行。我們不同意。由於 Go 程式碼旨在由 gofmt 自動格式化,因此必須選擇 某種 樣式。這種樣式可能與您在 C 或 Java 中使用的樣式不同,但 Go 是一種不同的語言,gofmt 的樣式與任何其他樣式一樣好。更重要的是——重要得多——所有 Go 程式都採用單一的、程式強制的格式的優點遠遠超過特定樣式帶來的任何感知到的缺點。還要注意,Go 的樣式意味著 Go 的互動式實現可以一次一行地使用標準語法,而無需特殊規則。

為什麼需要垃圾回收?會不會太昂貴?

系統程式中最大的簿記來源之一是管理分配物件的生命週期。在像 C 這樣手動完成的語言中,它可能會消耗大量的程式設計師時間,並且常常是惡性錯誤的根源。即使在像 C++ 或 Rust 這樣提供輔助機制的語言中,這些機制也可能對軟體設計產生顯著影響,常常增加自己的程式設計開銷。我們認為消除這種程式設計師開銷至關重要,並且近年來垃圾回收技術的進步使我們相信它可以以足夠低的成本和足夠低的延遲實現,使其成為網路系統的一種可行方法。

併發程式設計的許多困難根源於物件生命週期問題:當物件線上程之間傳遞時,很難保證它們能夠安全地釋放。自動垃圾回收使得併發程式碼更容易編寫。當然,在併發環境中實現垃圾回收本身也是一個挑戰,但一次性解決它而不是在每個程式中都解決它對每個人都有幫助。

最後,除了併發之外,垃圾回收使介面更簡單,因為它們不需要指定如何在它們之間管理記憶體。

這並不是說 Rust 等語言中為解決資源管理問題而帶來的新想法是錯誤的;我們鼓勵這項工作,並很高興看到它如何發展。但 Go 採用了一種更傳統的方法,透過垃圾回收(且僅透過垃圾回收)來解決物件生命週期問題。

目前的實現是一個標記清除收集器。如果機器是多處理器,收集器將在單獨的 CPU 核心上與主程式並行執行。近年來,收集器在暫停時間方面取得了重大進展,即使對於大型堆,也常常將其減少到亞毫秒級,幾乎消除了網路伺服器中垃圾回收的主要反對意見之一。演算法的完善、開銷和延遲的進一步降低以及新方法的探索仍在繼續。Go 團隊的 Rick Hudson 在 2018 年的 ISMM 主題演講 中描述了迄今為止的進展,並提出了一些未來的方法。

關於效能,請記住 Go 賦予程式設計師對記憶體佈局和分配的相當大的控制權,這比垃圾回收語言中通常的情況要多得多。細心的程式設計師可以透過很好地使用語言來顯著減少垃圾回收開銷;請參閱有關剖析 Go 程式的文章,瞭解一個示例,包括 Go 剖析工具的演示。