Go 在 Google:服務(wù)于軟件工程的語言設(shè)計(jì)(翻譯)(一)
本文譯者:Jayce Chant,來源:?存檔SaveAndLoad,原文鏈接:https://mp.weixin.qq.com/s/f0XkH-rFkgDkQfhsMsJ-uQ
最近在寫 Go 語言實(shí)戰(zhàn)系列的文章,中間想聊一下 Go 的設(shè)計(jì)原則,發(fā)現(xiàn)自己理解得還是不夠深入,寫得辭不達(dá)意。然后找到了 Rob Pike 在 8 年前的演講稿,拜讀學(xué)習(xí)之后,想推薦給我的讀者作為學(xué)習(xí)資料。
結(jié)果在中文互聯(lián)網(wǎng)只找到了 OSCHINA 上 13 年的眾包翻譯,再也沒找到其他翻譯版本。這個(gè)版本,不同譯者,以及同一譯者的不同段落,翻譯水平差異極大:個(gè)別地方翻譯得非常傳神,更多時(shí)候是忠實(shí)的術(shù)語翻譯,偶有生硬直譯和機(jī)翻的感覺,同時(shí)也能找到一些明顯的理解錯(cuò)誤和低級(jí)的筆誤。
總的來說,如果對(duì) Go、C 家族語言 以及 并發(fā)、垃圾回收等 涉及的主題有一定了解,翻譯的瑕疵不影響理解。譯文翻譯于 13 年,很早,應(yīng)該對(duì)中文世界早期 Go 的推廣起了一定的作用,向這些譯者致謝。但是如果是剛接觸編程或者 Go 語言的初學(xué)者,個(gè)別錯(cuò)誤可能會(huì)讓人看得云里霧里。
所以我不自量力地嘗試自己翻譯一遍。首先是試圖提供一個(gè)質(zhì)量稍微高一點(diǎn)點(diǎn)的版本(不一定能成功),其次也是希望通過這樣再深入學(xué)習(xí)一遍。
為了符合中文的閱讀習(xí)慣,在(盡量)不影響原意的前提下,一些句子(特別是長(zhǎng)從句)的語序作了調(diào)整,個(gè)別不符合中文表達(dá)習(xí)慣的表述做了刪減或者補(bǔ)充。文中的加粗也是我個(gè)人劃的重點(diǎn)。水平所限,譯文在(計(jì)算機(jī))專業(yè)上和英語理解上不可避免地會(huì)有理解偏差乃至錯(cuò)誤,存疑的地方請(qǐng)結(jié)合原文理解。翻譯過程有借助 辭典 和 DeepL 翻譯器作為參考,個(gè)別表述有借鑒 OSCHINA 版譯文。
原文:Go at Google: Language Design in the Service of Software Engineering
地址:https://talks.golang.org/2012/splash.article
作者:Rob Pike
翻譯:Jayce Chant(博客:jaycechant.info,公眾號(hào)ID:jayceio)
Rob Pike:Unix 小組成員,參與了 Plan 9 計(jì)劃,1992 年和 Ken Thompson 共同開發(fā)了 UTF-8。他和 Ken Thompson 也是 Go 語言最早期的設(shè)計(jì)者。

譯文較長(zhǎng),分三篇推送,這里是第 1~7 小節(jié)。
1. 摘要
這是 Rob Pike 2012 年 10 月 25 日在 亞利桑那州 圖森市 舉行的 SPLASH 2012 會(huì)議上發(fā)表的主題演講稿的修訂版。
我們?cè)?Google 開發(fā)軟件基礎(chǔ)設(shè)施時(shí)遇到一些問題,針對(duì)這些問題,Go 語言在 2007 年末被構(gòu)思出來。今天的計(jì)算環(huán)境與正在使用的語言(主要是 C++、Java 和 Python)創(chuàng)建時(shí)的環(huán)境幾乎毫無關(guān)系。多核處理器、網(wǎng)絡(luò)系統(tǒng)、大規(guī)模計(jì)算集群和網(wǎng)絡(luò)編程模型所帶來的問題,人們只是用變通辦法暫時(shí)繞開(being worked around),而不是正面解決(addressed head-on)。另外,軟件的規(guī)模也發(fā)生了變化:今天的服務(wù)器程序由數(shù)千萬行代碼組成,需要成百上千的程序員共同協(xié)作,并且每天都在更新。更糟糕的是,即使是在大型編譯集群上,構(gòu)建(build)時(shí)間也會(huì)延長(zhǎng)到幾分鐘,甚至幾小時(shí)。
設(shè)計(jì)和開發(fā) Go 就是為了在這種環(huán)境下提高工作效率。 Go 設(shè)計(jì)的考慮因素,除了眾所周知的像 內(nèi)置并發(fā) 和 垃圾回收,還包括 嚴(yán)格的依賴管理、軟件架構(gòu)在系統(tǒng)增長(zhǎng)時(shí)的適應(yīng)性,以及跨組件的健壯性。
本文將解釋在構(gòu)建一個(gè)高效的、編譯型的、輕量級(jí)的、使人愉悅的編程語言的過程中,如何解決這些問題。例子 和 解釋 都來自 Google 實(shí)際遇到的問題。
2. 簡(jiǎn)介
Go 是 Google 開發(fā)的一種編譯型、支持并發(fā)、帶垃圾回收、靜態(tài)類型的語言。它是一個(gè)開源項(xiàng)目:Google 從公共代碼庫(kù)導(dǎo)入代碼,而不是反過來。
Go 運(yùn)行效率高、可伸縮性強(qiáng),而且工作效率也高。有些程序員覺得用它干活很有趣;有些則覺得它缺乏想象力,甚至很無聊。在本文中,我們將解釋為什么這些觀點(diǎn)并不矛盾。Go 是為解決 Google 在軟件開發(fā)中面臨的問題而設(shè)計(jì)的,這導(dǎo)致 Go 并不是一門在研究領(lǐng)域有突破性的語言;盡管如此它仍是大型軟件項(xiàng)目工程化的優(yōu)秀工具。
譯者注:這是 8 年前的演講。Go 初期確實(shí)是為了解決 Google 內(nèi)部的問題而誕生的。但如今已經(jīng)是誕生的第 11 個(gè)年頭,Go 早已被寄予更多的期待。它要解決的問題沒變,只是不再局限于 Google 的內(nèi)部場(chǎng)景。
3. Go 在 Google
Google 設(shè)計(jì) Go 用來幫助解決 Google 自己的問題,而 Google 的問題很 大。
硬件大,軟件也大。軟件有好幾百萬行,服務(wù)器大部分用 C++,剩余的部分大量使用 Java 和 Python。成千上萬的工程師在代碼上工作,這些代碼位于一個(gè)包含了所有軟件的單棵大樹的『頭部』,所以樹的各個(gè)層次一天到晚都有重要變更。使用大型的、定制的分布式構(gòu)建系統(tǒng)使這種規(guī)模的開發(fā)變得可行,但它仍然很大。
當(dāng)然,所有這些軟件都運(yùn)行在無數(shù)(zillions)臺(tái)機(jī)器上,這些機(jī)器被看作數(shù)量不多的獨(dú)立的、互相聯(lián)網(wǎng)的計(jì)算集群。
簡(jiǎn)而言之,Google 的開發(fā)規(guī)模很大,速度可能很慢,而且經(jīng)常顯得很笨拙。但它是有效的。
Go 項(xiàng)目的目標(biāo),是消除 Google 軟件開發(fā)中的緩慢和笨拙,從而使開發(fā)過程更加高效和獲得更強(qiáng)的可伸縮性。這個(gè)語言是由編寫、閱讀、調(diào)試和維護(hù)大型軟件系統(tǒng)的人設(shè)計(jì)的,也是為這些人設(shè)計(jì)的。
因此,Go 的目的不是要做編程語言設(shè)計(jì)的研究,而是要改善語言設(shè)計(jì)者及其同事的工作環(huán)境。Go 考慮的更多是軟件工程的問題,而不是編程語言方面的科研。換句話說,它圍繞的是『服務(wù)于軟件工程的語言設(shè)計(jì)』。
但是,一門語言如何對(duì)軟件工程有所助益呢?本文剩下的內(nèi)容就是對(duì)這個(gè)問題的回答。
4. 痛點(diǎn)
在 Go 剛推出時(shí),有人聲稱,它缺少現(xiàn)代語言所必需的某些特性或方法論。缺少這些的 Go 能有什么價(jià)值呢?我們的回答是,Go 所具備的某些特性,可以解決嚴(yán)重困擾大規(guī)模軟件開發(fā)的一些問題。這些問題包括
構(gòu)建速度慢
失控的依賴關(guān)系
每個(gè)程序員使用相同語言的不同子集
程序難以理解(代碼難以閱讀,文檔不完善等)
重復(fù)勞動(dòng)
更新代價(jià)大
版本偏斜(version skew)
難以編寫自動(dòng)化工具
跨語言構(gòu)建
一門語言的單個(gè)特性并不能解決這些問題。這需要有軟件工程的大局觀(larger view),所以在 Go 的設(shè)計(jì)中,我們?cè)噲D把重點(diǎn)放在解決這些問題上。
作為一個(gè)簡(jiǎn)單而且獨(dú)立的例子,我們來看一下程序結(jié)構(gòu)的表示方式。一些觀察者反對(duì) Go 用花括號(hào)({...})來表示類似于 C 的塊狀結(jié)構(gòu),他們更喜歡用 Python 或 Haskell 風(fēng)格的空格來縮進(jìn)。然而,我們見過太多由跨語言構(gòu)建引起的構(gòu)建和測(cè)試失敗:嵌入到另一種語言里的 Python 代碼段(例如通過 SWIG 調(diào)用),會(huì)因?yàn)橹車a縮進(jìn)的變化而被意外地破壞,而且非常難以察覺。因此,我們的觀點(diǎn)是,雖然空格縮進(jìn)對(duì)于小程序來說是不錯(cuò)的選擇,但它并不具有大程序所需要的可伸縮性;而且代碼庫(kù)越大,異構(gòu)性越強(qiáng),就會(huì)帶來越多的麻煩。為了安全和可靠,最好還是放棄這點(diǎn)便利,所以 Go 使用花括號(hào)表示的代碼塊。
5. C 和 C++ 中的依賴關(guān)系
更能實(shí)質(zhì)性地說明上面提到的可伸縮性和其他問題的,是包依賴關(guān)系的處理。我們從回顧 C 和 C++ 如何處理依賴關(guān)系開始討論。
最早于 1989 年標(biāo)準(zhǔn)化的 ANSI C 在標(biāo)準(zhǔn)頭文件里推廣了 #ifndef 『防護(hù)(guards)』的概念。這個(gè)做法現(xiàn)在已經(jīng)是無處不在,就是每個(gè)頭文件都要用一個(gè)條件編譯語句(clause)包裹起來,這樣做就算這個(gè)頭文件被多次包含(include)也不會(huì)出錯(cuò)。例如,Unix 頭文件 的結(jié)構(gòu)是這樣的:
/*?大段的版權(quán)和許可證聲明?*/
#ifndef?_SYS_STAT_H_
#define?_SYS_STAT_H_
/*?類型和其他定義?*/
#endif
這樣做的目的,是讓 C 語言預(yù)處理器在第二次以及后續(xù)讀到該文件時(shí),忽略被包裹的內(nèi)容。符號(hào)_SYS_STAT_H_ 在第一次讀取文件時(shí)被定義,避免(guards)了后續(xù)的調(diào)用。
這樣設(shè)計(jì)有一些好處,最重要的是每個(gè)頭文件可以安全地 #include 它所有的依賴,即使其他頭文件也包含這些依賴,都不會(huì)有問題。如果遵循這個(gè)規(guī)則,并且按字母順序排列 #include 語句,可以寫出有條理的代碼。
但它的可伸縮性非常差。
1984 年,有人發(fā)現(xiàn)編譯 ps.c(Unix ps 命令的源碼)時(shí),整個(gè)預(yù)處理過程會(huì)遇到 37 次 #include 。盡管后面 36 次頭文件的內(nèi)容都會(huì)被忽略,但大多數(shù) C 語言的實(shí)現(xiàn)每次都會(huì)打開文件、讀取文件、完整掃描內(nèi)容,一連串動(dòng)作下來,一共 37 次。 這樣做非常不聰明,但是 C 預(yù)處理器需要處理非常復(fù)雜的宏語義,使它只能這樣實(shí)現(xiàn)。
這對(duì)軟件造成的影響是, C 程序里 #include 語句會(huì)不斷累積。添加 #include 語句不會(huì)破壞程序,卻很難知道什么時(shí)候不再需要它們。刪除一條 #include 后再編譯一次也檢查不出來,因?yàn)榭赡芰硪粭l #include 本身就包含你剛剛刪除的那條 ?#include。
從技術(shù)的角度講,沒必要弄成這樣子。意識(shí)到使用 #ifndef 防護(hù)的長(zhǎng)期問題,Plan 9 庫(kù)的設(shè)計(jì)者們采取了一種不同的、非 ANSI 標(biāo)準(zhǔn)的做法。在 Plan 9 里,頭文件禁止包含更多的 #include 語句;所有的 #include 都要放在頂層 C 文件里。當(dāng)然,這需要一些紀(jì)律:程序員需要按照正確的順序、準(zhǔn)確地列出必要的依賴關(guān)系;但文檔可以幫上忙,而且在實(shí)踐中效果非常好。這樣做的結(jié)果是,無論一個(gè) C 源文件有多少依賴,在編譯該文件時(shí),每個(gè) #include 文件都只會(huì)被讀取一次。而且,只要把 #include 語句先刪掉就能很容易地看出來它是否必要:當(dāng)且僅當(dāng)刪除的依賴不是必要的依賴時(shí),編輯后的程序才能通過編譯。
Plan 9 做法最重要的結(jié)果是編譯速度更快:編譯所需的 I/O 量比使用帶有 #ifndef 防護(hù)的庫(kù)時(shí)大大減少。
但在 Plan 9 之外,『防護(hù)』法仍是 C 和 C++ 的公認(rèn)做法。事實(shí)上,C++ 在更細(xì)的粒度上使用同樣的做法還加劇了這個(gè)問題 。按照慣例,C++ 程序的結(jié)構(gòu)通常是每個(gè)類有一個(gè)頭文件,也可能是一小組的相關(guān)類有一個(gè)頭文件,這種分組方式比像 這樣的頭文件要小得多。因此,它的依賴樹要復(fù)雜得多,反映的不是庫(kù)之間的依賴關(guān)系,而是完整的類型層次結(jié)構(gòu)。此外,C++ 頭文件通常包含真正的代碼——類型、方法和模板聲明——而不僅僅是一般 C 頭文件里常見的簡(jiǎn)單常量和函數(shù)簽名。因此,C++ 不僅向編譯器推送了更多的信息,而且推送的內(nèi)容更難編譯,編譯器的每次調(diào)用都必須重新處理這些信息。在構(gòu)建一個(gè)大型的 C++ 二進(jìn)制文件時(shí),編譯器可能要成千上萬次地處理頭文件 去學(xué)會(huì)如何表示一個(gè)字符串。(據(jù)記錄,1984 年左右,Tom Cargill 就提到,使用 C 預(yù)處理器進(jìn)行依賴管理將是 C++ 的長(zhǎng)期負(fù)擔(dān),應(yīng)該加以解決。)
在 Google,構(gòu)建一個(gè) C++ 二進(jìn)制文件,打開和讀取不同的頭文件可以達(dá)到數(shù)百個(gè),次數(shù)可以達(dá)到數(shù)萬次。2007 年,Google 的構(gòu)建工程師對(duì) Google 的一個(gè)主要二進(jìn)制文件的編譯進(jìn)行了檢測(cè)。這個(gè)二進(jìn)制文件包含了大約兩千個(gè)源文件,如果簡(jiǎn)單地連在一起,總共有 4.2 MB。在所有 ?#include 語句被展開后,超過 8 GB 內(nèi)容被送到編譯器的輸入端,也就是源碼里的每個(gè)字節(jié)膨脹了 2000 倍。
另一個(gè)數(shù)據(jù)是,2003 年,Google 的構(gòu)建系統(tǒng)從單一的 Makefile 轉(zhuǎn)變?yōu)槊總€(gè)目錄都有 Makefile 的設(shè)計(jì),有了更好的管理,更明確的依賴關(guān)系。僅僅是因?yàn)橛辛烁_的依賴關(guān)系記錄,一個(gè)典型的二進(jìn)制文件在文件大小上就縮減了 40%。即便如此,C++ (或 C 語言)的特性使得自動(dòng)驗(yàn)證這些依賴關(guān)系難以實(shí)現(xiàn),直到今天,我們對(duì) Google 的大型 C++ 二進(jìn)制文件的依賴關(guān)系需求仍然沒有一個(gè)準(zhǔn)確的把握。
依賴關(guān)系失控和規(guī)模太大的后果是,在單臺(tái)計(jì)算機(jī)上構(gòu)建 Google 服務(wù)器的二進(jìn)制文件變得不切實(shí)際,一個(gè)大型的分布式編譯系統(tǒng)應(yīng)運(yùn)而生。有了這個(gè)加了很多機(jī)器、很多緩存、很多復(fù)雜的東西的系統(tǒng)(構(gòu)建系統(tǒng)本身就是一個(gè)大程序),Google 的構(gòu)建總算可以進(jìn)行,雖然還是很麻煩。
即使采用分布式構(gòu)建系統(tǒng),Google 的一次大型構(gòu)建仍然需要很長(zhǎng)時(shí)間。前面提到 2007 年的那個(gè)二進(jìn)制程序使用上一版的分布式構(gòu)建系統(tǒng)花了 45 分鐘;同一程序今天的版本花了 27 分鐘,當(dāng)然這期間程序和它的依賴關(guān)系也還在增長(zhǎng)。擴(kuò)大構(gòu)建系統(tǒng)的工程投入,只能勉強(qiáng)比它所構(gòu)建的軟件的增長(zhǎng)速度領(lǐng)先一點(diǎn)。
6. 走進(jìn) Go
當(dāng)構(gòu)建速度很慢時(shí),就有了時(shí)間去思考。Go 有那么一個(gè)起源傳說(origin myth),聲稱 Go 正是在其中一次 45 分鐘的構(gòu)建過程中被構(gòu)思出來的。設(shè)計(jì)一門新的語言,使它適合編寫像 Web 服務(wù)器這樣的大型 Google 程序,同時(shí)考慮到軟件工程的因素,可以提高 Google 程序員的生活質(zhì)量。人們相信這個(gè)目標(biāo)值得一試。
雖然到目前為止的討論都集中在依賴關(guān)系上,但還有許多其他問題需要注意。一門語言要想在上述背景下取得成功,主要的考慮因素是:
它必須適應(yīng)大規(guī)模開發(fā)。能在有大量依賴關(guān)系、大量程序員團(tuán)隊(duì)一起協(xié)作的大型程序項(xiàng)目上很好地工作。 它必須是大家熟悉的,大致上類似于 C 語言的。在 Google 工作的程序員處于職業(yè)生涯的早期,對(duì)過程式編程語言(procedural languages),尤其是來自 C 家族的語言最熟悉。要想讓程序員在新語言中快速提高工作效率,意味著語言不能太激進(jìn)。 它必須是現(xiàn)代的。C、C++ 以及 Java 的某些方面都相當(dāng)老舊,是在多核機(jī)器、網(wǎng)絡(luò) 和 web 應(yīng)用開發(fā) 出現(xiàn)之前設(shè)計(jì)的。新的做法可以更好地適應(yīng)現(xiàn)代世界的一些特點(diǎn),比如內(nèi)置的并發(fā)支持。
那么,在這樣的背景下,讓我們從軟件工程的角度來看看 Go 的設(shè)計(jì)。
7. Go 的依賴關(guān)系
既然我們已經(jīng)詳細(xì)了解過 C 和 C++ 中的依賴關(guān)系,那么我們可以從 Go 如何處理依賴關(guān)系開始。依賴關(guān)系是由語言在語法和語義上定義的。它們是明確的、清晰的和『可計(jì)算』的,也就是說,很容易寫工具來分析。
Go 的語法是,在 package 語句(下一節(jié)的主題)之后,每個(gè)源文件可以有一個(gè)或多個(gè)導(dǎo)入語句,每個(gè)導(dǎo)入語句由 import 關(guān)鍵字和一個(gè)字符串常量組成,標(biāo)識(shí)要導(dǎo)入到當(dāng)前源文件(且只限當(dāng)前源文件)的包:
import?"encoding/json"
讓 Go 可以做到規(guī)模化、依賴智能化的第一步,是語言將 未使用的依賴 (unused dependencies)定義為編譯期錯(cuò)誤(注意不是警告,是錯(cuò)誤)。如果源文件導(dǎo)入了一個(gè)它不用的包,程序就不會(huì)通過編譯。這保證了任何 Go 程序構(gòu)建中的依賴關(guān)系樹都是精確的,沒有多余的邊。另一邊又保證了在構(gòu)建程序時(shí)不會(huì)有多余的代碼被編譯,從而最大限度地減少了編譯時(shí)間。
第二步是在編譯器的實(shí)現(xiàn)上,更進(jìn)一步保證效率。假設(shè)一個(gè)有三個(gè)包的 Go 程序,依賴關(guān)系如下:
A包導(dǎo)入了B包;B包導(dǎo)入了C包;A包沒有導(dǎo)入C包。
這意味著 A 包只是在引用 B 包的過程中,間接地引用了 C 包;換句話說,盡管 A 引用的來自 B 的某些代碼引用了 C,但在 A 的源碼里沒有直接涉及來自 C 的標(biāo)識(shí)符。例如, A 包可能會(huì)引用一個(gè)在 B 里面定義的結(jié)構(gòu)體類型,該結(jié)構(gòu)體有一個(gè)字段的類型是在 C 里定義的,但 A 本身并不直接引用 C 里面的類型。一個(gè)更具體的例子是,A 導(dǎo)入了一個(gè)格式化 I/O 包 B,B 使用了 C 提供的緩沖 I/O 實(shí)現(xiàn),但 A 本身并沒有調(diào)用緩沖 I/O。
要構(gòu)建這個(gè)程序,首先 C 被編譯;被依賴的包必須在依賴它們的包之前構(gòu)建。然后,B 被編譯;最后 A 被編譯,然后就可以鏈接程序。
在 A 被編譯時(shí),編譯器讀取的是 B 的目標(biāo)文件而不是源代碼。B 的目標(biāo)文件包含了編譯器在 A 的源代碼里執(zhí)行 import "B" 語句所需的所有類型信息。這些信息包括 B 的調(diào)用方(clients)在編譯時(shí)需要的任何關(guān)于 C 的信息。換句話說,當(dāng) B 被編譯時(shí),生成的目標(biāo)文件包含了 B 所有公共接口所需的依賴關(guān)系的類型信息。
這種設(shè)計(jì)的一個(gè)重要的效果,就是 當(dāng)編譯器執(zhí)行一條 import 語句時(shí),只會(huì)打開一個(gè)文件 ,那就是導(dǎo)入語句里的字符串所標(biāo)識(shí)的目標(biāo)文件。這讓人不由得想起 Plan 9 C(相對(duì)于 ANSI C)的依賴管理方法,但實(shí)際上編譯器在編譯 Go 源文件的時(shí)候就會(huì)寫入頭文件。考慮到導(dǎo)入時(shí)讀取的數(shù)據(jù)只是『導(dǎo)出的(exported)』數(shù)據(jù),而不是一般的程序源代碼,這個(gè)過程比 Plan 9 C 更自動(dòng),甚至更高效。這對(duì)整體編譯時(shí)間可以造成巨大的影響,還能隨著代碼庫(kù)的增長(zhǎng)彈性地伸縮。與 C 和 C++ 的 『include 文件里還有 include』的模式相比,生成依賴圖(dependency graph)并編譯的時(shí)間可以指數(shù)級(jí)地減少。
值得一提的是,這種通用的依賴管理方法并不是獨(dú)創(chuàng)的,其思想可以追溯到 20 世紀(jì) 70 年代,流傳于 Modula-2 和 Ada 等語言中。在 C 語言家族中,Java 也有這種方法的元素。
為了使編譯更有效率,目標(biāo)文件的內(nèi)容是經(jīng)過編排的,導(dǎo)出數(shù)據(jù)就在文件的開頭,所以編譯器只要讀到導(dǎo)出數(shù)據(jù)的結(jié)尾就可以結(jié)束,不需要讀取整個(gè)文件。
這種依賴管理方法是 Go 編譯比 C 或 C++ 快的一個(gè)最大原因。另一個(gè)因素是 Go 把導(dǎo)出數(shù)據(jù)放在目標(biāo)文件里,作為對(duì)比有些語言需要作者手寫或編譯器生成包含這些信息的另外的文件。這就需要打開兩倍數(shù)量的文件。在 Go 里,導(dǎo)入一個(gè)包只需要打開一個(gè)文件。另外,單文件的方式意味著導(dǎo)出數(shù)據(jù)(類似 C / C++ 里的頭文件)相對(duì)于目標(biāo)文件來說,永遠(yuǎn)不會(huì)過時(shí)。
為了做一個(gè)對(duì)比,我們測(cè)量了一個(gè)用 Go 編寫的大型 Google 程序的編譯情況,看看源代碼的扇出量與前面做的 C++ 分析相比如何。(譯者注:這里指第五節(jié)提到的 C++ 頭文件展開后的內(nèi)容量和源代碼的比值,為 2000 倍。)我們發(fā)現(xiàn)大約是 40 倍,比 C++ 好了 50 倍(同時(shí)也更簡(jiǎn)單,因此處理速度更快),但還是比我們預(yù)期的大。這有兩個(gè)原因。首先,我們發(fā)現(xiàn)了一個(gè) bug:Go 編譯器在導(dǎo)出部分生成了大量不需要的數(shù)據(jù)。其次,導(dǎo)出數(shù)據(jù)使用的是一種冗長(zhǎng)的編碼,還有改進(jìn)的余地。我們已經(jīng)計(jì)劃解決這些問題。(譯者注:Go 在 2012 年 3 月才發(fā)布了 1.0 版本,到現(xiàn)在已經(jīng)過去了 8 年多,到了 1.15 。這中間 Go 團(tuán)隊(duì)投入了大量時(shí)間在 編譯器、運(yùn)行時(shí) 和 工具鏈的優(yōu)化上,這兩個(gè)問題應(yīng)該已經(jīng)得到了很大的改善,甚至可能已經(jīng)徹底解決。)
盡管如此,減少到五十分之一,就足以把幾分鐘變成幾秒鐘,把茶歇時(shí)間變成交互式構(gòu)建。
Go 依賴圖的另一個(gè)特點(diǎn)是它沒有依賴環(huán)。語言定義了依賴中不能有循環(huán)導(dǎo)入,編譯器和鏈接器都會(huì)檢查確保不存在循環(huán)依賴。雖然循環(huán)導(dǎo)入偶爾有用,但它在規(guī)模上會(huì)帶來嚴(yán)重的問題。循環(huán)導(dǎo)入要求編譯器一次性處理更多的源文件,這就減緩了增量構(gòu)建的速度。更重要的是,根據(jù)我們的經(jīng)驗(yàn),如果允許這樣的導(dǎo)入,最終會(huì)把大片的源碼樹,糾纏成難以獨(dú)立管理的幾大塊,使二進(jìn)制文件膨脹,并使初始化、測(cè)試、重構(gòu)、發(fā)布和其他軟件開發(fā)任務(wù)變得復(fù)雜。
缺少循環(huán)導(dǎo)入偶爾會(huì)造成煩惱,但卻能保持依賴樹的干凈,迫使包之間有明確的邊界。就像 Go 里的許多設(shè)計(jì)決策一樣,它迫使程序員更早地考慮一個(gè)更大范圍的問題(在這里,這個(gè)問題是包的邊界),這些問題如果留到以后,可能永遠(yuǎn)不會(huì)得到令人滿意的解決。
Go 設(shè)計(jì)標(biāo)準(zhǔn)庫(kù)的過程中,花費(fèi)了大量精力在控制依賴關(guān)系上。如果只是需要一個(gè)函數(shù),拷貝一點(diǎn)代碼可能比直接拉來一個(gè)大庫(kù)強(qiáng)。(如果出現(xiàn)新的核心依賴關(guān)系,系統(tǒng)構(gòu)建中的測(cè)試就會(huì)報(bào)告問題。)依賴關(guān)系清晰勝過代碼重用。實(shí)踐中的一個(gè)例子是,(底層的)net 包有自己的 整型 到 小數(shù) 的轉(zhuǎn)換程序,以避免依賴更大的、依賴關(guān)系更復(fù)雜的格式化 I/O 包。另一個(gè)例子是字符串轉(zhuǎn)換包 strconv 有一個(gè)私有的 『可打印』字符定義的實(shí)現(xiàn),而不是引入大塊頭的 Unicode 字符類表;strconv 通過包的測(cè)試來確保符合 Unicode 標(biāo)準(zhǔn)。
推薦閱讀

