Go語言之父:開源14年,Go不止是編程語言,究竟做對了哪些?
譯文編譯 | 言征
作者 | Rob Pike
提及編程語言,2023 年,除了老牌的 C++ 和新晉之秀 Rust 熱度最高之外,就要數 Go 了。 從 2009 年由 C 語言獲取靈感而發布,到如今風靡已久的高性能語言,Go 已經走過了 14 個年頭。
“Go是一個項目,不只是一門語言。我們最初的目標不是創建一種新的編程語言,而是創建一種更好的軟件編寫方式。”
2023 年 11 月 10 日是 Go 作為開源項目推出 14 周年。Go語言之父 Rob Pike 在在悉尼 GopherConAU 會議上進行了一場耐人尋味的演講。Go走到今天,做對了什么?做錯了什么?這里總結在此,以饗諸君。
一、我們做對了什么,做錯了什么
今天是 2023 年 11 月 10 日,Go 作為開源項目推出 14 周年。2009年的這天,加州時間下午 3 點(如果沒記錯的話),Ken Thompson、Robert Griesemer、Russ Cox、Ian Taylor、Adam Langley、Jini Kim 和我滿懷期待地看著網站上線,全世界都知道了我們在做什么。
圖片
Go是一個項目,不只是一門語言。
即使是最成功的項目 ,經過反思,也有一些可以做得更好的地方。當然 ,事后看來,這些事情似乎是他們成功的關鍵。
這些因素正是我們想要解決的:構建現代服務器軟件的復雜性:控制依賴性、與人員不斷變化的大型團隊一起編程、易于維護、高效測試、多核 CPU 和網絡的有效使用等等。
根據大家的反饋看,認為 Go 做錯的事情都在語言層面, 而其實Go做對的事情,都在更大的故事中,即圍繞語言的周邊工具和生態, 比如 gofmt 以及部署和測試。這說明Go成員們做對了。
二、Go哪里做對了?
Go為什么會取得現在的成功?總結了有以下幾點。每一個都至關重要。
1.規范說明
我們從正式的規范開始。這不僅可以在編寫編譯器時鎖定行為,還可以使 多個實現共存并就該行為達成一致。編譯器本身并不是一個規范。您測試編譯器的依據是什么?
2.多種實現方式
有多個編譯器,它們都實現相同的規范。有了規范就可以更容易地實現這一點。
有一天,伊恩·泰勒(Ian Taylor)發郵件通知我們,在閱讀了我們的規范草案后,他自己編寫了一個編譯器,這讓我們感到驚訝。
圖片
我的一位同事向我指出了 http://.../go_lang.html 。 它看起來是一種有趣的語言,我拼湊了一個 gcc前端。 當然,它缺少很多功能,但它確實在網頁上編譯了素數篩代碼。
這是令人興奮的,但更多的事情也隨之而來,所有這些都因正式規范的存在而成為可能。
擁有多個編譯器幫助我們改進了語言并完善了規范,并為那些不太喜歡我們類似 Plan-9 的業務方式的其他人提供了替代環境。
如今有很多兼容的實現,這很棒。
3.便攜性
我們使交叉編譯變得微不足道,這使得程序員可以在他們喜歡的任何平臺上工作,并交付到任何需要的平臺。 人們很容易將編譯器視為是與運行的本機是原生的。Go可以打破這個假設,對許多開發者來說都是新聞。
4.兼容性
我們努力使語言成型1.0 版本,然后通過兼容性保證將其鎖定。Go的受歡迎程度已經很高,在一個幾乎沒有其他東西是穩定的世界中,不用擔心新的 Go 版本會破壞你的項目,是最讓人心安的。當然,很多其他項目不會這么做,因為維護強大的兼容性是需要成本的。
圖片
5.庫
庫的發展有點偶然。一開始可以安裝 Go 代碼的地方非常貧瘠,但這樣一個可靠、制作精良的庫的存在,其中包含編寫 21 世紀服務器代碼所需的大部分內容,是一項重要資產。它讓社區所有人都使用同一個工具包,直到我們有足夠的經驗來了解還應該提供什么。這非常有效,有助于防止變體庫的出現,有助于統一社區。
6.工具
我們確保該語言易于解析,從而支持工具構建。起初我們認為我們需要一個適用于 Go 的 IDE,但擁有簡單的工具反而會意味著,IDE 遲早將出現在 Go 上。這些工具和 gopls 一起就做到了,而且他們非常棒。
我們還為編譯器提供了一套輔助工具,例如 自動化測試、覆蓋率和代碼審查。當然還有 go 命令,它集成了整個構建過程,并且是許多項目構建和維護 Go 代碼所需的全部內容。
此外,Go 獲得了快速構建的聲譽。
7.gofmt
我將 gofmt 作為一個單獨的項目從工具中拿出來,因為 它不僅給Go,而且給整個編程社區上都帶來了新的活力。在 Robert 編寫 gofmt 之前,自動格式化程序的質量不高,因此大多未使用。
Gofmt 表明它可以做得很好,今天幾乎每種值得使用的語言都有一個標準格式化程序。不爭論空格和換行符所節省的時間值得花在定義標準格式和編寫這段相當困難的代碼以實現自動化上的所有時間。
此外,gofmt 還使無數其他工具成為可能,例如簡化器、分析器甚至代碼覆蓋率工具。因為 gofmt 的內容成為了任何人都可以使用的庫,所以您可以解析程序、編輯 AST,然后打印字節完美的輸出,供人類和機器使用。
三、Go 哪里做錯了?
1.并發成就了Go,但Go有責任做好指導
并發性成就了 Go,它讓Go看起來像是一種新事物。Go 對并發的支持是一個主要的吸引因素,提高了Go的早期采用率。同時,Go 的誕生在一定程度上讓并發編程成為了一種主流思想,它讓編程世界相信并發是一個強大的工具(特別是在多核網絡世界中)方面發揮了重要作用,并且它可以比 pthread 做得更好。現在大多數主流語言都對并發有很好的支持。
更有意思的是,Go 版本的并發,非常新穎。沒有協程,沒有任務,沒有線程,沒有名稱,只有 goroutine。我們發明了“goroutine”這個詞,因為沒有合適的現有術語。直到今天我還是希望 Unix 的拼寫命令能夠學會它。
問題來了,這就是我們犯了兩個重大錯誤的地方。
首先,并發很有趣,但我們想到的用例主要是服務器的東西,也就意味著在 net/http 等關鍵庫中完成,而不是在每個程序中隨處可見。因此,當許多程序員使用它時,他們很難弄清楚它如何真正幫助到他們。
我們應該預先解釋聲明一下,該語言中的并發支持真正帶來的是更簡單的服務器軟件。這個問題就在于許多人都認為重要,但并不是對所有嘗試過 Go 的人來說都很重要。“缺乏響應的指導是我們的責任。”
第二點就是,我們花了太長時間來澄清并行性(支持多核機器上并行的多個計算)和并發性(這是一種構造代碼以實現這一點的方法)之間的區別。
無數程序員試圖通過使用 goroutine 并行化來提高代碼速度,但常常對由此產生的速度減慢感到困惑。如果底層問題本質上是并行的,比如處理 HTTP 請求,那么并行代碼只會在并行化時運行得更快。我們在解釋這一點上做得很糟糕,結果讓許多程序員感到困惑,并可能趕走了一些程序員。
為了解決這個問題,2012 年,我在 Heroku 的 Waza 開發者大會上發表了演講 ,名為“并發不是并行”。這是一個有趣的演講,但應該早點發生。
對此表示歉意。但好處仍然存在:Go 幫助普及了并發性作為構建服務器軟件的一種方式。
2.接口
我認為接口是 Go 中設計得最好的東西之一。而且很明顯,接口和并發性是 Go 中的一個獨特的想法。它們是 Go 對面向對象設計的回答,采用原始的、以行為為中心的風格,盡管新來者不斷推動結構體承載這種負載。
使接口動態化,無需提前宣布哪些類型實現它們,這困擾了一些早期的批評者,并且仍然激怒了一些人,但這對于 Go 所培育的編程風格很重要。標準庫的大部分內容都是 建立在它們的基礎上的,而更廣泛的主題(例如測試 和管理依賴項)在很大程度上依賴于它們慷慨的“ 歡迎所有人”的性質。
這里有一個故事要講。在羅伯特和我辦公室的那個著名的第一天,我們問了一個問題:如何處理多態性。Ken 和我從 C 語言中知道 qsort 可以作為一個困難的測試用例,因此我們三個開始討論我們的“胚胎語言”如何實現類型安全的排序例程。
羅伯特和我幾乎同時提出了相同的想法:使用類型上的方法來提供排序所需的操作。這個概念很快就發展成為這樣的想法:值類型具有行為,定義為方法,并且方法集可以提供函數可以操作的接口。Go 的接口幾乎立刻就出現了。
這是經常被忽視的事情:Go 的排序是作為在接口上運行的函數實現的。這不是 大多數人熟悉的面向對象編程風格 ,但它是一個非常強大的想法。
當 Russ 加入時,他很快就指出了 I/O 如何完美地融入這個想法,并且庫的發展很快,很大程度上基于三個著名的接口:empty、Writer 和 Reader,平均持有三分之二的數據、各一個方法。 這些微小的方法是 Go 的慣用方法,而且無處不在。
接口的工作方式不僅成為 Go 的顯著特征,還成為我們思考庫、通用性和組合的方式。這是令人興奮的事情。但問題就產生在這里。你看,我們走這條路,很大一部分原因是我們經常看到泛型編程鼓勵一種這樣一種思維方式:傾向于關注類型而不是算法、關注早期的抽象而不是有機設計、關注容器而不是函數。
我們用語言本身定義了通用容器——映射、切片、數組、通道——但沒有讓程序員訪問它們所包含的通用性。這可以說是一個錯誤。雖然這些類型可以很好地處理大多數簡單的編程任務。但讓人感到困擾的是,語言提供的內容和用戶可以控制的內容之間存在障礙。
簡而言之,這種思維方式陳年已久,我們花了十多年的時間才糾正過來。Ian Taylor 從一開始就敦促我們面對這個問題,但考慮到接口作為 Go 編程的基石,這很難做到。
批評者經常抱怨我們應該只做泛型,因為它們 很“簡單”,也許它們可以用在某些語言中,但接口的存在意味著任何新形式的多態性都必須考慮它們。找到一種與該語言的其余部分良好配合的前進方式需要多次嘗試、幾次中止的實現以及數小時、數天和數周的討論。
最終我們請來了一些由 Phil Wadler 領導的類型理論家來幫忙。即使在今天,語言中已經有了可靠的通用模型,但接口作為方法集的存在仍然存在一些揮之不去的問題。
如您所知,最終的答案是設計一個接口的泛化,可以吸收更多形式的多態性,從“方法集”過渡到“類型集”。這是一個微妙但意義深遠的舉措,大多數社區似乎都同意這一舉措,但我的懷疑態度并未打消。
有時要花很多年的時間才能搞清楚一件事,甚至搞清楚“自己原來沒太搞清楚”這件事。然后繼續前進。
順便說一句,我希望我們有一個比“泛型”更好的術語,它起源于一種不同的、以數據結構為中心的多態性風格。“參數多態性”是 Go 提供的內容的正確術語,但它有些拗口。但我們平時提“泛型”,但也不太準確。
3.對于開源項目的管理挑戰與教訓
要想成功,Go 必須是一個開源項目。但在我們弄清楚關鍵想法并進行有效實施之前,私下開發會更有成效。因此,前兩年我們需要這個過程不受干擾。
向開源的過渡是一個巨大的變化,而且具有教育意義。社區的投入是巨大的。與社區互動需要花費大量的時間和精力,尤其是對于 Ian 來說,他竟然還能抽出時間來回答任何人提出的每個問題。但它帶來了更多。我仍然對 Windows 移植的速度如此之快感到驚訝,這完全是由社區在 Alex Brainman 的指導下完成的。
我們花了很長時間才理解轉向開源項目的含義以及如何管理它。我們說服了支持我們的社區,堅持通過強制代碼審查和對細節的全面關注來保持高代碼質量,而不是采用快速接受代碼并在提交后進行清理的方式。這種做法將更多的工作推回給社區,需要他們了解其價值,否則他們不會感到受到應有的歡迎。
圖片
有一個歷史細節并不廣為人知。該項目有 4 個不同的內容管理系統:SVN、Perforce、Mercurial 和 Git。感謝Russ為項目所做的歷史內容管理系統的維護工作。
此外,Go語言項目是獨立于Goole之外的,盡管Google提供了支持,但核心團隊是獨立的,沒有設定議程。Google 擁有龐大的內部 Go 代碼庫,團隊用它來測試和驗證版本,但這是通過從公共存儲庫導入 Google 來完成的。
4.包管理
Go語言的包管理開發過程做得并不好,關鍵在于使用純字符串來指定導入語句中的路徑,雖然提供了靈活性,但也帶來了很多問題。
Go核心團隊在早期缺乏處理具有大量包版本的包管理器以及解決依賴關系圖等復雜問題的經驗。盡管如此,我們仍然試圖讓社區參與解決依賴管理問題,但最終設計出來時,許多社區成員感到被輕視。
這次失敗給團隊上了一課,讓我們更加了解如何真正與社區互動。盡管道路崎嶇不平,但現在事情已經得到了解決,出現的設計在技術上非常出色,并且對大多數用戶來說效果很好。
四、寫在最后:Go 做到了
推出 14 年后,我們最后成功了。總的來說,很大程度上是因為最初把設計和開發 Go 作為一種編寫軟件的方式(而不僅僅是作為一種編程語言)這項決定,才帶領我們來到了這樣一個新地方。
我們到達這里的部分原因是:
- 一個強大的標準庫,可實現服務器代碼所需的大部分基礎知識
- 并發性作為該語言的首要組成部分
- 基于組合而不是繼承的方法
- 澄清依賴管理的打包模型
- 集成的快速構建和測試工具
- 嚴格一致的格式
- 注重可讀性而非聰明性
- 兼容性保證
最重要的是,得益于令人難以置信的、樂于助人、且多元化的 Gophers 社區的支持。
參考鏈接:https://commandcenter.blogspot.com/2024/01/what-we-got-right-what-we-got-wrong.html