我愛Go。從我開始使用這種語言的第一天起,我就迅速愛上了它。它提供了令人難以置信的簡單性,同時保持了出色的類型安全和快如閃電的編譯。它的執(zhí)行速度非???,并發(fā)性是一流的(這是一種輕描淡寫的說法),標準庫有大量的高級接口,可以用很少的依賴性來啟動任何應(yīng)用程序,它可以直接編譯成可執(zhí)行文件,我可以繼續(xù)說下去。盡管與其他C語言相比,Go的語法文字主義需要一些時間來適應(yīng),但在使用了一段時間后,感覺非常直觀。我的背景是Java,但對C、C++、JavaScript、TypeScript和Python都有豐富的經(jīng)驗。Go是我學習的第一種語言,我希望在任何地方都能用它來做任何事情。雖然我討厭 “產(chǎn)品殺手 “這種陳詞濫調(diào)的概念,但作為一個曾經(jīng)是專業(yè)的Java開發(fā)者的人,Go感覺就像是一個Java殺手。我認為Java會消失嗎?可能不會。我認為Go會在流行程度上超過Java嗎?不太可能。然而,對我個人來說,我無法想象在任何情況下(除了維護大到無法重寫的傳統(tǒng)產(chǎn)品),我寧愿使用Java而不是Go。
說到這里,你可能會想,”這篇文章不是應(yīng)該講你不喜歡Go的地方嗎?” 這是一個完全公平的問題,我正要回答這個問題,但重要的是要了解我有多喜歡Go,才能理解我為什么要抱怨它。那么就不多說了,到底有什么可批評的呢?
庫函數(shù)會修改其參數(shù)我對Go的第一個抱怨是我馬上就注意到的。許多內(nèi)置的庫函數(shù)修改它們的參數(shù)而不是返回新的結(jié)果。修改函數(shù)參數(shù)模糊了輸入和輸出之間的界限,最終破壞了代碼的表現(xiàn)力。一個函數(shù)的表現(xiàn)力是指它通過其簽名清楚地傳達意義和意圖的能力;意義傳達得越清楚,這個函數(shù)的表現(xiàn)力就越強。表達性是可維護代碼的最重要方面。顯然,有些時候性能比表現(xiàn)力更有利于程序,但從可維護性的角度來看,表現(xiàn)力應(yīng)該始終是優(yōu)先考慮的。那為什么Go會這樣做呢?通過修改參數(shù)而不是返回新數(shù)據(jù),Go編譯器可以更好地跟蹤特定變量的生命周期。Go是一種垃圾收集(GC)語言,所以任何時候函數(shù)返回一個指針或由指針支持的類型(例如一個片斷),都會增加內(nèi)存需要在堆而不是棧上分配的可能性。堆分配需要垃圾收集,而垃圾收集會占用你程序中寶貴的CPU周期。
避免堆分配–從而減少垃圾收集–絕對可以提高一個應(yīng)用程序的性能。這些優(yōu)化可能有利于渲染高FPS圖形的軟件,但對于大多數(shù)企業(yè)應(yīng)用和日常服務(wù)來說,很可能對終端用戶的好處幾乎是無法察覺的。一個合理的論點是,一種語言應(yīng)該盡量減少其自身庫的開銷,但是當速度和易用性是相互競爭的目標時,需要優(yōu)先考慮一個目標。已經(jīng)有很多語言為高性能的使用場景提供了顯式內(nèi)存管理(C、C++、Rust等),那么Go真的應(yīng)該為了提供更少的GC周期而犧牲其最大的優(yōu)勢之一(易用性)嗎?
作為一個開發(fā)者,我希望至少能有一些更有表現(xiàn)力、更直觀的替代品,在功能上與優(yōu)化的API相當。盡管有這樣的煩惱,Go遠不是唯一犯了這樣錯誤的語言,事實上,許多違規(guī)的Go函數(shù)都有幾乎相同的Java和C++對應(yīng)物。然而,僅僅因為有其他語言的先例,并不意味著Go應(yīng)該無可指責,最終,這是我不喜歡這種語言的地方。為了挽回一些分數(shù),可以說要求用指針作為參數(shù)是Go語言請求修改的一種表達方式。對于這一點,我將承認幾點,盡管我仍然沒有找到一個令人信服的方法來用slice這么做(而slice往往是這個模式用的最多的)。
泛型Go 1.18引入了泛型,所以我有點被寵壞了,因為我只需要等待幾個月就可以得到這個功能1,而許多資深的Go開發(fā)者已經(jīng)等待了好幾年。Go的泛型實現(xiàn)感覺有點像TypeScript的,在大多數(shù)情況下,這是件好事。與TS一樣,開發(fā)者可以很容易地約束泛型,使其符合幾種可能的已知類型或接口。但與TS不同的是,Go不必處理JavaScript的包袱,特別是圍繞著未定義和null。說白了,我喜歡泛型作為一種語言特性。如果使用得當,它們可以提高代碼的可重用性,從而提高一致性并降低錯誤的風險。當我說我不喜歡Go中的泛型時,我的意思有兩點:第一,Go對泛型的實現(xiàn)還有很多需要改進的地方;第二,長期以來語言中缺乏泛型,導致許多丑陋的反模式埋藏在許多庫的表面,包括Go的標準庫。解讀第一點,Go目前不支持方法或結(jié)構(gòu)域的泛型。對方法的不支持有點令人費解,因為在引擎蓋下,Go將方法視為以接收者為第一參數(shù)的函數(shù)。如果函數(shù)支持泛型,為什么方法不支持呢?Go對嵌入的支持降低了泛型結(jié)構(gòu)字段的關(guān)鍵性,因為很多泛型的用途都可以用嵌入來模仿:只要把 “非泛型 “字段放在一個單獨的結(jié)構(gòu)中,然后把同一個結(jié)構(gòu)嵌入幾次就好了。然而,嵌入并不是一個完美的替代品,因為操作和方法需要針對外部結(jié)構(gòu)的每一種變化進行重新實現(xiàn)。Go可以通過允許泛型結(jié)構(gòu)字段來將這一責任轉(zhuǎn)移給編譯器,而不是開發(fā)人員,但現(xiàn)在我們還只能復制和粘貼。由于Go中目前缺少泛型的地方,許多數(shù)據(jù)結(jié)構(gòu)的實現(xiàn)不得不使用反射、類型檢查和鑄造等黑客的變通方法,以提供對不同類型的廣泛支持。這讓我想到了第二個抱怨。Go作出了類型安全的承諾,然后立即在其標準庫中通過使用偽通用的變通方法:interface{}來破壞它。Go的空接口用法不僅是一種反模式的縮影,而且類型檢查和反射往往是較慢的操作(諷刺的是,這與我之前抱怨的表達能力對速度的權(quán)衡是不一致的)。最糟糕的是,第三方庫也大量采用了空接口的反模式,所以即使Go最終將其所有庫遷移到泛型,這種模式也可能在許多代碼庫中存在相當長一段時間。
make()函數(shù)make()函數(shù)是Go的 “原始類型 “初始化解決方案。大多數(shù)基元都有一個合理的零值,但在Go中,map、slices和channel都是受益于動態(tài)初始化的基元類型。使用map和slices的零值是完全可能的,有時甚至是合理的(例如JSON操作和避免nil返回),但對于大多數(shù)情況,make()是最好的選擇。我對make()有異議的地方是,它存在我已經(jīng)說過的兩個問題。首先,make()沒有表現(xiàn)力。它的完整簽名是func make(t Type, size …IntegerSize) Type,這讓我對如何正確使用它知之甚少。盡管它在技術(shù)上只是一個函數(shù),但在Go編譯器對它的特殊處理以及它對創(chuàng)建通道的必要性之間,make()就像for-loop一樣是Go的一個重要組成部分。采用這種思路可以部分地原諒它的簽名,但是提供NewMap()、NewSlice()和NewChan()函數(shù)也同樣容易,甚至更容易,這樣就不會產(chǎn)生歧義了。我不打算深入討論這些替代方案,因為我相信對于這些選擇為什么會有問題,有很多強烈的意見。但我要深入探討的是,make()的錯誤是多么容易發(fā)生(看到我做了什么了嗎)。m := make(map[int]int, 10) 創(chuàng)建一個空的地圖,分配足夠的空間來存儲10個條目;len(m) 返回0??吹絾栴}所在了嗎?無論是在寫代碼時,還是在審查代碼時,都很容易不小心忽略這個重要的區(qū)別。要獲得你所期望的切片行為,需要一個額外的參數(shù):s := make([]int, 0, 10)。在這種情況下,len(s)實際上會返回0。因此,Go并沒有為這些數(shù)據(jù)結(jié)構(gòu)提供更具表現(xiàn)力的、不同的初始化器,而是提供了一個具有更大模糊性的單一函數(shù),因此具有更大的誤用風險。在我對make()的看法上,我對它的第二個問題是它的偽通用性。Go通常不允許函數(shù)重載,但make()得到了一個特殊的通行證來假裝重載。由于這個特殊的傳遞,make()的第一個參數(shù)可以是幾種類型中的一種。它的返回類型也是如此。對于一個十年來一直聲稱不需要泛型的語言來說,Go不得不打破很多自己的規(guī)則,讓它最核心的一個函數(shù)在沒有泛型的情況下工作。對我來說,這讓我感覺很草率。
扁平化的包結(jié)構(gòu)我來自Java的世界。Java應(yīng)用程序往往有很多很多的包。在這個世界上,父包對于一個類的上下文來說往往和類的名字本身一樣重要,所以對于Go這個 “Java殺手 “來說,擁有這樣一個扁平的包結(jié)構(gòu)是有點刺耳的。這并不是Go的獨特之處。許多面向腳本的語言,如Python,傾向于采用更多的廣度而不是深度。盡管這是一種相對普遍的做法,但我想讓Go成為Java的 “直接替代品 “的夢想似乎已經(jīng)破滅了。扁平化的包結(jié)構(gòu)本身并沒有什么問題。一層層的空目錄(或包含單個文件的目錄)在沒有明確設(shè)計的語言中很少提供價值–誠然,這適用于大多數(shù)不是面向?qū)ο蟮恼Z言。然而,如果扁平語言聲稱要解決與嵌套語言相同的問題,那么扁平語言應(yīng)該提供語義上相等的機制來管理標識符的可見性和范圍。
Go通過大寫字母導出標識符的簡單方法非常好。在我的團隊的風格公約會議上,我又少了一件要爭論的事情。玩笑歸玩笑,盡管我很喜歡Go開發(fā)者的這一選擇,但包的語義和扁平結(jié)構(gòu)的慣例削弱了這一功能對應(yīng)用程序代碼的潛在價值。在庫代碼中,導出或不導出的簡單概念對于定義公共API是完美的。對于大型應(yīng)用,尤其是網(wǎng)絡(luò)服務(wù)器,這通常是不夠的。網(wǎng)絡(luò)服務(wù)器必然會有一些從來沒有被其他代碼明確消費的包(除了測試),而是被外部客戶和其他服務(wù)器通過HTTP等協(xié)議調(diào)用。這些包中的代碼將和其他代碼一樣從抽象中受益,但在扁平化的包結(jié)構(gòu)中,未導出的抽象將不可避免地被包中無權(quán)使用的其他區(qū)域看到。這導致了一個難題:我們應(yīng)該違反扁平化包結(jié)構(gòu)的慣例,犧牲可讀性和重用性來換取更少的抽象性,還是干脆讓未導出的標識符在它們不應(yīng)該出現(xiàn)的地方被訪問?這個問題的存在就是承認Go有問題。當然,扁平結(jié)構(gòu)是慣例而不是法律,但慣例在Go的發(fā)展過程中具有巨大的影響力,它決定了許多新的功能,并將其納入語言。所以是的,這可能不是Go的一個明確特征,但它仍然是我不喜歡的東西,因為Go社區(qū)把它作為一個最佳實踐而大力推動。
缺少lambda函數(shù)這個問題絕對是吹毛求疵的,所以我就直奔主題了。Go并沒有λ函數(shù)的簡寫方式。我知道有人提議使用λ函數(shù),也有人爭論為什么不需要λ函數(shù),但盡管有這些考慮,事實是我喜歡速記λ,而Go沒有。Go的函數(shù)語法恰好是短而精的。此外,函數(shù)在Go中是類型,可以分配給變量,這對我這個喜歡濫用Java 8中引入的 “方法引用 “的人來說很熟悉。即使如此,當我用Go寫作時,有時內(nèi)聯(lián)一個函數(shù)是解決一個問題的最合適的方法,然而即使是單行的,所產(chǎn)生的代碼也往往是笨拙的,特別是當需要返回語句時。我不認為有人能說服我說func(x, y int) int { return x+y }比(x, y) => (x+y)更漂亮或更易讀。隨你怎么爭論強類型或明確性,我還是會懷念速記的lambdas。
總結(jié)對于那些還沒有嘗試過Go,但正在考慮使用它的人來說,不要讓這些勸阻你;它是一個神奇的工具,幾乎肯定會改善你的開發(fā)生活。對于現(xiàn)有的Gophers,我希望你能同情我的抱怨,但仍然像我一樣喜歡這門語言。
Java程序員不喜歡Golang的地方 – Gavin