常見問題解答 (FAQ)

起源

專案目的為何?

在 2007 年 Go 創建之初,程式設計世界與今日大不相同。生產用軟體通常是用 C++ 或 Java 撰寫,沒有 GitHub,當時大多數電腦還不是多處理器,除了 Visual Studio 和 Eclipse 以外,可用的 IDE 或其他高階工具少之又少,更別說在網路上免費取得。

與此同時,我們對於使用既有的語言及相關建構系統來建構大型軟體專案時,所需要的過度複雜性感到沮喪。自從開發出 C、C++ 和 Java 等語言以來,電腦的速度已大幅提升,但程式設計本身的進展卻沒有那麼顯著。此外,明顯的是多處理器正普及化,但大多數語言對於有效率且安全地撰寫此類程式並沒有提供太多協助。

在技術發展過程中,針對未來幾年會主宰軟體工程的主要問題,以及一種新的語言如何協助解決這些問題,我們決定退一步思考。例如,多核心 CPU 的崛起顯示出一種語言應為某種類型的並行處理或並發性提供一流的支援。而要在一個大型並發程式中讓資源管理變得更容易,就需要垃圾收集,或至少某種安全的自動記憶體管理。

這些考量造就了 一系列的討論,而後 Go 便應運而生,一開始是一組想法和願景,然後才成為一種語言。一個首要目標是讓 Go 透過啟用工具、自動執行格式化程式碼等單調乏味的工作,並排除作業大型程式碼庫的障礙,進而為程式設計師提供更多協助。

可在文章 Go at 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 程式設計師(土撥鼠),而且每一天都有更多人加入。Go 的成功遠遠超過我們的預期。

土撥鼠吉祥物的起源為何?

吉祥物和標誌是由 Renée French 設計,她同時也設計了 Plan 9 小兔子 Glenda。一篇有關土撥鼠的 部落格文章 說明了它是如何演變自她幾年前為 WFMU T 恤設計所使用的土撥鼠。標誌和吉祥物受到 創用 CC 姓名標示 4.0 授權規範的保護。

土撥鼠有一個 樣式表,說明其特徵以及如何正確呈現它們。該樣式表首次出現在 Renée 於 2016 年 Gophercon 上發表的一場 演講 中。他有獨特的功能;他是 Go 土撥鼠,不是任何普通土撥鼠。

這門語言稱為 Go 或 Golang?

這門語言稱為 Go。會出現「golang」這個名稱是因為該網站最初為golang.org。(當時還沒有.dev網域名稱。)許多人使用 golang 這個名稱,而且它作為標籤也很方便。例如,該語言的社群媒體標籤為「#golang」。無論如何,該語言名稱就是 Go,很簡單。

附帶一提:儘管官方標誌包含兩個大寫字母,但該語言名稱寫作 Go,而非 GO。

為何您要建立一門新語言?

我們對當時在 Google 所從事的業務中所使用的語言和環境感到沮喪,Go 正是在這種挫折中誕生的。程式設計變得過於困難,部分原因在於所選用的語言。必須選擇有效率的編譯、有效率的執行或程式設計的容易性;同一主流語言並未兼具這三項特質。能夠的程式設計師會選擇動態類型語言(例如 Python 和 JavaScript)而非 C++ 或 Java(程度較低),以便利性優先於安全性與效率。

我們並不孤單。在程式語言方面經過多年沉寂,Go 成為數種新語言的先驅,包括 Rust、Elixir、Swift 等,再次讓程式語言開發成為一項活躍且幾乎是主流的領域。

Go 嘗試結合動態型別直譯式語言的易於程式設計,以及靜態型別編譯語言的高效率和安全性,以解決這些問題。它也致力於更適應目前的硬體,支援網路和多核心運算。最後,使用 Go 的目的是快速:在單一電腦上建置大型可執行檔最多只需要數秒。為了達成這些目標,我們重新思考了一些我們目前語言的程式設計方法,導致:採用組合式而非層級式型別系統;支援並行處理和垃圾回收;依據嚴格的規範進行相依性設定;等等。資料庫或工具無法妥善處理這些問題,因此需要一種新的語言。

這篇文章Google 中的 Go探討了 Go 語言設計的背景和動機,以及提供了更多有關此常見問題解答中許多解答的詳細資訊。

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?

是的。Google 內部在製作過程中廣泛使用 Go。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,但這並非自然的結合,且可能需要特殊的介面軟體。此外,連結 Go 程式碼與 C 程式碼會放棄 Go 所提供的記憶體安全性和堆疊管理屬性。有時,使用 C 函式庫來解決問題絕對有其必要,但這麼做總是會引入在純 Go 程式碼中不存在的風險元素,因此請小心行事。

如果你確實需要使用 C 與 Go 程式語言,後續處理方式取決於 Go 編譯器的執行方式。由 Google 的 Go 團隊支援且為 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++ 時的堆疊限制。

什麼 IDE 支援 Go?

Go 專案沒有提供客製 IDE,但它的語言和函式庫的設計讓大家可以輕鬆地分析原始程式碼。因此,大多數知名的編輯器和 IDE 都直接或透過外掛程式支援 Go。

Go 團隊也支援一個用於 LSP 協定的 Go 語言伺服器,稱為 gopls。支援 LSP 的工具可以使用 gopls 來整合特定語言的支援。

提供良好的 Go 支援的知名 IDE 和編輯器清單包括 Emacs、Vim、VSCode、Atom、Eclipse、Sublime、IntelliJ (透過名稱為 GoLand 的客製變體) 等等。很可能你最喜愛的環境是適用於用 Go 程式設計的高效率環境。

Go 是否支援 Google 的 Protocol Buffers?

一個獨立的開放原始碼專案提供必要的編譯器外掛程式和函式庫。它可以在 github.com/golang/protobuf/ 找到。

設計

Go 有執行時期嗎?

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)造成的,部分是由於過於強調低階的細節(例如互斥鎖、條件變數和記憶體屏障)。即使底層仍然有互斥鎖等東西,更高級別的介面也能產生更簡單的程式碼。

提供高階 linguist 支援的成功模型之一是來自 Hoare 的 Communicating Sequential Processes 或 CSP。Occam 和 Erlang 是兩種來自 CSP 的知名語言。Go 的 concurrency 原語衍生自家族樹的不同部分,其主要貢獻是將通道視為一級物件的強大概念。多種早期語言的經驗顯示,CSP 模型很適合放入程序語言架構中。

為何使用 goroutines 而非執行緒?

Goroutines 使得並行運算容易使用。這個概念已經存在一段時間,其想法是將獨立執行的函數 (協程) 多工到一組執行緒。當協程封鎖,例如呼叫封鎖系統呼叫時,執行階段時間會自動將同一個作業系統執行緒上的其他協程移動到不同的可執行執行緒,如此它們就不會被封鎖。程式設計人員看不到這一切,這就是重點。我們稱為 goroutines 的結果可以非常便宜:它們的作業開銷很低,僅超過堆疊的記憶體,大約幾 KB。

為了讓堆疊變小,Go 的執行階段時間使用可調整大小的有界堆疊。新鑄造的 goroutine 會獲得幾 KB,這幾乎總是足夠的。如果不足夠,執行階段時間會自動增加(和縮小)儲存堆疊的記憶體,讓許多 goroutine 能在小量的記憶體中執行。CPU 作業開銷平均大約是每個函數呼叫三個簡單的指令。在相同的位址空間建立數十萬個 goroutine 是合乎實際的。如果 goroutine 只是執行緒,系統資源會在數量更小的情況下用盡。

為何未定義型式操作為原子作業?

經過長時間的討論,我們決定典型的地圖使用並不需多個 goroutines的安全存取,而在需要的情況中,地圖很可能是某些已同步较大資料結構或運算的一部分。因此,要求讓所有地圖操作都取得互斥鎖將會使大多數的程式變慢,並只會讓部分程式更安全。然而,這並非易於做成的決定,因為這表示不受控制的地圖存取可能會使程式崩潰。

此語言並不排斥原子地圖更新。在需要的時候,例如當承載一個不受信任的程式時,實作可以交互鎖定地圖存取權。

僅在更新時,地圖存取才是不安全的。只要所有 goroutine 都只在進行讀取 (在映射中查詢元素,包括使用 for 範圍迴圈對其進行反覆運算),並且不會透過指派元素或執行刪除來變更映射,則它們就可以在沒有同步運算的情況下同時存取映射。

為了協助正確使用地圖,有些語言實作包含一項特殊檢查,當地圖因並行執行而遭不安全修改時,會在執行期間自動回報。 同步函式庫中還有一種類型稱為 sync.Map,適用於某些使用模式(例如靜態快取),不過不適合作為內建地圖類型的通用替換。

你願意接受我對語言的變更嗎?

人們經常建議改善語言(電子郵件清單 記載了大量此類討論歷史),但只有極少數的變更被接受。

儘管 Go 是開源專案,但語言和函式庫受到 相容承諾 的保護,以避免造成變更損壞現有程式,至少在原始碼層級中(程式可能需要偶爾重新編譯以維持最新)。如果你的建議違反 Go 1 規格,我們甚至不會考慮這個想法,不論其優點為何。未來的主要 Go 版本可能與 Go 1 不相容,但有關此主題的討論才剛開始,有一件事是肯定的:在此過程中,此類不相容之處會很少。此外,相容承諾鼓勵我們提供自動化路徑,讓舊程式在這種情況發生時適應。

即使你的建議與 Go 1 規格相容,它也可能與 Go 的設計目標精神不符。 Google 的 Go:服務軟體工程的語言設計 一文解釋了 Go 的起源及其設計背後的動機。

型態

Go 是物件導向語言嗎?

是也不是。雖然 Go 有型態和方法,並允許物件導向的程式設計風格,但它沒有型態階層。 Go 中的「介面」概念提供了不同的方法,我們認為它易於使用,在某些方面更為一般。另外有許多方法可以將型態嵌入到其他型態中,以提供類似(但不等於)子類別化的東西。此外,Go 中的方法比 C++ 或 Java 更為一般:它們可以定義為任何資料類型,甚至是內建類型,例如單純的「未包裝」整數。它們不受限於結構 (class)。

此外,由於缺少型態階層,Go 中的「物件」感覺比 C++ 或 Java 的物件更輕巧。

我要怎麼得到方法的動態傳遞?

要獲得動態傳遞方法的唯一方法是透過介面。結構或任何其他具體類型的方法總是會以靜態方式解析。

為什麼沒有型態繼承?

至少在最知名的語言中,面向物件程式設計過度討論類型之間的關係,而這些關係通常可以自動衍生。Go 採取了不同的方法。

Go 不要求程式設計師事先宣告兩個類型有關聯,相反地,類型可以自動滿足指定其方法子集的任何介面。除了減少分類事務以外,這種方法還有實際的好處。類型可以同時滿足多個介面,而沒有傳統多重繼承的複雜性。介面可以非常輕量級,一個只有單一方法、甚至是 0 個方法的介面可以表達一個有用的概念。如果有了新點子、或者是為了測試,都可以事後加入介面,而不需要對原先的類型進行註解。由於類型和介面之間沒有明確的關係,因此不需要管理或討論類型階層。

可以用這些概念建構類似的類型安全 Unix 管線。例如,請參閱 fmt.Fprintf 如何針對所有輸出啟用格式化列印,而不僅僅是檔案;或 bufio 套件如何可以完全與檔案 I/O 分離;或 image 套件如何產生壓縮的影像檔案。所有這些概念都源自於單一介面 (io.Writer) 代表單一方法 (Write)。而且這僅僅是皮毛而已。Go 的介面對於程式的結構方式影響深遠。

需要一些時間來習慣,但這種型別相依關係的隱含樣式是 Go 中最具生產力的事物之一。

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

我們討論了這個問題,但決定在實務上實作 len 與朋友們作為函式是沒問題的,而且不會讓基本類型的介面相關問題變得複雜(在 Go 類型的意義之中)。

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

如果方法調用不需要同時執行類型匹配,就會變得更簡單。其他語言的經驗告訴我們,偶爾使用名稱相同但簽章不同的多種方法是有用的,但在實務上也可能會令人困惑且脆弱。只透過名稱匹配,並要求類型一致,是 Go 型別系統中一個主要的簡化決策。

關於運算子超載,它似乎是一種便利,而不是絕對必要的需求。話說回來,沒有它事情會更簡單。

為什麼 Go 沒有「實作」宣告?

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 的引數;那是程式設計師的責任,就像類型 T2 所示範的一樣,它實作了 Equaler

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 都是未設定時(T=nil,未設定 V),介面值才會是 nil。特別是,nil 介面將永遠儲存 nil 類型。如果我們在介面值內儲存類型為 *intnil 指標,則無論指標的值為何,其內部類型都會是 *int: (T=*int, V=nil)。因此,這樣的介面值會是非 nil 的,即使其內部的指標值 V nil

這種情況可能會令人困惑,而且會在類似於 error 傳回的介面值中儲存 nil 值時發生

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 才能傳回適當的 nil error 給呼叫者。

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

對於傳回錯誤的函式而言,在 deren 簽章中一律使用 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 中加入變異型態,但在討論後決定不採用,因為它們與介面重疊的方式令人混淆。如果變異型態的元素本身就是介面,會發生什麼情況呢?

此外,某些變異型態所處理的內容已由語言涵蓋。錯誤範例可以很輕易地使用介面值來保留錯誤,並使用類型變換來區辨案例。語法樹範例也可以做到,雖然沒有那麼優雅。

為何 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 呼叫使用。

常數 這篇網誌文章更詳細地探討了這個主題。

為什麼要建立內建的映射?

原因和字串相同:它們是一種強大且重要的資料結構,提供具語法支援的絕佳實作可讓程式設計更為愉快。我們相信 Go 對映射的實作出於用心,足以應付絕大部分的應用程式的運用。若某特定應用程式能從客製化實作中受益,那是可能寫出實作的,但語法上不會那麼方便;這似乎是一個合理的折衷方案。

為什麼映射不允許切片作為鍵值?

映射比對需要一個等號運算子,而切片並未實作此運算子。切片並未實作等號運算子,是因為等號運算子在這些類型上並未定義完善;在淺層和深層比較、指標和值比較、以及如何處理遞迴類型之間,存在多項考量,如此種種。我們可能會重新檢視此議題,且為切片實作等號運算子並不會使任何現有的程式無效——但是在對於切片等號運算子的意義沒有明確概念的情況下,目前最單純的做法就是略過它。

結構和陣列已經定義好等號運算子,因此它們可用作映射鍵值。

為什麼映射、切片和通道是參考,而陣列則是值?

此主題有很多歷史淵源。在早期,映射和通道在語法上是指標,且無法宣告或使用非指標實體。另外,我們對於陣列應該如何運作而苦惱了好一陣子。最後我們決定,指標和值的嚴格區分讓程式語言更加難以使用。將這些類型變更為對應共用資料結構的參考,便能解決這些問題。這種改變為語言增加了一些令人遺憾的複雜度,但在可用性方面卻有極大的效果:Go 在這項變更後,成為了更具生產力、更舒適的語言。

撰寫程式碼

函式庫是如何記錄的?

若要從命令列存取文件,go 工具有一個 doc 子指令,它提供一個用於宣告、檔案、套件等等文件的文字介面。

整體函式庫探索頁面 pkg.go.dev/pkg/ 執行一個伺服器,其會從網路上任何位置的 Go 原始碼中萃取出函式庫文件,並以 HTML 格式提供文件,其中包含前往宣告和其他相關元素的連結。這是瞭解現有 Go 函式庫的最簡單管道。

在專案的早期,有一個可執行類似的程式碼 godoc,它也可以執行來擷取本機中檔案的說明文件;pkg.go.dev/pkg/ 基本上是延續者。另一個延續者是 pkgsite 指令,與 godoc 一樣,它可以在本機執行,儘管它尚未整合到 go doc 顯示的結果中。

是否有 Go 程式設計風格指南?

沒有明確的風格指南,儘管肯定有公認的「Go 風格」。

Go 建立指導命名、配置和檔案組織決策的慣例。文件 有效的 Go 包含關於這些主題的一些建議。更直接地說,程式 gofmt 是一個美化列印器,其目的是強制執行佈局規則;它取代了允許詮釋的各種注意事項清單。貯存庫中的所有 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 驗證或在 HTTPS 代替下使用 SSH。若要透過 HTTPS 驗證,您可以在 git 諮詢的 $HOME/.netrc 檔案中新增一行

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

對於 GitHub 帳戶,密碼可以是 個人存取權杖

Git 也可以設定成使用 SSH 取代 HTTPS 來存取符合特定字首的 URL。例如,若要對所有 GitHub 存取使用 SSH,請將這些行新增至您的 ~/.gitconfig

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

我應該如何使用「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 的副本,而傳遞指標值時會建立指標的副本,但不會建立所指資料的副本。(請參閱 後續章節,了解這對方法接收者有何影響。)

映射和切片值就像指標一樣:它們是包含指向底層映射或切片資料的指標的描述符。複製映射或切片值不會複製所指資料。複製介面值時會複製介面值中儲存的項目。如果介面值保留結構,則複製介面值會複製結構。如果介面值保留指標,則複製介面值會建立指標的副本,但不會建立所指資料的副本。

請注意,此討論是針對這些操作的語義。實際實作可能會套用最佳化,以避免複製,只要最佳化不會變更語義即可。

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

幾乎不會。指標指向介面值的情況只會在罕見、棘手的狀況發生,會涉及偽裝介面值的類型,以便延後評估。

傳遞指標指向介面值給預期為介面的函式,是很常見的錯誤。編譯器會對此錯誤提出警告,但此狀況仍可能會造成混淆,因為有時候指標是滿足介面所必要的(指標指向介面)。洞見如下:雖然指向具體類型的指標可以滿足介面,但有一個例外,即指標指向介面絕不會滿足介面

考量變數宣告:

var w io.Writer

列印函式 `fmt.Fprintf` 會將滿足 `io.Writer` 的值作為其第一個引數,而滿足 `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,使用指標接收器可能比較省。

接下來是相容性。如果類型中有些方法必須要有指標接收器的話,其他的也應該要,這樣不論類型怎麼用,方法組都維持相容性。詳細資訊請參閱方法組一節。

對基本類型、切片和小型structs之類的類型來說,值接收器非常省,所以除非方法的語意需要指標,否則值接收器既有效率又清楚。

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 會透過其 goroutines 和 channels 來支援此方法。例如,你可以調整你的程式結構,使其一次只有一個 goroutine 負責一段特定資料。此方法由最初的 Go 格言 總結如下:

不要透過共享記憶體來溝通。而是透過溝通來共享記憶體。

參閱 透過溝通來共享記憶體 程式範例及其 相關文章 以深入探討此觀念。

大型並行程序很可能會同時取用這兩個工具組。

為何我的程式無法隨著 CPU 增加而執行得更快?

程式是否會隨著 CPU 增加而執行得更快,取決於它所要解決的問題。Go 語言提供並行原語,例如 goroutines 和 channels,但是唯當基本問題內含並行性時,並行運算才可能實現平行性。本來性質上就連續的問題不可能藉由增加更多的 CPU 而加快速度,而可以分解成可平行執行的區塊的問題則可以加快速度,有時甚至是大幅度加快。

有時增加更多的 CPU 反而會讓程式變慢。在實際上,相較於執行有用程式運算,花費更多時間進行同步或溝通的程式可能會在使用多個 OS 執行緒時經歷效能上的劣化。這是因為在執行緒間傳遞資料時會涉及切換處理脈絡,而這會產生極大的成本,且此成本會隨著 CPU 增多而增加。例如,Go 規範的 質數篩選範例 雖然啟動很多 goroutines,但卻沒有顯著的平行性;此時增加執行緒 (CPU) 數量很可能會讓程式變慢,而不是變快。

要更詳細了解這個主題,請參閱標題為 Concurrency is not Parallelism 的討論。

我要怎麼控制 CPU 數量?

平行執行 Goroutine 可用的 CPUs 數量,由 GOMAXPROCS shell 環境變數控制,其預設值為現有的 CPU 核心數量。因此,程式中具有平行執行潛力的內容,在具備多重 CPU 的機器上,將會預設執行平行操作。如要變更要使用的平行 CPU 數量,請設定環境變數,或使用執行時期套件中,名稱相似的 function 來設定執行時期支援,以利用不同的執行緒數量。將其設定為 1 會消除實際平行性的可能性,並強制獨立的 Goroutine 輪流執行。

執行時期可以使用比 GOMAXPROCS 的值更多的執行緒,來處理多個未完成的 I/O 要求。GOMAXPROCS 僅影響實際可以同時執行的 Goroutine 的數量;可能還有任意多個執行緒因系統呼叫而遭阻擋。

Go 的 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 變數的同一執行個體,所以每個閉包都共用這個單一變數。當閉包執行時,它會列印 v 在呼叫 fmt.Println 時的值,但是自從 goroutine 啟動後,v 可能已被修改。為在發生問題之前協助偵測它和其他問題,請執行 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 有參數類型?

參數類型允許泛型程式設計,其中函式和資料結構的定義時根據類型,而這些類型會在這些函式和資料結構使用時稍後指定。例如,這些類型讓您可以撰寫函式,傳回任何已排序類型中兩個值的最小值,而無需為每一種可能的類型撰寫個別版本。如需更深入的說明和範例,請參閱部落格文章 為何需要泛型?

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 中,特性界限和類型之間的關聯必須明確定義,無論是在定義特性界限的程式庫中或定義類型的程式庫中。在 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)
    }
}

假如 xEmpty,這個程式碼如何運作?這時 x 似乎必須滿足所有三個測試,以及任何其他類型與任何其他形式。

呼叫這些方法時,會執行什麼程式碼?對於非泛型方法,編譯器會針對所有方法實作產生程式碼,並將它們連結到最終的程式中。但對於泛型方法,可能會出現數量無限多的方法實作,所以需要不同的策略。

總共有四個選擇

  1. 在連結時,列出所有可能的動態介面檢查,然後找出可以滿足這些檢查,卻缺少編譯方法的類型,接著再次呼叫編譯器新增這些方法。

    這會顯著地降低編譯速度,因為它需要在連結後停止並重複一些編譯。它將特別降低增量編譯的速度。更糟的是,新編譯的方法碼本身可能會有新的動態介面檢查,且這個程序必須重複進行。可以建構出程序永遠無法完成的範例。

  2. 實作某種類型的 JIT,於執行時期編譯所需的方法碼。

    Go 從純粹提前編譯的簡單性和可預測的效能中受益良多。我們不願僅為了實作一個語言功能而採用 JIT 的複雜性。

  3. 安排為每個一般方法輸出一個慢速後備,這個方法會使用函數表來針對型別參數上每個可能的語言運算進行處理,然後使用那個後備實作來進行動態測試。

    這個方法會使得某個一般方法使用一個意外的型別來參數化,比由編譯時期觀察到的型別來參數化同一方法慢得多。這將使得效能的可預測性降低許多。

  4. 定義一般方法無法以任何方式用來滿足介面。

    介面是 Go 程式設計中不可或缺的一部分。從設計的觀點來看,不允許一般方法滿足介面是不可接受的。

這些選擇沒有好的,因此我們選擇「以上皆非」。

使用具有型別參數的頂層函數,或將型別參數新增至接收者型別,代替具有型別參數的方法。

有關更多詳情,包括更多範例,請參閱提案

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

一般型別的方法宣告是用包含型別參數名稱的接收者來編寫的。也許是因為在呼叫網站中指定型別的語法類似,某些人認為這提供了透過在接收者中命名特定型別,例如 字串,來產生針對特定型別引數自訂方法的機制。

type S[T any] struct { f T }

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

這個會失敗,因為編譯器會將字串 string 這個字視為方法中型別引數的名稱。編譯器錯誤訊息會類似於「字串 s.f(字串型別的變數)上未定義運算子 +」。這可能會令人感到困惑,因為 + 運算子在預先宣告的型別 string 上執行良好,但這個宣告已經覆寫了 string 的定義(對於這個方法而言),而且運算子無法在 string 的那個不相關版本上執行。這樣覆寫預先宣告的名稱是有效的,但這樣做很奇怪,而且經常會出錯。

編譯器為什麼無法在我的程式中推斷型別引數?

有許多情況程式設計師可以輕易知道泛型類型或函數的類型引數,但語言並不允許編譯器推論出來。類型推論有意限制在於確保永遠不會混淆推論的類型為何。與其他語言的經驗顯示,在讀取與偵錯程式時,意外的類型推論可能導致相當的混淆。永遠可以使用指定呼叫中使用的明確類型引數。未來只要該規則保持簡單且清楚,就可以支援新的推論形式。

封裝與測試

我如何建立多檔封裝?

將封裝的所有原始檔單獨放在資料夾中。原始檔可以參考不同檔中的項目;不必向前宣告或建立標頭檔。

除了切割成多個檔案外,封裝的編譯和測試將像單一檔案的封裝一樣。

我如何撰寫單元測試?

在封裝原始檔相同的目錄中,建立一個以 _test.go 結尾的新檔案。在該檔案中,import "testing" 並撰寫表單函數

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

在該目錄中執行 go test。該指令碼會找出 Test 函數,建立測試二進位檔,並執行它。

請參閱 如何撰寫 Go 程式碼 文件、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 程式所需的關鍵功能,例如格式化輸入/輸出和網路通訊。它也包含網頁程式設計中重要的元素,例如密碼編譯和支援 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 會讓我們更難以導入一些 ABI 和相關變更,像是 Go 所需要但並非標準 C 設定一部份的堆疊管理。

雖然 Go 並非最初的目標,它後來證明是個實作 Go 編譯器的好語言。一開始不要求 Go 自舉啟動,讓它可以將其設計專注於原本的用例,也就是網路伺服器。如果我們很早便決定讓 Go 編譯自己,我們最後可能會得到一個更著重於編譯器建構的語言,這雖然是個有價值的目標,但並非我們最初的目標。

儘管 gc 有自己的實作,但 go/parser 套件中提供原生字元分析器和解析器,另外還有一個原生的 類型檢查器gc 編譯器使用這些函式庫的變體。

執行時期支援是如何實作的?

同樣又是由於自舉啟動的問題,執行時期程式碼最初大部分是用 C 編寫(一小部分組語),但後來已轉譯成 Go(某些組語部分除外)。Gccgo 的執行時期支援使用 glibcgccgo 編譯器使用一種名為分割堆疊的技術實作 goroutine,這項技術受到最近對 gold 連結器的修改支援。Gollvm 也是建立在對應的 LLVM 基礎架構上。

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

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

一個在 Linux 上使用 gcc 編譯和靜態連結的簡單 C「hello, world」程式,包括實作 printf,大小約為 750 kB。一個使用 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])也跟著消除了。這個簡化相當顯著。至於後置和前置,兩者都沒有問題,但後置版本比較傳統;對前置的堅持是由一個語言的程式庫 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 剖析工具的示範。