常見問題 (FAQ)
起源
初衷是什麼?
2007年Go語言誕生時,程式設計世界與今天大不相同。當時生產環境中的軟體通常使用C++或Java撰寫,GitHub還不存在,大多數電腦還不是多處理器,除了Visual Studio和Eclipse之外,幾乎沒有什麼整合開發環境(IDE)或其他高階工具,更不用說在網路上免費提供了。
與此同時,我們對使用當時語言及其相關建構系統來建構大型軟體專案所需的過度複雜性感到沮喪。自C、C++和Java等語言最初開發以來,電腦的速度已大幅提升,但程式設計本身並沒有取得太大進展。此外,多處理器正變得無所不在,但大多數語言在如何高效安全地程式設計方面提供的幫助很少。
我們決定退一步,思考隨著技術發展,未來幾年哪些重大問題將主導軟體工程,以及一種新語言可能如何解決這些問題。例如,多核CPU的興起表明,語言應提供對某種並行或平行計算的一等支援。為了使資源管理在大型並行程式中易於處理,垃圾回收(或至少是某種安全的自動記憶體管理)是必需的。
這些考量促成了一系列討論,Go由此誕生——最初是一系列想法與需求,隨後發展成了一門語言。 一個首要目標是:Go應該透過支援工具化、自動化格式化程式碼等日常任務、並消除大型程式碼庫帶來的障礙,來幫助工作中的程式設計師做更多事情。
關於Go的目標及其實現方式(至少是接近實現的方式),在文章《Go之於Google:服務於軟體工程的語言設計》中有更為詳盡的描述。
專案歷史是怎樣的?### 專案歷史
2007年9月21日,Robert Griesemer、Rob Pike和Ken Thompson在白板上勾勒出了新語言的設計目標。
幾天內,團隊就明確了具體計畫與方向。隨後在兼職推進其他工作的同時,持續進行語言設計。
至2008年初,Ken開始著手開發編譯器原型(輸出C碼)用於驗證概念。
同年年中,該專案升級為全職專案,並具備了開發生產級編譯器的條件。
2008年5月,Ian Taylor基於草案規範,獨立為Go語言開發了GCC前端。
Russ Cox於2008年末加入,推動語言特性與標準函式庫從原型走向成熟。
2009年11月10日,Go正式成為開放原始碼專案。
社群成員貢獻了大量程式碼、討論與創意。
如今全球已有數百萬Go程式設計師(Gopher),數量持續增長。
Go的成功已遠遠超越團隊最初的預期。
地鼠吉祥物的起源
吉祥物和標誌由Renée French 設計,她也是Plan 9系統兔子Glenda 的設計者。
官方部落格文章 解釋了該地鼠形象源自她多年前為WFMU電台的T恤設計的圖案。
該標誌和吉祥物採用知識共享署名4.0授權協議授權。
地鼠有專門的角色設定圖,詳細說明了其特徵及標準繪製方法。該設定圖首次公開於2016年Gophercon大會上Renée的演講。
它具有獨特特徵——是專屬的"Go地鼠",而非普通地鼠形象。
語言名稱是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》,該文深入探討了設計動機,並對此FAQ中諸多問題給出更詳實的背景說明。 (註:採用技術白皮書式結構化表達,透過對比表格突顯創新點;關鍵術語使用粗體強調;保留原文超連結;"fast" 譯為 "極致開發體驗" 以完整傳遞 "秒級構建" 的隱含意義)
語言淵源
Go主要源自C語言家族(基礎語法),同時吸收:
- Pascal/Modula/Oberon家族的宣告語法與包機制
- CSP理論衍生語言(如Newsqueak/Limbo)的並行思想
但Go是全方位創新的語言——每個細節都基於對"程式設計師實際需求"的深度思考,旨在提升程式設計效率(以及趣味性)。
設計哲學
核心矛盾
設計Go時,Java/C++是主要伺服器端主流語言(至少在Google如此)。但團隊認為:
- 這些語言需要過多模板程式碼
- 程式設計師為效率轉向Python等動態語言,卻犧牲了類型安全和執行效率
Go的突破點在於:單一語言同時實現高效、安全與靈活
減法原則
- 零冗餘設計:無前置宣告/標頭檔,所有元素僅宣告一次
- 智能初始化:自動型別推導(
:=宣告並初始化) - 極簡語法:輕量關鍵字,消除重複程式碼(如
new(foo.Foo)可簡化為myFoo := &Foo{}) - 無型別繼承:型別即介面,無需顯式宣告關係
- 正交性:
✓ 方法可繫結任意型別
✓ 結構體承載資料,介面定義抽象
✓ 組合優於繼承
這些設計使Go在保持高表達力的同時,不增加認知負擔,實現"簡單即高效"。
(註:採用分層標題突顯邏輯;關鍵創新點用粗體標記;用程式碼範例對比說明語法優勢;"orthogonal" 譯為 "正交性" 保留數學概念;透過✓符號列表提升可讀性)
應用實踐
Google內部是否使用Go?
是的。Go在Google內部生產環境廣泛使用。典型例子如下載伺服器dl.google.com,負責分發Chrome二進位包、apt-get等大型安裝包。
雖然Go並非Google唯一語言(遠非如此),但它是多個核心領域的關鍵語言:
- 站點可靠性工程(SRE)
- 大型資料處理
- 谷歌雲端計算平台核心元件
哪些公司在用Go?
Go在全球快速增長,尤其在(但不限於)雲端運算領域:
能否與C/C++程式交互?
技術可行但有代價:
- 需專用的介面層,且會破壞Go的記憶體安全與堆疊管理特性
- 僅建議必要時呼叫C庫,且需謹慎處理風險
多編譯器支援:
SWIG工具可進一步支援C++庫集成。
IDE支援如何?
Go雖無官方客製IDE,但程式碼分析友善的設計使得主流編輯器/IDE均提供良好支援:
- 官方方案:
gopls語言伺服器(LSP協定) - 主流支援:VSCode、IntelliJ(GoLand)、Eclipse、Vim、Emacs等均有外掛或原生支援
支援Protocol Buffers嗎?
透過獨立開放原始碼專案實現:github.com/golang/protobuf
提供編譯外掛及配套函式庫。
設計哲學
Go需要執行時嗎?
是的,但與傳統理解不同。Go的執行時函式庫(通常簡稱 runtime)提供以下核心功能:
- 垃圾回收
- 並發排程
- 堆疊管理
與C的libc類似,但不包含虛擬機。Go程式會直接編譯為本地機器碼(或JavaScript/WebAssembly),因此"runtime"在Go中僅指提供語言服務的函式庫,而非託管執行環境。
關於Unicode識別字
Go設計時有意突破ASCII限制,允許使用Unicode字母/數字作為識別字。但存在兩個重要限制:
- 排除組合字元:如梵文等組合文字無法使用
- 匯出規則衝突:因匯出識別字需首字元大寫,導致部分語言字元(如中文)永遠無法匯出
現行解決方案(如X日本語)並不理想。未來可能參考Unicode TR31 建議改進,但需保持向後相容性與大小寫可見性規則。
為何沒有特性 X?
Go設計聚焦於:
- 程式設計愉悅性
- 編譯速度
- 概念正交性
- 並發/垃圾回收支援
若你鍾愛的特性缺失,可能是因為它:
- 不符合設計哲學
- 影響編譯效率
- 破壞系統模型簡潔性
建議深入探索Go現有特性,或許能找到更優雅的替代方案。
泛型何時引入?
Go 1.18 正式加入型別參數支援。詳情見:
為何初期沒有泛型?
- 原始目標:建構易維護的服務端程式
- 早期重點:可維護性、可讀性、並發
- 泛型會增加型別系統和執行時複雜度,團隊花費數年尋找平衡方案
為什麼不用異常?
Go認為try-catch-finally會導致:
- 程式碼結構複雜化
- 錯誤過度分類(如檔案開啟失敗)
Go的方案:
- 多回傳值錯誤處理:天然支援錯誤碼
- 內建
panic/recover:僅處理致命錯誤(見Defer, Panic, and Recover) - 錯誤即值:充分利用Go特性建構錯誤處理鏈(見Errors are values)
為什麼沒有斷言?
斷言雖方便,但易導致程式設計師逃避錯誤處理。Go要求明確處理錯誤,確保:
- 服務持續執行(非致命錯誤不崩潰)
- 錯誤資訊直接明確(減少堆疊分析負擔)
為何基於CSP模型?
傳統並發(如Pthreads)因過度關注底層細節(鎖、條件變數等)而複雜。CSP模型優勢:
- 提供高階抽象
- 保持底層效率
- 與程序式語言天然契合(如Occam/Erlang)
Go的並發原語(尤其是一等通道物件)源自CSP家族分支。
為何用Goroutines而非執行緒?
Goroutines讓並發更易用:
- 輕量:初始堆疊僅幾KB,可動態伸縮
- 自動排程:執行時自動將阻塞的協程遷移到可用執行緒
- 低成本:每函式呼叫約3條指令開銷
- 高並發:單行程可支援數十萬Goroutines(執行緒通常僅數千)
為什麼map操作不設計成原子性的?
經過長期討論,團隊認為map的典型使用場景不需要多goroutine安全訪問。若確實需要(通常map是某個更大同步資料結構的一部分),強制所有map操作加鎖會:
- 拖慢大多數無需並發安全的程式
- 僅對少數場景提供安全性
需注意事項:非受控的map更新會導致程式崩潰。但語言本身不禁止原子更新——在需要時(如託管不可信程式),執行時可實現內部鎖機制。
安全存取規則:
- ✅ 並發讀安全:所有goroutine僅查詢/遍歷map(
for range) - ❌ 寫時危險:存在賦值/刪除操作時需外部同步
輔助工具:
- 執行時期檢測:部分實作會主動回報並行修改錯誤
sync.Map:適用於靜態快取等特定情境(非常規 map 替代方案)
會採納我的語言修改建議嗎?
雖然社群討論活躍(見郵件列表),但絕大多數修改提案未被採納。原因包括:
- 相容性承諾:Go1相容保證禁止破壞現有程式碼
- 設計哲學:需符合《Go之於Google》中的核心目標
- 變更成本:即使相容Go1,也可能因引入複雜性被拒
未來大版本可能不相容Go1,但會:
- 保持變更最小化
- 提供舊程式碼自動遷移路徑
類型系統
Go是物件導向語言嗎?
是,也不是。關鍵特性對比:
優勢:Go的"物件"比C++/Java更輕量,且支援:
- 多介面實作
- 零方法介面(如
interface{}) - 事後擴充介面(無需修改原型別)
如何實作動態方法分派?
僅透過介面實作:
- 介面方法:動態分派
- 結構體/具體型別方法:靜態解析
為什麼沒有類型繼承?
傳統OOP中類型關係討論過於複雜,Go採用隱式介面滿足:
- 型別自動滿足包含其方法子集的介面
- 無需明確宣告關係
- 支援多介面、零方法介面
- 可事後加入介面(如測試時)
典型案例:io.Writer介面驅動了fmt.Fprintf/bufio/image等套件的解耦設計。
為什麼len是函數而非方法?
設計權衡結果:
- 作為函數實作更簡潔
- 避免基礎型別(如int)的介面複雜性
- 實務上無不良影響
為什麼Go不支援方法和運算子多載?
如果方法調度不需要進行類型比對,將會簡化很多。根據其他語言的經驗,我們發現雖然同名但不同簽名的多種方法偶爾很有用,但在實務中也可能造成混淆和脆弱性。僅透過名稱進行比對並要求型別一致,是Go類型系統中一個主要的簡化決策。
至於運算子多載,它看起來更像是一種便利,而非絕對必要。再次強調,沒有它,事情會變得更簡單。
為什麼Go沒有「implements」宣告?
Go的型別透過實作介面的方法來實作介面,僅此而已。這個特性允許在不修改現有程式碼的情況下定義和使用介面。它實作了一種結構型別,促進了關注點分離,提高了程式碼重用性,並使得隨著程式碼發展出現模式時更容易建構。 介面的語意是Go感覺靈活、輕量級的主要原因之一。
更多詳細資訊,請參閱關於類型繼承的問題。
如何確保我的型別滿足介面?
你可以透過嘗試使用T或指向T的指標的零值進行賦值,讓編譯器檢查型別T是否實作了介面I:
如果T(或對應的*T)未實作I,編譯時會發現這個錯誤。
如果你希望介面的使用者明確宣告他們實作了該介面,可以在介面的方法集合中加入一個具有描述名稱的方法。例如:
然後,型別必須實作ImplementsFooer方法才能成為Fooer,清楚地記錄這一事實,並在go doc的輸出中宣告。
大多數程式碼不使用這種限制,因為它們限制了介面概念的實用性。但有時,它們對於解決類似介面之間的歧義是必要的。
為什麼型別T不滿足Equal介面?
考慮這個簡單的介面,表示一個可以與另一個值進行比較的物件:
以及這個型別T:
與某些多型型別系統中的類似情況不同,T並未實作Equaler。
T.Equal的參數型別是T,而不是嚴格要求的Equaler型別。
在Go中,型別系統不會自動提升Equal的參數;這是程式設計師的責任,如型別T2所示,它確實實作了Equaler:
但即使這樣,也與其他型別系統不同,因為在Go中,任何滿足Equaler的型別都可以作為T2.Equal的參數,並且在執行時期我們必須檢查參數是否為T2型別。一些語言在編譯時期就能做出這種保證。
相關的例子反過來:
在Go中,T3不滿足Opener,儘管在其他語言中可能會。
雖然Go的型別系統在這種情況下為程式設計師做的事情較少,但缺乏子型別使得關於介面滿足的規則非常簡單:函數的名稱和簽名是否與介面的完全一致? Go的規則也很容易有效地實作。我們認為這些好處彌補了自動型別提升的缺失。
我能將[]T轉換為[]interface嗎?
不能直接轉換。
語言規範禁止這樣做,因為這兩種型別在記憶體中的表示方式不同。有必要將元素逐個複製到目標切片。以下範例將int的切片轉換為interface{}的切片:
如果T1和T2有相同的基本型別,我能將[]T1轉換為[]T2嗎?
以下程式碼樣本的最後一行無法編譯。
在Go中,型別與方法是密切相關的,每個命名型別都有一個(可能為空的)方法集。 通用規則是,你可以更改要轉換的型別名稱(從而可能更改其方法集),但不能更改複合型別的元素名稱(和方法集)。Go要求你明確地進行型別轉換。
為什麼我的nil錯誤值不等於nil?
在內部,介面被實作為兩個元素,型別T和值V。
V是一個具體值,如int、struct或指標,從不是介面本身,且具有型別T。例如,如果我們把整數值3儲存在一個介面中,得到的介面值在示意圖上是(T=int, V=3)。值V也稱為介面的動態值,因為給定的介面變數在程式執行過程中可能持有不同的值V(和相應的型別T)。
只有當V和T都未設定時,介面值才是nil,即(T=nil, V未設定)。
特別地,一個nil介面將始終持有nil型別。如果我們把一個*int型別的nil指標儲存在介面值中,內部型別將是*int,不管指標的值是多少:(T=*int, V=nil)。因此,這樣的介面值將是非nil,即使內部的指標值V是nil。
這種情況可能會令人困惑,並且當一個nil值被儲存在介面值中時會發生,例如一個error返回值:
如果一切順利,函數返回一個nil p,所以返回值是一個持有(T=*MyError, V=nil)的error介面值。這意味著如果呼叫者將返回的錯誤與nil比較,即使沒有壞事發生,它看起來也總是一個錯誤。要向呼叫者返回一個正確的nil error,函數必須明確返回nil:
對於返回錯誤的函數來說,最好始終在其簽名中使用error型別(如我們上面所做的),而不是具體型別如*MyError,以幫助確保錯誤被正確建立。例如,os.Open
返回一個error,即使不是nil,它總是具體型別
*os.PathError。
只要使用介面,這裡描述的情況就可能發生。只要記住,如果任何具體值已經儲存在介面中,介面就不會是nil。
更多資訊,請參閱反射定律。
為什麼零大小型別表現奇怪?
Go支援零大小型別,例如沒有欄位的結構體(struct{})或沒有元素的陣列([0]byte)。
零大小型別中無法儲存任何值,但在不需要值的情況下,這些型別有時很有用,例如在map[int]struct{}或具有方法但沒有值的型別中。
具有零大小型別的不同變數可能會被放置在記憶體中的同一位置。 這是安全的,因為那些變數中不能儲存任何值。
此外,語言對指向兩個不同的零大小變數的指標是否相等不做任何保證。
根據程式的編譯和執行方式,這種比較甚至可能在程式的一個點返回true,而在另一個點返回false。
與零大小型別相關的另一個問題是,指向零大小結構體欄位的指標不得與指向記憶體中另一個不同物件的指標重疊。 這可能會在垃圾回收器中引起混淆。 這意味著,如果結構體的最後一個欄位是零大小,結構體會被填充,以確保指向最後一個欄位的指標不與緊隨結構體的記憶體重疊。 因此,這個程式:
在大多數Go實作中將列印2,而不是1。
為什麼沒有像C那樣的無標記聯合體?
無標記聯合體會違反Go的記憶體安全保證。
為什麼Go沒有變體型別?
變體型別,也稱為代數型別,提供了一種指定值可能是其他型別之一的方法,但僅限於那些型別。一個常見的系統程式設計範例是,指定一個錯誤是網路錯誤、安全錯誤或應用程式錯誤,並允許呼叫者透過檢查錯誤的型別來區分問題的來源。另一個例子是語法樹,其中每個節點可以是不同的型別:宣告、語句、指派等。
我們考慮過為Go加入變體型別,但經過討論後決定不加入,因為它們在介面方面有混淆的重疊。如果變體型別的元素本身是介面,會發生什麼?
此外,語言已經涵蓋了一些變體型別所解決的問題。錯誤範例很容易透過使用介面值來保存錯誤,並使用型別開關來區分情況。語法樹範例也可以實作,儘管不那麼優雅。
為什麼Go沒有協變結果型別?
協變結果型別意味著像
這樣的介面會被
這個方法滿足,因為Value實作了空介面。
在Go中,方法型別必須完全匹配,所以Value不實作Copyable。
Go將型別的功能(其方法)與型別的實作分開。如果兩個方法返回不同的型別,它們就不是在做相同的事情。希望有協變結果型別的程式設計師通常試圖透過介面表達型別階層。在Go中,更自然的是在介面和實作之間有清晰的區隔。
值
為什麼Go不提供隱式數字轉換?
C中數值型別之間自動轉換的便利性被它引起的混淆所抵消。運算式什麼時候是無符號的?值有多大?它會溢出嗎?結果是可移植的嗎,獨立於執行的機器? 它還使編譯器複雜化;C的「通常算術轉換」不容易實作,並且在不同架構上不一致。出於可移植性的原因,我們決定以程式碼中一些明確轉換為代價,使事情變得清晰和直接。 Go中常量的定義——無符號和大小註解的任意精度值——大大改善了這種情況。
一個相關的細節是,與C不同,即使int是64位型別,int和int64也是不同的型別。int型別是通用的;如果你關心整數持有多少位,Go鼓勵你明確指定。
Go中的常量是如何運作的?
儘管Go對不同型別的變數之間的轉換很嚴格,但語言中的常量要靈活得多。
文字常量如23、3.14159和math.Pi佔據一種理想數字空間,具有任意精度,沒有溢出或下溢。
例如,math.Pi的值在原始碼中指定為63位小數,涉及該值的常量表達式保持的精度超過float64能容納的。
只有當常量或常量表達式被分配給變數——程式中的一個記憶體位置時,它才成為一個「電腦」數字,具有通常的浮點屬性和精度。
此外, 因為它們是數字,不是型別化的值,Go中的常量可以比變數更自由地使用,從而緩解了嚴格轉換規則的一些尷尬。 人們可以寫出表達式,如
而編譯器不會抱怨,因為理想數字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已經建立了圍繞命名、佈局和文件組織的慣例,以指導決策。
文件Effective 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進行身份驗證,或使用SSH代替HTTPS。
要透過HTTPS進行身份驗證,你可以在git查詢的$HOME/.netrc檔案中加入一行:
對於GitHub帳戶,密碼可以是個人訪問令牌。
Git還可以設定為對符合給定前綴的URL使用SSH代替HTTPS。
例如,要對所有GitHub訪問使用SSH,
在你的~/.gitconfig中加入這些行:
當使用私人模組,但對依賴項使用公共模組代理時,你可能需要設定GOPRIVATE。
有關詳細資訊和附加設定,請參閱私人模組。
我應該如何以「go get」管理套件版本?
Go工具鏈有一個內建系統,用於管理相關套件的版本集,稱為模組。 模組在Go 1.11中引入,自1.14以來已準備好用於生產。
要建立一個使用模組的專案,執行go mod init。
此命令建立一個追蹤依賴項版本的go.mod檔案。
要新增、升級或降級依賴項,執行go get:
有關入門的更多資訊,請參閱教學:建立模組。
有關使用模組管理依賴項的指南,請參閱開發模組。
模組中的套件應隨著演變保持向後相容性,遵循匯入相容性規則:
如果舊套件和新套件有相同的匯入路徑,
新套件必須與舊套件向後相容。
Go 1相容性指南在這裡是一個很好的參考: 不要刪除匯出的名稱,鼓勵標記的複合字面量等。 如果需要不同的功能,加入一個新名稱,而不是更改舊名稱。
模組透過語意版本控制和語意匯入版本控制將其編纂。
如果需要破壞相容性,以新的主要版本發布模組。
主要版本2及以上的模組需要在其路徑中包含一個主要版本後綴(如/v2)。
這保留了匯入相容性規則:模組不同主要版本的套件具有不同的路徑。
指標和分配
函數參數什麼時候按值傳遞?
與C系列中的所有語言一樣,Go中的一切都是按值傳遞的。
也就是說,函數總是獲得被傳遞事物的副本,就像有一個指派語句將值指派給參數一樣。
例如,將int值傳遞給函數會複製int,傳遞指標值會複製指標,但不會複製它指向的資料。
(關於這對方法接收器的影響的討論,請參見後續部分。)
映射和切片值的行為類似於指標:它們是指包含指向底層映射或切片資料指標的描述符。 複製映射或切片值不會複製它指向的資料。 複製介面值會複製儲存在介面值中的事物。 如果介面值包含結構體,複製介面值會複製結構體。 如果介面值包含指標,複製介面值會複製指標,但不會複製它指向的資料。
注意,這個討論是關於操作的語意。 實際實作可能會應用最佳化以避免複製,只要這些最佳化不改變語意。
什麼時候應該使用指向介面的指標?
幾乎從不。指向介面值的指標只在罕見、複雜的情況下出現,這些情況涉及延遲評估時隱藏介面值的型別。
將指向介面值的指標傳遞給期望介面的函數是一個常見錯誤。編譯器會抱怨這個錯誤,但情況可能仍然令人困惑,因為有時指標是滿足介面所必需的。 關鍵是要認識到,儘管指向具體型別的指標可以滿足介面,但有一個例外指向介面的指標永遠不能滿足介面。
考慮變數宣告,
列印函數fmt.Fprintf將其第一個參數作為滿足io.Writer的值——實作規範Write方法的東西。因此我們可以寫
然而,如果我們傳遞w的地址,程式將無法編譯。
唯一的例外是,任何值,甚至是指向介面的指標,都可以指派給空介面型別(interface{})的變數。
即便如此,如果值是指向介面的指標,這幾乎肯定是一個錯誤;結果可能令人困惑。
我應該定義值上的方法還是指標上的方法?
對於不習慣指標的程式設計師來說,這兩個例子之間的區別可能會令人困惑,但實際上情況非常簡單。
在定義型別上的方法時,接收器(上面例子中的s)的行為就像它是方法的參數一樣。
因此,將接收器定義為值還是指標,與函數參數應該是值還是指標是同一個問題。有幾個考慮因素。
首先,也是最重要的,方法是否需要修改接收器?
如果需要,接收器必須是指標。
(切片和映射作為引用,所以他們的故事有點微妙,但例如,要更改方法中切片的長度,接收器仍然必須是指標。)
在上面的例子中,如果pointerMethod修改了s的欄位,呼叫者會看到這些更改,但valueMethod是用呼叫者參數的副本呼叫的(這就是傳遞值的定義),所以它所做的更改對呼叫者不可見。
順便說一下,在Java中,方法接收器一直是隱式的指標,儘管它們的指標性質有些被掩蓋了(最近的進展正在為Java帶來值接收器)。 Go中的值接收器是不尋常的。
第二是效率的考慮。如果接收器很大,比如一個大的struct,使用指標接收器可能更便宜。
接下來是一致性。如果型別的一些方法必須有指標接收器,其餘的方法也應該有,所以無論如何使用型別,方法集都是一致的。 詳見方法集部分。
對於基本型別、切片和小的structs這樣的型別,值接收器非常便宜,因此除非方法的語意需要指標,否則值接收器是高效且清晰的。
new和make有什麼區別?
簡而言之:new分配記憶體,而make初始化切片、映射和通道型別。
更多細節請參見Effective Go的相關部分。
在64位機器上int的大小是多少?
int和uint的大小是特定於實作的,但在給定平台上它們是相同的。
為了可移植性,依賴於特定值大小的程式碼應該使用明確大小的型別,如int64。
在32位機器上,編譯器預設使用32位整數,而在64位機器上,整數有64位。
(歷史上,這並不總是正確的。)
另一方面,浮點標量和複雜型別總是有大小的(沒有float或complex基本型別),因為程式設計師在使用浮點數時應該意識到精度。
(未型別的)浮點常量的預設型別是float64。
因此foo := 3.0宣告了一個float64型別的變數foo。
對於由(未型別的)常量初始化的float32變數,變數型別必須在變數宣告中明確指定:
或者,常量必須透過轉換給定型別,如foo := float32(3.0)。
我怎麼知道變數是分配在堆上還是堆疊上?
從正確性的角度來看,你不需要知道。 Go中的每個變數只要還有對它的引用就存在。 實作選擇的儲存位置與語言的語意無關。
儲存位置確實對撰寫高效的程式有影響。 當可能時,Go編譯器會將函數的局部變數分配在該函數的堆疊幀中。 但是,如果編譯器無法證明函數返回後變數不再被引用,那麼編譯器必須在垃圾回收的堆上分配變數,以避免懸空指標錯誤。 此外,如果局部變數非常大,將其儲存在堆上而不是堆疊上可能更有意義。
在當前編譯器中,如果變數的地址被取走,該變數就是堆上分配的候選。 但是,基本的逃逸分析能識別出一些情況,即這些變數在函數返回後將不再存活,可以駐留在堆疊上。
為什麼我的Go行程使用這麼多虛擬記憶體?
Go記憶體分配器為分配保留了一大塊虛擬記憶體作為競技場。 這個虛擬記憶體是特定Go行程本地的;保留不會剝奪其他行程的記憶體。
要查找分配給Go行程的實際記憶體量,使用Unix的top命令並查看RES (Linux) 或
RSIZE (macOS) 欄。
待辦事項: 查找這在Windows上是如何工作的。
並發
哪些操作是原子的?互斥鎖呢?
Go中操作的原子性描述可以在Go記憶體模型文件中找到。
低級同步和原子原語在sync和sync/atomic套件中可用。 這些套件適用於簡單的任務,如增加引用計數或保證小規模互斥。
對於更高級別的操作,如並發伺服器之間的協調,更高級的技術可以帶來更好的程式,Go透過其goroutines和channels支援這種方法。 例如,你可以建構程式,使每次只有一個goroutine負責特定資料。 這種方法由原始的Go諺語總結,
不要透過共享記憶體來通訊。相反,透過通訊來共享記憶體。
有關這個概念的詳細討論,請參見透過通訊共享記憶體程式碼漫步及其相關文章。
大型並發程式可能從這兩個工具套件中借用。
為什麼我的程式在增加CPU數量後執行速度沒有提升?
程式是否在增加CPU數量後執行得更快,取決於它解決的問題。 Go語言提供了並發原語,如goroutines和channels,但只有當下層問題本質上是並行的時候,並發才能實現平行性。 本質上是順序的問題無法透過增加更多CPU來加速,而那些可以分解為可以並行執行的片段的問題可以加速,有時甚至是顯著加速。
有時增加更多CPU可能會使程式變慢。 實際上,如果程式在同步或通訊上花費的時間比做有用計算的時間多,使用多個OS執行緒時可能會經歷效能下降。 這是因為在執行緒之間傳遞資料涉及切換上下文,這有顯著成本,而這個成本可能會隨著更多CPU的增加而增加。 例如,Go規範中的素數篩例子儘管啟動了多個goroutines,但沒有顯著的平行性;增加執行緒(CPU)數量更可能使其變慢而不是變快。
有關這個主題的更多細節,請參見題為並發不是平行的演講。
我如何控制CPU的數量?
同時執行goroutines可用的CPU數量由GOMAXPROCS shell環境變數控制,其預設值是可用CPU核心的數量。
因此,具有平行執行潛力的程式在多CPU機器上預設應該實現平行。
要更改要使用的平行CPU數量,設定環境變數或使用執行時期套件中同名函數來設定執行時期支援以使用不同數量的執行緒。
將其設定為1將消除真正的平行性可能性,迫使獨立的goroutines輪流執行。
執行時期可以分配比GOMAXPROCS值更多的執行緒來服務多個未完成的I/O請求。
GOMAXPROCS只影響實際同時執行的goroutines數量;任意更多的goroutines可能在系統呼叫中被阻塞。
Go的goroutine排程器在平衡goroutines和執行緒方面做得很好,甚至可以搶佔goroutine的執行,以確保同一執行緒上的其他goroutines不會餓死。
然而,它並不完美。
如果你看到效能問題,基於每個應用程式設定GOMAXPROCS可能會有所幫助。
為什麼沒有goroutine ID?
Goroutines沒有名字;它們只是匿名的工人。
它們不向程式設計師暴露任何唯一識別符、名稱或資料結構。
有些人對此感到驚訝,期望go語句返回一些可以用來稍後訪問和控制goroutine的項目。
Goroutines匿名的根本原因是為了在撰寫並發程式碼時可以使用完整的Go語言。 相比之下,當執行緒和goroutines被命名時,使用模式會限制使用它們的庫能做什麼。
這裡是困難之處的說明。
一旦一個人給一個goroutine命名並圍繞它建立模型,它就變得特殊,一個人會傾向於將所有計算與那個goroutine關聯,忽略使用多個可能共享的goroutines進行處理的可能性。
如果net/http套件將每個請求的狀態與一個goroutine關聯,用戶端將不能在使用請求時啟動更多goroutines。
此外,對於圖形系統庫這樣的庫的經驗表明,要求所有處理都在「主執行緒」上發生,在並發語言中部署時這種方法可能多麼尷尬和限制性。 特殊執行緒或goroutine的存在迫使程式設計師扭曲程式以避免意外操作錯誤執行緒導致的崩潰和其他問題。
對於那些特定goroutine確實特殊的案例,語言提供了諸如channels這樣的特性,可以以靈活的方式與之互動。
函數和方法
為什麼T和*T有不同的方法集?
正如Go規範所說,型別T的方法集由所有接收器型別為T的方法組成,而相應指標型別*T的方法集由所有接收器為*T或T的方法組成。
這意味著*T的方法集包括T的方法集,但不是反過來。
這種區別出現是因為如果介面值包含指標*T,方法呼叫可以透過解引用指標獲得值,但如果介面值包含值T,方法呼叫沒有安全的方式獲得指標。
(這樣做將允許方法修改介面內值的內容,這是語言規範不允許的。)
即使在編譯器可以取值的地址以傳遞給方法的情況下,如果方法修改值,更改將在呼叫者中遺失。
作為例子,如果下面的程式碼是有效的:
它會將標準輸入複製到buf的副本中,而不是buf本身。
這幾乎從來不是期望的行為,因此被語言禁止。
閉包作為goroutines執行時會發生什麼?
由於循環變數的工作方式,在Go 1.22版本之前(有關更新,請參見本節末尾),使用閉包和並發時可能會引起一些困惑。考慮以下程式:
人們可能會錯誤地期望看到a, b, c作為輸出。
你看到的卻可能是c, c, c。
這是因為循環的每次迭代都使用變數v的同一實例,所以每個閉包共享那個單一變數。當閉包執行時,它列印fmt.Println執行時v的值,但v可能自goroutine啟動以來已被修改。為了在問題發生前幫助檢測這些問題,執行go vet。
為了在啟動每個閉包時綁定v的當前值,必須修改內部循環以在每次迭代中建立新變數。
一種方法是將變數作為參數傳遞給閉包:
在這個例子中,v的值作為參數傳遞給匿名函數。然後該值在函數內部作為變數u可訪問。
更簡單的方法是只需建立一個新變數,使用一種在Go中可能看起來奇怪但完全有效的宣告風格:
這種語言行為,不為每次迭代定義新變數,後來被視作一個錯誤,並在Go 1.22中得到解決,它確實為每次迭代建立新變數,消除了這個問題。
控制流
為什麼Go沒有?:運算子?
Go中沒有三元測試操作。你可以使用以下程式碼來達到相同的結果:
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中,約束對於所有類型參數都是強制的。 C20概念表示為必須與類型參數一起編譯的小程式碼片段。 Go約束是定義所有允許類型參數集合的介面類型。
C++支援模板元編程;Go不支援。 實際上,所有C++編譯器都在實例化點編譯每個模板;如上所述,Go可以並且確實對不同實例化使用不同的方法。
-
Rust
Rust版本的約束被稱為trait bounds。 在Rust中,trait bound和類型之間的關聯必須明確宣告,要么在定義trait bound的crate中,要么在定義類型的crate中。 在Go中,類型參數隱式滿足約束,就像Go類型隱式實作介面類型一樣。 Rust標準庫為標準操作(如比較或加法)定義了標準traits;Go標準庫沒有,因為這些可以透過介面類型在用戶程式碼中表示。唯一的例外是Go的
comparable預定義介面,它捕獲了類型系統中無法表示的屬性。 -
Python
Python不是一種靜態類型語言,所以可以說所有Python函數預設都是泛型的:它們總是可以用任何類型的值呼叫,任何類型錯誤都在執行時期檢測。
為什麼Go在類型參數列表中使用方括號?
Java和C++在類型參數列表中使用尖括號,如Java的List<Integer>和C++的std::vector<int>。
然而,這個選項對Go來說不可用,因為它導致了一個語法問題:當解析函數內的程式碼時,例如v := F<T>,在看到<的那一刻,我們無法確定是看到實例化還是使用<運算子的表達式。
沒有類型資訊,這非常難以解決。
例如,考慮這樣的語句
沒有類型資訊,不可能決定賦值的右邊是一對表達式(w < x and y > z),還是一個返回兩個結果的泛型函數實例化和呼叫((w<x, y>)(z))。
Go的一個關鍵設計決策是解析可以在沒有類型資訊的情況下進行,這在泛型中使用尖括號時似乎是不可能的。
Go在使用方括號方面不是唯一或原創的;還有其他語言如Scala也在泛型程式碼中使用方括號。
為什麼Go不支援具有類型參數的方法?
Go允許泛型類型具有方法,但除了接收器之外,這些方法的參數不能使用參數化類型。 我們不預期Go會加入泛型方法。
問題在於如何實作它們。
具體來說,考慮檢查介面中的值是否實作了另一個具有額外方法的介面。
例如,考慮這個類型,一個空結構體,具有一個泛型Nop方法,返回其參數,對於任何可能的類型:
現在假設一個Empty值儲存在any中並傳遞給其他程式碼,檢查它能做什麼:
如果x是Empty,這段程式碼如何運作?
似乎x必須滿足所有三個測試,以及任何其他類型的其他形式。
呼叫這些方法時執行什麼程式碼? 對於非泛型方法,編譯器為所有方法實作產生程式碼並將它們連結到最終程式中。 但對於泛型方法,可以有無限多的方法實作,所以需要不同的策略。
有四個選擇:
-
在連結時,列出所有可能的動態介面檢查,然後尋找滿足這些檢查但缺少已編譯方法的類型,然後重新呼叫編譯器以加入這些方法。
這將使建置顯著變慢,需要在連結後停止並重複一些編譯。它會特別減慢增量建置。更糟的是,新編譯的方法程式碼本身可能有新的動態介面檢查,這個過程必須重複。可以建立例子,其中這個過程永遠不會結束。
-
實作某種JIT,在執行時期編譯所需的方法程式碼。
Go從純粹提前編譯的簡單性和可預測效能中獲益匪淺。 我們不願意僅僅為了實作一個語言特性而承擔JIT的複雜性。
-
安排為每個泛型方法發出一個慢速回退,使用類型參數的每個可能語言操作的函數表,然後對動態測試使用那個回退實作。
這種方法會使由意外類型參數化的泛型方法比由編譯時觀察到的類型參數化的相同方法慢得多。 這將使效能變得非常不可預測。
-
定義泛型方法根本不能用於滿足介面。
介面是Go程式設計的重要組成部分。 不允許泛型方法滿足介面從設計角度來看是不可接受的。
這些選擇都不好,所以我們選擇了「以上皆非」。
代替具有類型參數的方法,使用具有類型參數的全域函數,或將類型參數加入到接收器類型。
有關更多詳細資訊,包括更多範例,請參見[提案](https://go.dev/design/43651-type-parameters#no-parameterized-methods
為什麼我不能為參數化類型的接收器使用更具體的類型?
泛型類型的方法宣告使用包含類型參數名稱的接收器。
也許是因為在呼叫點指定類型的語法相似性,
有些人認為這提供了一種機制,透過命名接收器中的特定類型(如string)來為某些類型參數產生客製化的方法:
這失敗了,因為單詞string被編譯器視為方法中類型參數的名稱。
編譯器錯誤訊息將是類似「operator + not defined on s.f (variable of type string)」。
這可能會令人困惑,因為+運算子在預宣告類型string上工作得很好,
但宣告已經為這個方法覆蓋了string的定義,
而運算子在那個不相關的string版本上不工作。
像這樣覆蓋預宣告名稱是有效的,但這是很奇怪的做法,通常是一個錯誤。
為什麼編譯器不能推論我程式中的類型參數?
有很多情況下,程式設計師很容易看到泛型類型或函數的類型參數應該是什麼,但語言不允許編譯器推論它。 類型推論被有意限制,以確保永遠不會對推論的類型產生任何混淆。 其他語言的經驗表明,意外的類型推論在閱讀和除錯程式時會導致相當大的混淆。 總是可以指定呼叫中要使用的明確類型參數。 將來可能會支援新的推論形式,只要規則保持簡單和清晰。
套件和測試
我如何建立多檔案套件?
將套件的所有原始碼檔案放在一個目錄中。 原始碼檔案可以自由參考不同檔案中的項目;不需要前向宣告或標頭檔案。
除了被分成多個檔案外,該套件將像單檔案套件一樣編譯和測試。
我如何撰寫單元測試?
在與套件原始碼檔案相同的目錄中建立一個以_test.go結尾的新檔案。
在該檔案中,import "testing"並撰寫形式如下的函數
在該目錄中執行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程式所需的關鍵功能,如格式化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開始會使引入一些ABI和相關更改(如Go需要但不屬於標準C設定的堆疊管理)變得更加困難。
Go最終被證明是一種實作Go編譯器的優秀語言,儘管這不是它的最初目標。 從一開始就不自託管使Go的設計可以專注於其原始用例,即網路伺服器。 如果我們決定Go應該盡早編譯自己,我們可能會最終得到一個更針對編譯器建構的語言,這是一個有價值的目標,但不是我們最初的。
儘管gc有自己的實作,但原生的詞法分析器和解析器在go/parser套件中可用,還有一個原生的類型檢查器。gc編譯器使用這些套件的變體。
執行時期支援是如何實作的?
再次由於自舉問題,執行時期程式碼最初主要用C撰寫(帶有一小部分組譯器),但後來已轉換為Go(除了一些組譯器部分)。
Gccgo的執行時期支援使用glibc。
gccgo編譯器使用一種稱為分段堆疊的技術實作goroutines,
這由對gold連結器的最新修改支援。
Gollvm類似地建構在相應的LLVM基礎設施上。
為什麼我的簡單程式產生的二進制檔案這麼大?
gc工具鏈中的連結器預設建立靜態連結的二進制檔案。
因此,所有Go二進制檔案都包含Go執行時期,以及支援動態類型檢查、反射甚至恐慌時堆疊追蹤所需的執行時期類型資訊。
一個簡單的C "hello, world"程式在Linux上使用gcc靜態編譯和連結後大約750 kB,包括printf的實作。
一個等效的Go程式使用fmt.Printf重達幾兆位元組,但這包含了更強大的執行時期支援和類型及除錯資訊。
使用gc編譯的Go程式可以使用-ldflags=-w標誌連結以停用DWARF產生,
從二進制檔案中移除除錯資訊,但不會損失其他功能。
這可以顯著減小二進制檔案的大小。
我可以停止關於未使用變數/匯入的抱怨嗎?
未使用變數的存在可能表明有錯誤,而未使用的匯入只會減慢編譯速度, 這種影響會隨著程式隨著時間累積程式碼和程式設計師而變得顯著。 出於這些原因,Go拒絕編譯具有未使用變數或匯入的程式, 以短期便利換取長期建構速度和程式清晰度。
儘管如此,在開發程式碼時,通常會暫時建立這些情況, 在程式編譯之前必須編輯掉它們可能會很煩人。
有些人要求編譯器選項來關閉這些檢查,或至少將它們減少為警告。 然而,這樣的選項尚未加入, 因為編譯器選項不應影響語言的語意,而且Go編譯器不報告警告,只報告阻止編譯的錯誤。
沒有警告有兩個原因。首先,如果值得抱怨,就值得在程式碼中修復。(相反,如果不值得修復,就不值得提及。)其次,讓編譯器產生警告會鼓勵實作對弱情況發出警告,這會使編譯變得嘈雜,掩蓋了應該修復的真正錯誤。
不過,很容易解決這種情況。使用空白識別符來讓未使用的內容在你開發時持續存在。
如今,大多數Go程式設計師使用一個工具,
goimports,
它會自動重寫Go原始檔案以擁有正確的匯入,
實際上消除了未使用匯入的問題。
這個程式很容易連接到大多數編輯器和IDE,以在Go原始檔案被寫入時自動執行。
此功能也內建在gopls中,如上面討論的。
為什麼我的病毒掃描軟體認為我的Go發行版或編譯的二進制檔案被感染了?
這在Windows機器上很常見,幾乎總是誤報。 商業病毒掃描程式經常被Go二進制檔案的結構搞糊塗, 它們不像其他語言編譯的二進制檔案那樣常見。
如果你剛安裝Go發行版,系統報告它被感染了,那肯定是個錯誤。 為了徹底,你可以透過將校驗和與下載頁面上的校驗和進行比較來驗證下載。
在任何情況下,如果你認為報告是錯誤的,請向你的病毒掃描器供應商報告一個錯誤。 也許隨著時間推移,病毒掃描器可以學會理解Go程式。
效能
為什麼Go在基準測試X上表現不佳?
Go的設計目標之一是接近C在可比程式上的效能,但在一些基準測試上表現相當差,包括 golang.org/x/exp/shootout中的幾個。 最慢的依賴於Go中沒有可比效能版本的函式庫。 例如,pidigits.go 依賴於多精度數學套件,C版本使用GMP(用最佳化的組譯器撰寫),而Go的版本不使用。 依賴於正規表示式的基準測試 (例如regex-dna.go) 本質上是在將Go的原生regexp套件與成熟的、高度最佳化的正規表示式函式庫如PCRE進行比較。
基準測試遊戲透過廣泛的最佳化獲勝,大多數基準測試的Go版本需要關注。 如果你測量真正可比的C和Go程式 (reverse-complement.go 是一個例子),你會發現這兩種語言在原始效能上比這個套件表明的更接近。
儘管如此,仍有改進的空間。編譯器很好但可以更好,許多函式庫需要主要的效能工作,垃圾回收器還不夠快。(即使它是,注意不要產生不必要的垃圾也會產生巨大影響。)
在任何情況下,Go通常可以非常有競爭力。 隨著語言和工具的發展,許多程式的效能有了顯著改善。 請參閱關於分析Go程式的部落格文章,了解一個資訊豐富的例子。 它很老了,但仍然包含有用的資訊。
與C的差異
為什麼語法與C如此不同?
除了宣告語法外,差異並不大,源於兩個願望。 首先,語法應該感覺輕鬆,沒有太多強制性關鍵字、重複或奧秘。 其次,語言被設計為易於分析,可以在沒有符號表的情況下解析。 這使得建構除錯器、依賴分析器、自動文件提取器、IDE外掛等工具變得更加容易。 C及其後代在這方面是出了名的困難。
為什麼宣告是反向的?
如果你習慣了C,它們只是反向的。在C中,概念是變數應該像表示其類型的表達式一樣宣告,這是一個好主意,但類型和表達式語法混合得不太好,結果可能令人困惑;考慮函數指標。
Go主要分離了表達式和類型語法,這簡化了事情(使用前置*表示指標是一個例外,證明了規則)。
在C中,宣告
宣告a為指標但不宣告b;在Go中
宣告兩者都是指標。這更清晰、更規則。
此外,:=短宣告形式認為完整的變數宣告應該與:=呈現相同的順序,因此
與
有相同的效果。
透過擁有一個與表達式語法不同的類型語法,解析也得到了簡化;關鍵字如func和chan使事情保持清晰。
有關更多詳細資訊,請參閱關於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分析工具的演示。