Go在谷歌:以软件工程为目的的语言设计
From: http://www.oschina.net/translate/go-at-google-language-design-in-the-service-of-software-engineering
?
1. 摘要(本文是根據(jù)Rob Pike于2012年10月25日在Tucson, Arizona舉行的SPLASH 2012大會上所做的主題演講進行修改后所撰寫的。) 針對我們在Google公司內(nèi)開發(fā)軟件基礎(chǔ)設(shè)施時遇到的一些問題,我們于2007年末構(gòu)思出Go編程語言。當今的計算領(lǐng)域同創(chuàng)建如今所使用的編程語言(使用最多的有C++、Java和Python)時的環(huán)境幾乎沒什么關(guān)系了。由多核處理器、系統(tǒng)的網(wǎng)絡(luò)化、大規(guī)模計算機集群和Web編程模型帶來的編程問題都是以迂回的方式而不是迎頭而上的方式解決的。此外,程序的規(guī)模也已發(fā)生了變化:現(xiàn)在的服務(wù)器程序由成百上千甚至成千上萬的程序員共同編寫,源代碼也以數(shù)百萬行計,而且實際上還需要每天都進行更新。更加雪上加霜的是,即使在大型編譯集群之上進行一次build,所花的時間也已長達數(shù)十分鐘甚至數(shù)小時。 之所以設(shè)計開發(fā)Go,就是為了提高這種環(huán)境下的工作效率。Go語言設(shè)計時考慮的因素,除了大家較為了解的內(nèi)置并發(fā)和內(nèi)存垃圾自動回收這些方面之外,還包括嚴格的依賴管理、對隨系統(tǒng)增大而在體系結(jié)構(gòu)方面發(fā)生變化的適應(yīng)性、跨組件邊界的健壯性(robustness)。 本文將詳細講解在構(gòu)造一門輕量級并讓人感覺愉悅的、高效的編譯型編程語言時,這些問題是如何得到解決的。講解過程中使用的例子都是來自Google公司中所遇到的現(xiàn)實問題。 | |
| ? |
2. 簡介Go語言開發(fā)自Google,是一門支持并發(fā)編程和內(nèi)存垃圾回收的編譯型靜態(tài)類型語言。它是一個開源的項目:Google從公共的代碼庫中導(dǎo)入代碼而不是相反。 Go語言運行效率高,具有較強的可伸縮性(scalable),而且使用它進行工作時的效率也很高。有些程序員發(fā)現(xiàn)用它編程很有意思;還有一些程序員認為它缺乏想象力甚至很煩人。在本文中我們將解釋為什么這兩種觀點并不相互矛盾。Go是為解決Google在軟件開發(fā)中遇到的問題而設(shè)計的,雖然因此而設(shè)計出的語言不會是一門在研究領(lǐng)域里具有突破性進展的語言,但它卻是大型軟件項目中軟件工程方面的一個非常棒的工具。 | |
| ? |
3. Google公司中的Go語言為了幫助解決Google自己的問題,Google設(shè)計了Go這門編程語言,可以說,Google有很大的問題。 硬件的規(guī)模很大而且軟件的規(guī)模也很大。軟件的代碼行數(shù)以百萬計,服務(wù)器軟件絕大多數(shù)用的是C++,還有很多用的是Java,剩下的一部分還用到了Python。成千上萬的工程師在這些代碼上工作,這些代碼位于由所有軟件組成的一棵樹上的“頭部”,所以每天這棵樹的各個層次都會發(fā)生大量的修改動作。盡管使用了一個大型自主設(shè)計的分布式Build系統(tǒng)才讓這種規(guī)模的開發(fā)變得可行,但這個規(guī)模還是太大 了。 當然,所有這些軟件都是運行在無數(shù)臺機器之上的,但這些無數(shù)臺的機器只是被看做數(shù)量并不多若干互相獨立而僅通過網(wǎng)絡(luò)互相連接的計算機集群。 簡言之,Google公司的開發(fā)規(guī)模很大,速度可能會比較慢,看上去往往也比較笨拙。但很有效果。 Go項目的目標是要消除Google公司軟件開發(fā)中的慢速和笨拙,從而讓開發(fā)過程更加高效并且更加具有可伸縮性。該語言的設(shè)計者和使用者都是要為大型軟件系統(tǒng)編寫、閱讀和調(diào)試以及維護代碼的人。 因此,Go語言的目的不是要在編程語言設(shè)計方面進行科研;它要能為它的設(shè)計者以及設(shè)計者的同事們改善工作環(huán)境。Go語言考慮更多的是軟件工程而不是編程語言方面的科研。或者,換句話說,它是為軟件工程服務(wù)而進行的語言設(shè)計。 但是,編程語言怎么會對軟件工程有所幫助呢?下文就是該問題的答案。 |
4. 痛之所在當Go剛推出來時,有人認為它缺乏某些大家公認的現(xiàn)代編程語言中所特有的特性或方法論。缺了這些東西,Go語言怎么可能會有存在的價值?我們回答這個問題的答案在于,Go的確具有一些特性,而這些特性可以解決困擾大規(guī)模軟件開發(fā)的一些問題。這些問題包括:
一門語言每個單個的特性都解決不了這些問題。這需要從軟件工程的大局觀,而在Go語言的設(shè)計中我們試圖致力于解決所有這些問題。 舉個簡單而獨立的例子,我們來看看程序結(jié)果的表示方式。有些評論者反對Go中使用象C一樣用花括號表示塊結(jié)構(gòu),他們更喜歡Python或Haskell風格式,使用空格表示縮進。可是,我們無數(shù)次地碰到過以下這種由語言交叉Build造成的Build和測試失敗:通過類似SWIG調(diào)用的方式,將一段Python代碼嵌入到另外一種語言中,由于修改了這段代碼周圍的一些代碼的縮進格式,從而導(dǎo)致Python代碼也出乎意料地出問題了并且還非常難以覺察。?因此,我們的觀點是,雖然空格縮進對于小規(guī)模的程序來說非常適用,但對大點的程序可不盡然,而且程序規(guī)模越大、代碼庫中的代碼語言種類越多,空格縮進造成的問題就會越多。為了安全可靠,舍棄這點便利還是更好一點,因此Go采用了花括號表示的語句塊。 |
5.C和C++中的依賴在處理包依賴(package dependency)時會出現(xiàn)一些伸縮性以及其它方面的問題,這些問題可以更加實質(zhì)性的說明上個小結(jié)中提出的問題。讓我們先來回顧一下C和C++是如何處理包依賴的。 ANSI C第一次進行標準化是在1989年,它提倡要在標準的頭文件中使用#ifndef這樣的"防護措施"。 這個觀點現(xiàn)已廣泛采用,就是要求每個頭文件都要用一個條件編譯語句(clause)括起來,這樣就可以將該頭文件包含多次而不會導(dǎo)致編譯錯誤。比如,Unix中的頭文件<sys/stat.h>看上去大致是這樣的: ?
此舉的目的是讓C的預(yù)處理器在第二次以及以后讀到該文件時要完全忽略該頭文件。符號_SYS_STAT_H_在文件第一次讀到時進行定義,可以“防止”后繼的調(diào)用。 這么設(shè)計有一些好處,最重要的是可以讓每個頭文件能夠安全地include它所有的依賴,即時其它的頭文件也有同樣的include語句也不會出問題。?如果遵循此規(guī)則,就可以通過對所有的#include語句按字母順序進行排序,讓代碼看上去更整潔。 但是,這種設(shè)計的可伸縮性非常差。 |
| 在1984年,有人發(fā)現(xiàn)在編譯Unix中ps命令的源程序ps.c時,在整個的預(yù)處理過程中,它包含了<sys/stat.h>這個頭文件37次之多。盡管在這么多次的包含中有36次它的文件的內(nèi)容都不會被包含進來,但絕大多數(shù)C編譯器實現(xiàn)都會把"打開文件并讀取文件內(nèi)容然后進行字符串掃描"這串動作做37遍。這么做可真不聰明,實際上,C語言的預(yù)處理器要處理的宏具有如此復(fù)雜的語義,其勢必導(dǎo)致這種行為。 對軟件產(chǎn)生的效果就是在C程序中不斷的堆積#include語句。多加一些#include語句并不會導(dǎo)致程序出問題,而且想判斷出其中哪些是再也不需要了的也很困難。刪除一條#include語句然后再進行編譯也不太足以判斷出來,因為還可能有另外一條#include所包含的文件中本身還包含了你剛剛刪除的那條#include語句。 從技術(shù)角度講,事情并不一定非得弄成這樣。在意識到使用#ifndef這種防護措施所帶來的長期問題之后,Plan 9的library的設(shè)計者采取了一種不同的、非ANSI標準的方法。Plan 9禁止在頭文件中使用#include語句,并要求將所有的#include語句放到頂層的C文件中。 當然,這么做需要一些訓(xùn)練 —— 程序員需要一次列出所有需要的依賴,還要以正確的順序排列 —— 但是文檔可以幫忙而且實踐中效果也非常好。這么做的結(jié)果是,一個C源程序文件無論需要多少依賴,在對它進行編譯時,每個#include文件只會被讀一次。當然,這樣一來,對于任何#include語句都可以通過先拿掉然后在進行編譯的方式判斷出這條#include語句到底有無include的必要:當且僅當不需要該依賴時,拿掉#include后的源程序才能仍然可以通過編譯。 |
| Plan 9的這種方式產(chǎn)生的一個最重要的結(jié)果是編譯速度比以前快了很多:采用這種方式后編譯過程中所需的I/O量,同采用#ifndef的庫相比,顯著地減少了不少。 但在Plan 9之外,那種“防護”式的方式依然是C和C++編程實踐中大家廣為接受的方式。實際上,C++還惡化了該問題,因為它把這種防護措施使用到了更細的粒度之上。按照慣例,C++程序通常采用每個類或者一小組相關(guān)的類擁有一個頭文件這種結(jié)構(gòu),這種分組方式要更小,比方說,同<stdio.h>相比要小。因而其依賴樹更加錯綜復(fù)雜,它反映的不是對庫的依賴而是對完整類型層次結(jié)構(gòu)的依賴。而且,C++的頭文件通常包含真正的代碼 —— 類型、方法以及模板聲明 ——不像一般的C語言頭文件里面僅僅有一些簡單的常量定義和函數(shù)簽名。這樣,C++就把更多的工作推給了編譯器,這些東西編譯起來要更難一些,而且每次編譯時編譯器都必須重復(fù)處理這些信息。當要build一個比較大型的C++二進制程序時,編譯器可能需要成千上萬次地處理頭文件<string>以了解字符串的表示方式。(根據(jù)當時的記錄,大約在1984年,Tom Cargill說道,在C++中使用C預(yù)處理器來處理依賴管理將是個長期的不利因素,這個問題應(yīng)該得到解決。) |
| 在Google,Build一個單個的C++二進制文件就能夠數(shù)萬次地打開并讀取數(shù)百個頭文件中的每個頭文件。在2007年,Google的build工程師們編譯了一次Google里一個比較主要的C++二進制程序。該文件包含了兩千個文件,如果只是將這些文件串接到一起,總大型為4.2M。將#include完全擴展完成后,就有8G的內(nèi)容丟給編譯器編譯,也就是說,C++源代碼中的每個自己都膨脹成到了2000字節(jié)。 還有一個數(shù)據(jù)是,在2003年Google的Build系統(tǒng)轉(zhuǎn)變了做法,在每個目錄中安排了一個Makefile,這樣可以讓依賴更加清晰明了并且也能好的進行管理。一般的二進制文件大小都減小了40%,就因為記錄了更準確的依賴關(guān)系。即使如此,C++(或者說C引起的這個問題)的特性使得自動對依賴關(guān)系進行驗證無法得以實現(xiàn),直到今天我們?nèi)匀晃野l(fā)準確掌握Google中大型的C++二進制程序的依賴要求的具體情況。 |
| 由于這種失控的依賴關(guān)系以及程序的規(guī)模非常之大,所以在單個的計算機上build出Google的服務(wù)器二進制程序就變得不太實際了,因此我們創(chuàng)建了一個大型分布式編譯系統(tǒng)。該系統(tǒng)非常復(fù)雜(這個Build系統(tǒng)本身也是個大型程序)還使用了大量機器以及大量緩存,藉此在Google進行Build才算行得通了,盡管還是有些困難。 即時采用了分布式Build系統(tǒng),在Google進行一次大規(guī)模的build仍需要花幾十分鐘的時間才能完成。前文提到的2007年那個二進制程序使用上一版本的分布式build系統(tǒng)花了45分鐘進行build。現(xiàn)在所花的時間是27分鐘,但是,這個程序的長度以及它的依賴關(guān)系在此期間當然也增加了。為了按比例增大build系統(tǒng)而在工程方面所付出的勞動剛剛比軟件創(chuàng)建的增長速度提前了一小步。 |
6. 走進 Go 語言當編譯緩慢進行時,我們有充足的時間來思考。關(guān)于 Go 的起源有一個傳說,話說正是一次長達45分鐘的編譯過程中,Go 的設(shè)想出現(xiàn)了。人們深信,為類似谷歌網(wǎng)絡(luò)服務(wù)這樣的大型程序編寫一門新的語言是很有意義的,軟件工程師們認為這將極大的改善谷歌程序員的生活質(zhì)量。 盡管現(xiàn)在的討論更專注于依賴關(guān)系,這里依然還有很多其他需要關(guān)注的問題。這一門成功語言的主要因素是:
說完了背景,現(xiàn)在讓我們從軟件工程的角度談一談 Go 語言的設(shè)計。 |
總結(jié)
以上是生活随笔為你收集整理的Go在谷歌:以软件工程为目的的语言设计的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: ORACLE TDE 透明数据加密技术
- 下一篇: mybatis动态sql中的where标