Go在谷歌:以軟件工程為目的的語言設(shè)計
1. 摘要
(本文是根據(jù)Rob Pike于2012年10月25日在Tucson, Arizona舉行的SPLASH 2012大會上所做的主題演講進行修改后所撰寫的。)
針對我們在Google公司內(nèi)開發(fā)軟件基礎(chǔ)設(shè)施時遇到的一些問題,我們于2007年末構(gòu)思出Go編程語言。當(dāng)今的計算領(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)存垃圾自動回收這些方面之外,還包括嚴(yán)格的依賴管理、對隨系統(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)用它編程很有意思;還有一些程序員認(rè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ī)模還是太大 了。
當(dāng)然,所有這些軟件都是運行在無數(shù)臺機器之上的,但這些無數(shù)臺的機器只是被看做數(shù)量并不多若干互相獨立而僅通過網(wǎng)絡(luò)互相連接的計算機集群。
簡言之,Google公司的開發(fā)規(guī)模很大,速度可能會比較慢,看上去往往也比較笨拙。但很有效果。
Go項目的目標(biāo)是要消除Google公司軟件開發(fā)中的慢速和笨拙,從而讓開發(fā)過程更加高效并且更加具有可伸縮性。該語言的設(shè)計者和使用者都是要為大型軟件系統(tǒng)編寫、閱讀和調(diào)試以及維護代碼的人。
因此,Go語言的目的不是要在編程語言設(shè)計方面進行科研;它要能為它的設(shè)計者以及設(shè)計者的同事們改善工作環(huán)境。Go語言考慮更多的是軟件工程而不是編程語言方面的科研。或者,換句話說,它是為軟件工程服務(wù)而進行的語言設(shè)計。
但是,編程語言怎么會對軟件工程有所幫助呢?下文就是該問題的答案。
4. 痛之所在
當(dāng)Go剛推出來時,有人認(rèn)為它缺乏某些大家公認(rèn)的現(xiàn)代編程語言中所特有的特性或方法論。缺了這些東西,Go語言怎么可能會有存在的價值?我們回答這個問題的答案在于,Go的確具有一些特性,而這些特性可以解決困擾大規(guī)模軟件開發(fā)的一些問題。這些問題包括:
·Build速度緩慢
·失控的依賴關(guān)系
·每個程序員使用同一門語言的不同子集
·程序難以理解(代碼難以閱讀,文檔不全面等待)
·很多重復(fù)性的勞動
·更新的代價大
·版本偏斜(version skew)
·難以編寫自動化工具
·語言交叉Build(cross-language build)產(chǎn)生的問題
一門語言每個單個的特性都解決不了這些問題。這需要從軟件工程的大局觀,而在Go語言的設(shè)計中我們試圖致力于解決所有這些問題。
舉個簡單而獨立的例子,我們來看看程序結(jié)果的表示方式。有些評論者反對Go中使用象C一樣用花括號表示塊結(jié)構(gòu),他們更喜歡Python或 Haskell風(fēng)格式,使用空格表示縮進。可是,我們無數(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第一次進行標(biāo)準(zhǔn)化是在1989年,它提倡要在標(biāo)準(zhǔn)的頭文件中使用#ifndef這樣的”防護措施”。 這個觀點現(xiàn)已廣泛采用,就是要求每個頭文件都要用一個條件編譯語句(clause)括起來,這樣就可以將該頭文件包含多次而不會導(dǎo)致編譯錯誤。比如,Unix中的頭文件<sys/stat.h>看上去大致是這樣的:
- /* Large copyright and licensing notice */
- #ifndef _SYS_STAT_H_
- #define _SYS_STAT_H_
- /* Types and other definitions */
- #endif
此舉的目的是讓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標(biāo)準(zhǔn)的方法。Plan 9禁止在頭文件中使用#include語句,并要求將所有的#include語句放到頂層的C文件中。 當(dāng)然,這么做需要一些訓(xùn)練 —— 程序員需要一次列出所有需要的依賴,還要以正確的順序排列 —— 但是文檔可以幫忙而且實踐中效果也非常好。這么做的結(jié)果是,一個C源程序文件無論需要多少依賴,在對它進行編譯時,每個#include文件只會被讀一次。當(dāng)然,這樣一來,對于任何#include語句都可以通過先拿掉然后在進行編譯的方式判斷出這條#include語句到底有無include的必要:當(dāng)且僅當(dāng)不需要該依賴時,拿掉#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ù)處理這些信息。當(dāng)要build一個比較大型的C++二進制程序時,編譯器可能需要成千上萬次地處理頭文件<string>以了解字符串的表示方式。(根據(jù)當(dāng)時的記錄,大約在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%,就因為記錄了更準(zhǔn)確的依賴關(guān)系。即使如此,C++(或者說C引起的這個問題)的特性使得自動對依賴關(guān)系進行驗證無法得以實現(xiàn),直到今天我們?nèi)匀晃野l(fā)準(zhǔn)確掌握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?,F(xiàn)在所花的時間是27分鐘,但是,這個程序的長度以及它的依賴關(guān)系在此期間當(dāng)然也增加了。為了按比例增大build系統(tǒng)而在工程方面所付出的勞動剛剛比軟件創(chuàng)建的增長速度提前了一小步。
#p#
6. 走進 Go 語言
當(dāng)編譯緩慢進行時,我們有充足的時間來思考。關(guān)于 Go 的起源有一個傳說,話說正是一次長達45分鐘的編譯過程中,Go 的設(shè)想出現(xiàn)了。人們深信,為類似谷歌網(wǎng)絡(luò)服務(wù)這樣的大型程序編寫一門新的語言是很有意義的,軟件工程師們認(rèn)為這將極大的改善谷歌程序員的生活質(zhì)量。
盡管現(xiàn)在的討論更專注于依賴關(guān)系,這里依然還有很多其他需要關(guān)注的問題。這一門成功語言的主要因素是:
·它必須適應(yīng)于大規(guī)模開發(fā),如擁有大量依賴的大型程序,且又一個很大的程序員團隊為之工作。
·它必須是熟悉的,大致為 C 風(fēng)格的。谷歌的程序員在職業(yè)生涯的早期,對函數(shù)式語言,特別是 C家族更加熟稔。要想程序員用一門新語言快速開發(fā),新語言的語法不能過于激進。
·它必須是現(xiàn)代的。C、C++以及Java的某些方面,已經(jīng)過于老舊,設(shè)計于多核計算機、網(wǎng)絡(luò)和網(wǎng)絡(luò)應(yīng)用出現(xiàn)之前。新方法能夠滿足現(xiàn)代世界的特性,例如內(nèi)置的并發(fā)。
說完了背景,現(xiàn)在讓我們從軟件工程的角度談一談 Go 語言的設(shè)計。
7. Go 語言的依賴處理
既然我們談及了很多C 和 C++ 中依賴關(guān)系處理細節(jié),讓我們看看 Go 語言是如何處理的吧。在語義和語法上,依賴處理是由語言定義的。它們是明確的、清晰的、且“能被計算的”,就是說,應(yīng)該很容易被編寫工具分析。
在包封裝(下節(jié)的主題)之后,每個源碼文件都或有至少一個引入語句,包括 import 關(guān)鍵詞和一個用來明確當(dāng)前(只是當(dāng)前)文件引入包的字符串:
- import "encoding/json"
使 Go 語言規(guī)整的第一步就是:睿智的依賴處理,在編譯階段,語言將未被使用的依賴視為錯誤(并非警告,是錯誤)。如果源碼文件引入一個包卻沒有使用它,程序?qū)o法完成編譯。這將保證 Go 程序的依賴關(guān)系是明確的,沒有任何多余的邊際。另一方面,它可以保證編譯過程不會包含無用代碼,降低編譯消耗的時間。
第二步則是由編譯器實現(xiàn)的,它將通過深入依賴關(guān)系確保編譯效率。設(shè)想一個含有三個包的 Go 程序,其依賴關(guān)系如下:
·A 包 引用 B 包;
·B 包 引用 C 包;
·A 包 不引用 C 包
這就意味著,A 包對 C 包的調(diào)用是由對 B 包的調(diào)用間接實現(xiàn)的;也就是說,在 A 包的代碼中,不存在 C 包的標(biāo)識符。例如,C 包中有一個類型定義,它是 B 包中的某個為 A 包調(diào)用的結(jié)構(gòu)體中的字段類型,但其本身并未被 A 包調(diào)用。具一個更實際的例子,設(shè)想一下,A 包引用了一個 格式化 I/O 包 B,B 包則引用了 C 包提供的緩沖 I/O 實現(xiàn),A 包本身并沒有聲明緩沖 I/O。
要編譯這個程序,首先 C 被編譯,被依賴的包必須在依賴于它們的包之前被編譯。之后 B 包被編譯;最后 A 包被編譯,然后程序?qū)⒈贿B接。
當(dāng) A 包編譯完成之后,編譯器將讀取 B 包的目標(biāo)文件,而不是代碼。此目標(biāo)文件包含編譯器處理 A 包代碼中
- import "B"
語句所需的所有類型信息。這些信息也包含著 B 包在編譯是所需的 C 包的信息。換句話說,當(dāng) B 包被編譯時,生成的目標(biāo)文件包含了所有 B 包公共接口所需的全部依賴的類型信息。
這種設(shè)計擁有很重要的意義,當(dāng)編譯器處理 import 語句時,它將打開一個文件——該語句所明確的對象文件。當(dāng)然,這不由的讓人想起 Plan 9 C (非 ANSI C)對依賴管理方法,但不同的是,當(dāng) Go 代碼文件被編譯完成時,編譯器將寫入頭文件。同 Plan 9 C 相比,這個過程將更自動化、更高效,因為:在處理 import 時讀取的數(shù)據(jù)只是“輸出”數(shù)據(jù),而非程序代碼。這對編譯效率的影響是巨大的,而且,即便代碼增長,程序依然規(guī)整如故。處理依賴樹并對之編譯的時間相較于 C 和 C++ 的“引入被引用文件”的模型將極大的減少。
值得一提的是,這個依賴管理的通用方法并不是原始的;這些思維要追溯到1970年代的像Modula-2和Ada語言。在C語言家族里,Java就包含這一方法的元素。
為了使編譯更加高效,對象文件以導(dǎo)出數(shù)據(jù)作為它的首要步驟,這樣編譯器一旦到達文件的末尾就可以停止讀取。這種依賴管理方法是為什么Go編譯比C或 C++編譯更快的最大原因。另一個因素是Go語言把導(dǎo)出數(shù)據(jù)放在對象文件中;而一些語言要求程序員編寫或讓編譯器生成包含這一信息的另一個文件。這相當(dāng)于兩次打開文件。在Go語言中導(dǎo)入一個程序包只需要打開一次文件。并且,單一文件方法意味著導(dǎo)出數(shù)據(jù)(或在C/C++的頭文件)相對于對象文件永遠不會過時。
為了準(zhǔn)確起見,我們對Google中用Go編寫的某大型程序的編譯進行了測算,將源代碼的展開情況同前文中對C++的分析做一對比。結(jié)果發(fā)現(xiàn)是40 倍,要比C++好50倍(同樣也要比C++簡單因而處理速度也快),但是這仍然比我們預(yù)期的要大。原因有兩點。第一,我們發(fā)現(xiàn)了一個bug:Go編譯器在 export部分產(chǎn)生了大量的無用數(shù)據(jù)。第二,export數(shù)據(jù)采用了一種比較冗長的編碼方式,還有改善的余地。我們正計劃解決這些問題。
然而,僅需作50分之1的事情就把原來的Build時間從分鐘級的變?yōu)槊爰壍?,將咖啡時間轉(zhuǎn)化為交互式build。
Go的依賴圖還有另外一個特性,就是它不包含循環(huán)。Go語言定義了不允許其依賴圖中有循環(huán)性的包含關(guān)系,編譯器和鏈接器都會對此進行檢查以確保不存在循環(huán)依賴。雖然循環(huán)依賴偶爾也有用,但它在大規(guī)模程序中會引入巨大的問題。循環(huán)依賴要求編譯器同時處理大量源文件,從而會減慢增量式build的速度。更重要的是,如果允許循環(huán)依賴,我們的經(jīng)驗告訴我們,這種依賴最后會形成大片互相糾纏不清的源代碼樹,從而讓樹中各部分也變得很大,難以進行獨立管理,最后二進制文件會膨脹,使得軟件開發(fā)中的初始化、測試、重構(gòu)、發(fā)布以及其它一些任務(wù)變得過于復(fù)雜。
不支持循環(huán)import偶爾會讓人感到苦惱,但卻能讓依賴樹保持清晰明了,對package的清晰劃分也提了個更高的要求。就象Go中其它許多設(shè)計決策一樣,這會迫使程序員早早地就對一些大規(guī)模程序里的問題提前進行思考(在這種情況下,指的是package的邊界),而這些問題一旦留給以后解決往往就會永遠得不到滿意的解決。 在標(biāo)準(zhǔn)庫的設(shè)計中,大量精力花在了控制依賴關(guān)系上了。為了使用一個函數(shù),把所需的那一小段代碼拷貝過來要比拉進來一個比較大的庫強(如果出現(xiàn)新的核心依賴的話,系統(tǒng)build里的一個test會報告問題)。在依賴關(guān)系方面保持良好狀況要比代碼重用重要。在實踐中有這樣一個例子,底層的網(wǎng)絡(luò)package里有自己的整數(shù)到小數(shù)的轉(zhuǎn)換程序,就是為了避免對較大的、依賴關(guān)系復(fù)雜的格式化I/O package的依賴。還有另外一個例子,字符串轉(zhuǎn)換package的strconv擁有一個對‘可打印’字符的進行定義的private實現(xiàn),而不是將整個大哥的Unicode字符類表格拖進去, strconv里的Unicode標(biāo)準(zhǔn)是通過package的test進行驗證的。
8. 包
Go 的包系統(tǒng)設(shè)計結(jié)合了一些庫、命名控件和模塊的特性。
每個 Go 的代碼文件,例如“encoding/json/json.go”,都以包聲明開始,如同:
- package json
“json” 就是“包名稱”,一個簡單的識別符號。通常包名稱都比較精煉。
要使用包,使用 import 聲明引入代碼,并以 包路徑 區(qū)分。“路徑”的意義并未在語言中指定,而是約定為以/分割的代碼包目錄路徑,如下:
- import "encoding/json"
后面用包名稱(有別于路徑)則用來限定引入自代碼文件中包的條目。
- var dec = json.NewDecoder(reader)
這種設(shè)計非常清晰,從語法(Namevs.pkg.Name)上就能識別一個名字是否屬于某個包(在此之后)。
在我們的示例中,包的路徑是“encoding/json”而包的名稱是 json。標(biāo)準(zhǔn)資源庫以外,通常約定以項目或公司名作為命名控件的根:
- import "google/base/go/log
確認(rèn)包路徑的唯一性非常重要,而對包名稱則不必強求。包必須通過唯一的路徑引入,而包名稱則為引用者調(diào)用內(nèi)容方式的一個約定。包名稱不必唯一,可以通過引入語句重命名識別符。下面有兩個自稱為“package log”的包,如果要在單個源碼文件中引入,需要在引入時重命名一個。
- import "log" // Standard package
- import googlelog "google/base/go/log" // Google-specific package
每個公司都可能有自己的 log 包,不必要特別命名。恰恰相反:Go 的風(fēng)格建議包名稱保持簡短和清晰,且不必?fù)?dān)心沖突。
另一個例子:在 Google 代碼庫中有很多server 庫。
9. 遠程包
Go的包管理系統(tǒng)的一個重要特性是包路徑,通常是一個字符串,通過識別 網(wǎng)站資源的URL 可以增加遠程存儲庫。
下面就是如何使用儲存在 GitHub 上的包。go get 命令使用 go 編譯工具獲取資源并安裝。一旦安裝完畢,就可以如同其它包一樣引用它。
- $ go get github.com/4ad/doozer // Shell command to fetch package
- import "github.com/4ad/doozer" // Doozer client's import statement
- var client doozer.Conn // Client's use of package
這是值得注意的,go get 命令遞歸下載依賴,此特性得以實現(xiàn)的原因就是依賴關(guān)系的明確性。另外,由于引入路徑的命名空間依賴于 URL,使得 Go 相較于其它語言,在包命名上更加分散和易于擴展。
#p#
10. 語法
語法就是編程語言的用戶界面。雖然對于一門編程語言來說更重要的是語意,并且語法對于語意的影響也是有限的,但是語法決定了編程語言的可讀性和明確性。同時,語法對于編程語言相關(guān)工具的編寫至關(guān)重要:如果編程語言難以解析,那么自動化工具也將難以編寫。
Go語言因此在設(shè)計階段就為語言的明確性和相關(guān)工具的編寫做了考慮,設(shè)計了一套簡潔的語法。與C語言家族的其他幾個成員相比,Go語言的詞法更為精煉,僅25個關(guān)鍵字(C99為37個;C++11為84個;并且數(shù)量還在持續(xù)增加)。更為重要的是,Go語言的詞法是規(guī)范的,因此也是易于解析的(應(yīng)該說絕大部分是規(guī)范的;也存在一些我們本應(yīng)修正卻沒有能夠及時發(fā)現(xiàn)的怪異詞法)。與C、Java特別是C++等語言不同,Go語言可以在沒有類型信息或者符號表的情況下被解析,并且沒有類型相關(guān)的上下文信息。Go語言的詞法是易于推論的,降低了相關(guān)工具編寫的難度。
Go 語法不同于 C 的一個細節(jié)是,它的變量聲明語法相較于 C 語言,更接近 Pascal 語言。聲明的變量名稱在類型之前,而有更多的關(guān)鍵詞很:
- var fn func([]int) int
- type T struct { a, b int }
相較于 C 語言
- int (*fn)(int[]);
- struct T { int a, b; }
無論是對人還是對計算機,通過關(guān)鍵詞進行變量聲明將更容易被識別。而通過類型語法而非 C 的表達式語法對詞法分析有一個顯著的影響:它增加了語法,但消除了歧義。不過,還有一個:你可以丟掉 var 關(guān)鍵詞,而只在表達式用使用變量的類型。兩種變量聲明是等價的;只是第二個更簡短且共通用:
- var buf *bytes.Buffer = bytes.NewBuffer(x) // 精確
- buf := bytes.NewBuffer(x) // 衍生
golang.org/s/decl-syntax 是一篇更詳細講解 Go 語言聲明語句以及為什么同 C 如此不同的文章。
函數(shù)聲明語法對于簡單函數(shù)非常直接。這里有一個 Abs 函數(shù)的聲明示例,它接受一個類型為 T 的變量 x,并返回一個64位浮點值:
- func Abs(x T) float64
一個方法只是一個擁有特殊參數(shù)的函數(shù),而它的 接收器(receiver)則可以使用標(biāo)準(zhǔn)的“點”符號傳遞給函數(shù)。方法的聲明語法將接收器放在函數(shù)名稱之前的括號里。下面是一個與之前相同的函數(shù),但它是 T 類型的一個方法:
- func (x T) Abs() float64
下面則是擁有 T 類型參數(shù)的一個變量(閉包);Go 語言擁有第一類函數(shù)和閉包功能:
- negAbs := func(x T) float64 { return -Abs(x) }
最后,在 Go 語言中,函數(shù)可以返回多個值。通用的方法是成對返回函數(shù)結(jié)果和錯誤值,例如:
- func ReadByte() (c byte, err error)
- c, err := ReadByte()
- if err != nil { ... }
我們過會兒再說錯誤。
Go語言缺少的一個特性是它不支持缺省參數(shù)。這是它故意簡化的。經(jīng)驗告訴我們?nèi)笔?shù)太容易通過添加更多的參數(shù)來給API設(shè)計缺陷打補丁,進而導(dǎo)致太多使程序難以理清深圳費解的交互參數(shù)。默認(rèn)參數(shù)的缺失要求更多的函數(shù)或方法被定義,因為一個函數(shù)不能控制整個接口,但這使得一個API更清晰易懂。哪些函數(shù)也都需要獨立的名字, 使程序更清楚存在哪些組合,同時也鼓勵更多地考慮命名–一個有關(guān)清晰性和可讀性的關(guān)鍵因素。一個默認(rèn)參數(shù)缺失的緩解因素是Go語言為可變參數(shù)函數(shù)提供易用和類型安全支持的特性。
#p#
11. 命名
Go 采用了一個不常見的方法來定義標(biāo)識符的可見性(可見性:包使用者(client fo a package)通過標(biāo)識符使用包內(nèi)成員的能力)。Go 語言中,名字自己包含了可見性的信息,而不是使用常見的private,public等關(guān)鍵字來標(biāo)識可見性:標(biāo)識符首字母的大小寫決定了可見性。如果首字母是大寫字母,這個標(biāo)識符是exported(public); 否則是私有的。
·首字母大寫:名字對于包使用者可見
·否則:name(或者_Name)是不可見的。
這條規(guī)則適用于變量,類型,函數(shù),方法,常量,域成員…等所有的東西。關(guān)于命名,需要了解的就這么多。
這個設(shè)計不是個容易的決定。我們掙扎了一年多來決定怎么表示可見性。一旦我們決定了用名字的大小寫來表示可見性,我們意識到這變成了Go語言最重要特性之一。畢竟,包使用者使用包時最關(guān)注名字;把可見性放在名字上而不是類型上,當(dāng)用戶想知道某個標(biāo)示符是否是public接口,很容易就可以看出來。用了Go語言一段時間后,再用那些需要查看聲明才知道可見性的語言就會覺得很麻煩。
很清楚,這樣再一次使程序源代碼清晰簡潔的表達了程序員的意圖。
·另一個簡潔之處是Go語言有非常緊湊的范圍體系:
·全局(預(yù)定義的標(biāo)示符例如 int 和 string)
·包(包里的所有源代碼文件在同一個范圍)
·文件(只是在引入包時重命名,實踐中不是很重要)
·函數(shù)(所有函數(shù)都有,不解釋)
·塊(不解釋)
Go語言沒有命名空間,類或者其他范圍。名字只來源于很少的地方,而且所有名字都遵循一樣的范圍體系:在源碼的任何位置,一個標(biāo)示符只表示一個語言對象,而獨立于它的用法。(唯一的例外是語句標(biāo)簽(label)-break和其他類似跳轉(zhuǎn)語句的目標(biāo)地址;他們總是在當(dāng)前函數(shù)范圍有效)。
這樣就使Go語言很清晰。例如,方法總是顯式(expicit)的表明接受者(receiver)-用來訪問接受者的域成員或者方法,而不是隱式(impliciti)的調(diào)用。也就是,程序員總是寫
- rcvr.Field
(rcvr 代表接受者變量) 所以在詞法上(lexically),每個元素總是綁定到接受者類型的某個值。 同樣,包命修飾符(qualifier)總是要寫在導(dǎo)入的名字前-要寫成io.Reader而不是Reader。除了更清晰,這樣Reader這種很常用的名字可以使用在任何包中。事實上,在標(biāo)準(zhǔn)庫中有多個包都導(dǎo)出Reader,Printf這些名字,由于加上包的修飾符,這些名字引用于那個包就很清晰,不會被混淆。
最終,這些規(guī)則組合起來確保了:除了頂級預(yù)先定義好的名字例如 int,每一個名字(的第一個部分-x.y中的x)總是聲明在當(dāng)前包。
簡單說,名字是本地的。在C,C++,或者Java名字 y 可以指向任何事。在Go中,y(或Y)總是定義在包中, x.Y 的解釋也很清晰:本地查找x,Y就在x里。
這些規(guī)則為可伸縮性提供了一個很重要的價值,因為他們確保為一個包增加一個公開的名字不會破壞現(xiàn)有的包使用者。命名規(guī)則解耦包,提供了可伸縮性,清晰性和強健性。
關(guān)于命名有一個更重要的方面要說一下:方法查找總是根據(jù)名字而不是方法的簽名(類型) 。也就是說,一個類型里不會有兩個同名的方法。給定一個方法 x.M,只有一個M在x中。這樣,在只給定名字的情況下,這種方法很容易可以找到它指向那個方法。這樣也使的方法調(diào)用的實現(xiàn)簡單化了。
12. 語意
Go語言的程序語句在語意上基本與C相似。它是一種擁有指針等特性的編譯型的、靜態(tài)類型的過程式語言。它有意的給予習(xí)慣于C語言家族的程序員一種熟悉感。對于一門新興的編程語言來說,降低目標(biāo)受眾程序員的學(xué)習(xí)門檻是非常重要的;植根于C語言家族有助于確保那些掌握J(rèn)ava、JavaScript或是 C語言的年輕程序員能更輕松的學(xué)習(xí)Go語言。
盡管如此,Go語言為了提高程序的健壯性,還是對C語言的語意做出了很多小改動。它們包括:
·不能對指針進行算術(shù)運算
·沒有隱式的數(shù)值轉(zhuǎn)換
·數(shù)組的邊界總是會被檢查
·沒有類型別名(進行type X int的聲明后,X和int是兩種不同的類型而不是別名)
·++和–是語句而不是表達式
·賦值不是一種表達式
·獲取棧變量的地址是合法的(甚至是被鼓勵的)
·其他
還有一些很大的改變,同傳統(tǒng)的C 、C++ 、甚至是JAVA 的模型十分不同。它包含了對以下功能的支持:
·并發(fā)
·垃圾回收
·接口類型
·反射
·類型轉(zhuǎn)換
下面的章節(jié)從軟件工程的角度對 Go 語言這幾個主題中的兩個的討論:并發(fā)和垃圾回收。對于語言的語義和應(yīng)用的完整討論,請參閱 golang.org 網(wǎng)站中的更多資源。
13. 并發(fā)
運行于多核機器之上并擁有眾多客戶端的web服務(wù)器程序,可稱為Google里最典型程序。在這樣的現(xiàn)代計算環(huán)境中,并發(fā)很重要。這種軟件用C++或Java做都不是特別好,因為它們?nèi)痹谂c語言級對并發(fā)支持的都不夠好。
Go采用了一流的channel,體現(xiàn)為CSP的一個變種。之所以選擇CSP,部分原因是因為大家對它的熟悉程度(我們中有一位同事曾使用過構(gòu)建于 CSP中的概念之上的前任語言),另外還因為CSP具有一種在無須對其模型做任何深入的改變就能輕易添加到過程性編程模型中的特性。也即,對于類C語言,CSP可以一種最長正交化(orthogonal)的方式添加到這種語言中,為該語言提供額外的表達能力而且還不會對該語言的其它用它施加任何約束。簡言之,就是該語言的其它部分仍可保持“通常的樣子”。
這種方法就是這樣對獨立執(zhí)行非常規(guī)過程代碼的組合。
結(jié)果得到的語言可以允許我們將并發(fā)同計算無縫結(jié)合都一起。假設(shè)Web服務(wù)器必須驗證它的每個客戶端的安全證書;在Go語言中可以很容易的使用CSP來構(gòu)建這樣的軟件,將客戶端以獨立執(zhí)行的過程來管理,而且還具有編譯型語言的執(zhí)行效率,足夠應(yīng)付昂貴的加密計算。
總的來說,CSP對于Go和Google來說非常實用。在編寫Web服務(wù)器這種Go語言的典型程序時,這個模型簡直是天作之合。
有一條警告很重要:因為有并發(fā),所以Go不能成為純的內(nèi)存安全(memory safe)的語言。共享內(nèi)存是允許的,通過channel來傳遞指針也是一種習(xí)慣用法(而且效率很高)。
有些并發(fā)和函數(shù)式編程專家很失望,因為Go沒有在并發(fā)計算的上下文中采用只寫一次的方式作為值語義,比如這一點上Go和Erlang就太象。其中的原因大體上還是在于對問題域的熟悉程度和適合程度。Go的并發(fā)特性在大多數(shù)程序員所熟悉的上下文中運行得很好。Go讓使得簡單而安全的并發(fā)編程成為可能,但它并不阻止糟糕的編程方式。這個問題我們通過慣例來折中,訓(xùn)練程序員將消息傳遞看做擁有權(quán)限控制的一個版本。有句格言道:“不要通過共享內(nèi)存來通信,要通過通信來共享內(nèi)存。”
在對Go和并發(fā)編程都是剛剛新接觸的程序員方面我們經(jīng)驗有限,但也表明了這是一種非常實用的方式。程序員喜歡這種支持并發(fā)為網(wǎng)絡(luò)軟件所帶來的簡單性,而簡單性自然會帶來健壯性。
14. 垃圾回收
對于一門系統(tǒng)級的編程語言來說,垃圾回收可能會是一項非常有爭議的特性,但我們還是毫不猶豫地確定了Go語言將會是一門擁有垃圾回收機制的編程語言。Go語言沒有顯式的內(nèi)存釋放操作,那些被分配的內(nèi)存只能通過垃圾回收器這一唯一途徑來返回內(nèi)存池。
做出這個決定并不難,因為內(nèi)存管理對于一門編程語言的實際使用方式有著深遠的影響。在C和C++中,程序員們往往需要花費大量的時間和精力在內(nèi)存的分配和釋放上,這樣的設(shè)計有助于暴露那些本可以被隱藏得很好的內(nèi)存管理的細節(jié);但反過來說,對于內(nèi)存使用的過多考量又限制了程序員使用內(nèi)存的方式。相比之下,垃圾回收使得接口更容易被指定。
此外,擁有自動化的內(nèi)存管理機制對于一門并發(fā)的面向?qū)ο蟮木幊陶Z言來說很關(guān)鍵,因為一個內(nèi)存塊可能會在不同的并發(fā)執(zhí)行單元間被來回傳遞,要管理這樣一塊內(nèi)存的所有權(quán)對于程序員來說將會是一項挑戰(zhàn)。將行為與資源的管理分離是很重要的。
垃圾回收使得Go語言在使用上顯得更加簡單。
當(dāng)然,垃圾回收機制會帶來很大的成本:資源的消耗、回收的延遲以及復(fù)雜的實現(xiàn)等。盡管如此,我們相信它所帶來的好處,特別是對于程序員的編程體驗來說,是要大于它所帶來的成本的,因為這些成本大都是加諸在編程語言的實現(xiàn)者身上。
在面向用戶的系統(tǒng)中使用Java來進行服務(wù)器編程的經(jīng)歷使得一些程序員對垃圾回收顧慮重重:不可控的資源消耗、極大的延遲以及為了達到較好的性能而需要做的一大堆參數(shù)優(yōu)化。Go語言則不同,語言本身的屬性能夠減輕以上的一些顧慮,雖然不是全部。
有個關(guān)鍵點在于,Go為程序員提供了通過控制數(shù)據(jù)結(jié)構(gòu)的格式來限制內(nèi)存分配的手段。請看下面這個簡單的類型定義了包含一個字節(jié)(數(shù)組)型的緩沖區(qū):
- type X struct {
- a, b, c int
- buf [256]byte
- }
在Java中,buffer字段需要再次進行內(nèi)存分配,因為需要另一層的間接訪問形式。然而在Go中,該緩沖區(qū)同包含它的struct一起分配到了一塊單獨的內(nèi)存塊中,無需間接形式。對于系統(tǒng)編程,這種設(shè)計可以得到更好的性能并減少回收器(collector)需要了解的項目數(shù)。要是在大規(guī)模的程序中,這么做導(dǎo)致的差別會非常巨大。
有個更加直接一點的例子,在Go中,可以非常容易和高效地提供二階內(nèi)存分配器(second-order allocator),例如,為一個由大量struct組成的大型數(shù)組分配內(nèi)存,并用一個自由列表(a free list)將它們鏈接起來的arena分配器(an arena allocator)。在重復(fù)使用大量小型數(shù)據(jù)結(jié)構(gòu)的庫中,可以通過少量的提前安排,就能不產(chǎn)生任何垃圾還能兼顧高效和高響應(yīng)度。
雖然Go是一種支持內(nèi)存垃圾回收的編程語言,但是資深程序員能夠限制施加給回收器的壓力從而提高程序的運行效率(Go的安裝包中還提供了一些非常好的工具,用這些工具可以研究程序運行過程中動態(tài)內(nèi)存的性能。)
要給程序員這樣的靈活性,Go必需支持指向分配在堆中對象的指針,我們將這種指針稱為內(nèi)部指針。上文的例子中X.buff字段保存于struct之中,但也可以保留這個內(nèi)部字段的地址。比如,可以將這個地址傳遞給I/O子程序。在Java以及許多類似的支持垃圾回收的語音中,不可能構(gòu)造象這樣的內(nèi)部指針,但在Go中這么做很自然。這樣設(shè)計的指針會影響可以使用的回收算法,并可能會讓算法變得更難寫,但經(jīng)過慎重考慮,我們決定允許內(nèi)部指針是必要的,因為這對程序員有好處,讓大家具有降低對(可能實現(xiàn)起來更困難)回收器的壓力的能力。到現(xiàn)在為止,我們的將大致相同的Go和Java程序進行對比的經(jīng)驗表明,使用內(nèi)部指針能夠大大影響arena總計大型、延遲和回收次數(shù)。
總的說來,Go是一門支持垃圾回收的語言,但它同時也提供給程序員一些手段,可以對回收開銷進行控制。
垃圾回收器目前仍在積極地開發(fā)中。當(dāng)前的設(shè)計方案是并行的邊標(biāo)示邊掃描(mark-and-sweep)的回收器,未來還有機會提高其性能甚至其設(shè)計方案。(Go語言規(guī)范中并沒有限定必需使用哪種特定的回收器實現(xiàn)方案)。盡管如此,如果程序員在使用內(nèi)存時小心謹(jǐn)慎,當(dāng)前的實現(xiàn)完全可以在生產(chǎn)環(huán)境中使用。
#p#
15. 要組合,不要繼承
Go 采用了一個不尋常的方法來支持面向?qū)ο缶幊?,允許添加方法到任意類型,而不僅僅是class,但是并沒有采用任何類似子類化的類型繼承。這也就意味著沒有類型體系(type hierarchy)。這是精心的設(shè)計選擇。雖然類型繼承已經(jīng)被用來建立很多成功的軟件,但是我們認(rèn)為它還是被過度使用了,我們應(yīng)該在這個方向上退一步。
Go使用接口(interface), 接口已經(jīng)在很多地方被詳盡的討論過了 (例如 research.swtch.com/interfaces ), 但是這里我還是簡單的說一下。
在 Go 中,接口只是一組方法。例如,下面是標(biāo)準(zhǔn)庫中的Hash接口的定義。
- type Hash interface {
- Write(p []byte) (n int, err error)
- Sum(b []byte) []byte
- Reset()
- Size() int
- BlockSize() int
- }
實現(xiàn)了這組方法的所有數(shù)據(jù)類型都滿足這個接口;而不需要用implements聲明。即便如此,由于接口匹配在編譯時靜態(tài)檢查,所以這樣也是類型安全的。
一個類型往往要滿足多個接口,其方法的每一個子集滿足每一個接口。例如,任何滿足Hash接口的類型同時也滿足Writer接口:
- type Writer interface {
- Write(p []byte) (n int, err error)
- }
這種接口滿足的流動性會促成一種不同的軟件構(gòu)造方法。但在解釋這一點之前,我們應(yīng)該先解釋一下為什么Go中沒有子類型化(subclassing)。
面向?qū)ο蟮木幊烫峁┝艘环N強大的見解:數(shù)據(jù)的行為可以獨立于數(shù)據(jù)的表示進行泛化。這個模型在行為(方法集)是固定不變的情況下效果最好,但是,一旦你為某類型建立了一個子類型并添加了一個方法后,其行為就再也不同了。如果象Go中的靜態(tài)定義的接口這樣,將行為集固定下來,那么這種行為的一致性就使得可以把數(shù)據(jù)和程序一致地、正交地(orthogonally)、安全地組合到一起了。
有個極端一點的例子,在Plan 9的內(nèi)核中,所有的系統(tǒng)數(shù)據(jù)項完全都實現(xiàn)了同一個接口,該接口是一個由14個方法組成的文件系統(tǒng)API。即使在今天看來,這種一致性所允許的對象組合水平在其它系統(tǒng)中是很罕見的。這樣的例子數(shù)不勝數(shù)。這里還有一個:一個系統(tǒng)可以將TCP棧導(dǎo)入(這是Plan 9中的術(shù)語)一個不支持TCP甚至以太網(wǎng)的計算機中,然后通過網(wǎng)絡(luò)將其連接到另一臺具有不同CPU架構(gòu)的機器上,通過導(dǎo)入其/proctree,就可以允許一個本地的調(diào)試器對遠程的進程進行斷點調(diào)試。這類操作在Plan 9中很是平常,一點也不特殊。能夠做這樣的事情的能力完全來自其設(shè)計方案,無需任何特殊安排(所有的工作都是在普通的C代碼中完成的)。
我們認(rèn)為,這種系統(tǒng)構(gòu)建中的組合風(fēng)格完全被推崇類型層次結(jié)構(gòu)設(shè)計的語言所忽略了。類型層次結(jié)構(gòu)造成非常脆弱的代碼。層次結(jié)構(gòu)必需在早期進行設(shè)計,通常會是程序設(shè)計的第一步,而一旦寫出程序后,早期的決策就很難進行改變了。所以,類型層次結(jié)構(gòu)這種模型會促成早期的過度設(shè)計,因為程序員要盡力對軟件可能需要的各種可能的用法進行預(yù)測,不斷地為了避免掛一漏萬,不斷的增加類型和抽象的層次。這種做法有點顛倒了,系統(tǒng)各個部分之間交互的方式本應(yīng)該隨著系統(tǒng)的發(fā)展而做出相應(yīng)的改變,而不應(yīng)該在一開始就固定下來。
因此,通過使用簡單到通常只有一個方法的接口來定義一些很細小的行為,將這些接口作為組件間清晰易懂的邊界, Go鼓勵使用組合而不是繼承,
上文中提到過Writer接口,它定義于io包中。任何具有相同簽名(signature)的Write方法的類型都可以很好的同下面這個與之互補的Reader接口共存:
- type Reader interface {
- Read(p []byte) (n int, err error)
- }
這兩個互補的方法可以拿來進行具有多種不同行為的、類型安全的連接(chaining),比如,一般性的Unix管道。文件、緩沖區(qū)、加密程序、壓縮程序、圖像編碼程序等等都能夠連接到一起。與C中的FILE*不同,F(xiàn)printf格式化I/O子程序帶有anio.Writer。格式化輸出程序并不了解它要輸出到哪里;可能是輸出給了圖像編碼程序,該程序接著輸出給了壓縮程序,該程序再接著輸出給了加密程序,最后加密程序輸出到了網(wǎng)絡(luò)連接之中。
接口組合是一種不同的編程風(fēng)格,已經(jīng)熟悉了類型層次結(jié)構(gòu)的人需要調(diào)整其思維方式才能做得好,但調(diào)整思維所得到的是類型層次結(jié)構(gòu)中難以獲得的具有高度適應(yīng)性的設(shè)計方案。
還要注意,消除了類型層次結(jié)構(gòu)也就消除了一種形式的依賴層次結(jié)構(gòu)。接口滿足式的設(shè)計使得程序無需預(yù)先確定的合約就能實現(xiàn)有機增長,而且這種增長是線性的;對一個接口進行更改影響的只有直接使用該接口的類型;不存在需要更改的子樹。 沒有implements聲明會讓有些人感覺不安但這么做可以讓程序以自然、優(yōu)雅、安全的方式進行發(fā)展。
Go的接口對程序設(shè)計有一個主要的影響。我們已經(jīng)看到的一個地方就是使用具有接口參數(shù)的函數(shù)。這些不是方法而是函數(shù)。幾個例子就應(yīng)該能說明它們的威力。ReadAll返回一段字節(jié)(數(shù)組),其中包含的是能夠從anio.Reader中讀出來的所有數(shù)據(jù):
- func ReadAll(r io.Reader) ([]byte, error)
封裝器 —— 指的是以接口為參數(shù)并且其返回結(jié)果也是一個接口的函數(shù),用的也很廣泛。這里有幾個原型。LoggingReader將每次的Read調(diào)用記錄到傳人的參數(shù) r這個Reader中。LimitingReader在讀到n字節(jié)后便停止讀取操作。ErrorInjector通過模擬I/O錯誤用以輔助完成測試工作。還有更多的例子。
- func LoggingReader(r io.Reader) io.Reader
- func LimitingReader(r io.Reader, n int64) io.Reader
- func ErrorInjector(r io.Reader) io.Reader
這種設(shè)計方法同層次型的、子類型繼承方法完全不同。它們更加松散(甚至是臨時性的),屬于有機式的、解耦式的、獨立式的,因而具有強大的伸縮性。
16. 錯誤
Go不具有傳統(tǒng)意義上的異常機制,也就是說,Go里沒有同錯誤處理相關(guān)的控制結(jié)構(gòu)。(Go的確為類似被零除這樣的異常情況的提供了處理機制。 有一對叫做panic和recover的內(nèi)建函數(shù),用來讓程序員處理這些情況。然而,這些函數(shù)是故意弄的不好用因而也很少使用它們,而且也不像Java庫中使用異常那樣,并沒有將它們集成到庫中。)
Go語言中錯誤處理的一個關(guān)鍵特性是一個預(yù)先定義為error的接口類型,它具有一個返回一個字符串讀到Error方法,表示了一個錯誤值。:
- type error interface {
- Error() string
- }
Go的庫使用error類型的數(shù)據(jù)返回對錯誤的描述。結(jié)合函數(shù)具有返回多個數(shù)值的能力, 在返回計算結(jié)果的同時返回可能出現(xiàn)的錯誤值很容易實現(xiàn)。比如,Go中同C里的對應(yīng)的getchar不會在EOF處返回一個超范圍的值,也不會拋出異常;它只是返回在返回讀到的字符的同時返回一個error值,以error的值為nil表示讀取成功。以下所示為帶緩沖區(qū)的I/O包中bufio.Reader 類型的ReadByte方法的簽名:
- func (b *Reader) ReadByte() (c byte, err error)
這樣的設(shè)計簡單清晰,也非常容易理解。error僅僅是一種值,程序可以象對其它別的類型的值一樣,對error值進行計算。
Go中不包含異常,是我們故意為之的。雖然有大量的批評者并不同意這個設(shè)計決策,但是我們相信有幾個原因讓我們認(rèn)為這樣做才能編寫出更好的軟件。
首先,計算機程序中的錯誤并不是真正的異常情況。例如,無法打開一個文件是種常見的問題,無需任何的特殊語言結(jié)構(gòu),if和return完全可以勝任。
- f, err := os.Open(fileName)
- if err != nil {
- return err
- }
再者,如果錯誤要使用特殊的控制結(jié)構(gòu),錯誤處理就會扭曲處理錯誤的程序的控制流(control flow)。象Java那樣try-catch-finally語句結(jié)構(gòu)會形成交叉重疊的多個控制流,這些控制流之間的交互方式非常復(fù)雜。雖然相比較而言,Go檢查錯誤的方式更加繁瑣,但這種顯式的設(shè)計使得控制流更加直截了當(dāng) —— 從字面上的確如此。
毫無疑問這會使代碼更長一些,但如此編碼帶來的清晰度和簡單性可以彌補其冗長的缺點。顯式地錯誤檢查會迫使程序員在錯誤出現(xiàn)的時候?qū)﹀e誤進行思考并進行相應(yīng)的處理。異常機制只是將錯誤處理推卸到了調(diào)用堆棧之中,直到錯過了修復(fù)問題或準(zhǔn)確診斷錯誤情況的時機,這就使得程序員更容易去忽略錯誤而不是處理錯誤了。
#p#
17. 工具
軟件工程需要工具的支持。每種語言都要運行于同其它語言共存的環(huán)境,它還需要大量工具才能進行編譯、編輯、調(diào)試、性能分析、測試已經(jīng)運行。
Go的語法、包管理系統(tǒng)、命名規(guī)則以及其它功能在設(shè)計時就考慮了要易于為這種語言編寫工具以及包括詞法分析器、語法分析器以及類型檢測器等等在內(nèi)的各種庫。
操作Go程序的工具非常容易編寫,因此現(xiàn)在已經(jīng)編寫出了許多這樣的工具,其中有些工具對軟件工程來講已經(jīng)產(chǎn)生了一些值得關(guān)注的效果。
其中最著名的是gofmt,它是Go源程序的格式化程序。該項目伊始,我們就將Go程序定位為由機器對其進行格式化, 從而消除了在程序員中具有爭議的一大類問題:我要以什么樣的格式寫代碼?我們對我們所需的所有Go程序運行Gofmt,絕大多數(shù)開源社區(qū)也用它進行代碼格式化。 它是作為“提交前”的例行檢查運行的,它在代碼提交到代碼庫之前運行,以確保所有檢入的Go程序都是具有相同的格式。
Go fmt 往往被其使用者推崇為Go最好的特性之一,盡管它本身并屬于Go語言的一個部分。 存在并使用gofmt意味著,從一開始社區(qū)里看到的Go代碼就是用它進行格式化過的代碼,因此Go程序具有現(xiàn)在已為人熟知的單一風(fēng)格。同一的寫法使得代碼閱讀起來更加容易,因而用起來速度也快。沒有在格式化代碼方面浪費的時間就是剩下來的時間。Gofmt也會影響伸縮性:既然所有的代碼看上去格式完全相同,團隊就更易于展開合作,用起別人的代碼來也更容易。
Go fmt 還讓編寫我們并沒有清晰地預(yù)見到的另一類工具成為可能。Gofmt的運行原理就是對源代碼進行語法分析,然后根據(jù)語法樹本身對代碼進行格式化。這讓在格式化代碼之前對語法樹進行更改成為可能,因此產(chǎn)生了一批進行自動重構(gòu)的工具。這些工具編寫起來很容易,因為它們直接作用于語法分析樹之上,因而其語義可以非常多樣化,最后產(chǎn)生的格式化代碼也非常規(guī)范。
第一個例子就是gofmt本身的a-r(重寫)標(biāo)志,該標(biāo)志采用了一種很簡單的模式匹配語言,可以用來進行表達式級的重寫。例如,有一天我們引入了一段表達式右側(cè)缺省值:該段表達式的長度。整個Go源代碼樹要使用該缺省值進行更新,僅限使用下面這一條命令:
- gofmt -r 'a[b:len(a)] -> a[b:]'
該變換中的一個關(guān)鍵點在于,因為輸入和輸出二者均為規(guī)范格式(canonical format),對源代碼的唯一更改也是語義上的更改
采用與此類似但更復(fù)雜一些的處理就可以讓gofmt用于在Go語言中的語句以換行而不再是分號結(jié)尾的情況下,對語法樹進行相應(yīng)的更新。
gofix是另外一個非常重要的工具,它是語法樹重寫模塊,而且它用Go語言本身所編寫的,因而可以用來完成更加高級的重構(gòu)操作。 gofix工具可以用來對直到Go 1發(fā)布為止的所有API和語言特性進行全方位修改,包括修改從map中刪除數(shù)據(jù)項的語法、引入操作時間值的一個完全不同的API等等很多更新。隨著這些更新一一推出,使用者可以通過運行下面這條簡單的命令對他們的所有代碼進行更新
- gofix
注意,這些工具允許我們即使在舊代碼仍舊能夠正常運行的情況下對它們進行更新。 因此,Go的代碼庫很容易就能隨著 library的更新而更新。棄用舊的API可以很快以自動化的形式實現(xiàn),所以只有最新版本的API需要維護。例如,我們最近將Go的協(xié)議緩沖區(qū)實現(xiàn)更改為使用“getter”函數(shù),而原本的接口中并不包含該函數(shù)。我們對Google中所有的Go代碼運行了gofix命令,對所有使用了協(xié)議緩沖區(qū)的程序進行了更新,所以,現(xiàn)在使用中的協(xié)議緩沖區(qū)API只有一個版本。要對C++或者 Java庫進行這樣的全面更新,對于Google這樣大的代碼庫來講,幾乎是不可能實現(xiàn)的。
Go的標(biāo)準(zhǔn)庫中具有語法分析包也使得編寫大量其它工具成為可能。例如,用來管理程序構(gòu)建的具有類似從遠程代碼庫中獲取包等功能的gotool;用來在library更新時驗證API兼容性協(xié)約的文檔抽取程序godoc;類似還有很多工具。
雖然類似這些工具很少在討論語言設(shè)計時提到過,但是它們屬于一種語言的生態(tài)系統(tǒng)中不可或缺的部分。事實上Go在設(shè)計時就考慮了工具的事情,這對該語言及其library以及整個社區(qū)的發(fā)展都已產(chǎn)生了巨大的影響。
18. 結(jié)論
Go在google內(nèi)部的使用正在越來越廣泛。
很多大型的面向用戶的服務(wù)都在使用它,包括youtube.comanddl.google.com(為chrome、android等提供下載服務(wù)的下載服務(wù)器),我們的golang.org也是用go搭建的。當(dāng)然很多小的服務(wù)也在使用go,大部分都是使用Google App Engine上的內(nèi)建Go環(huán)境。
還有很多公司也在使用Go,名單很長,其中有一些是很有名的:
·BBC國際廣播
·Canonical
·Heroku
·諾基亞
·SoundCloud
看起來Go已經(jīng)實現(xiàn)了它的目標(biāo)。雖然一切看起來都很好,但是現(xiàn)在就說它已經(jīng)成功還太早。到目前為止我們還需要更多的使用經(jīng)驗,特別是大型的項目(百萬航代碼級),來表明我們已經(jīng)成功搭建一種可擴展的語言。
相對規(guī)模比較小,有些小問題還不太對,可能會在該語言的下一個(Go 2?)版本中得以糾正。例如,變量定義的語法形式過多,程序員容易被非nil接口中的nil值搞糊涂,還有許多l(xiāng)ibrary以及接口的方面的細節(jié)還可以再經(jīng)過一輪的設(shè)計。
但是,值得注意的是,在升級到Go版本1時,gofix和gofmt給予了我們修復(fù)很多其它問題的機會。今天的Go同其設(shè)計者所設(shè)想的樣子之間的距離因此而更近了一步,要是沒有這些工具的支持就很難做到這一點,而這些工具也是因為該語言的設(shè)計思想才成為可能的。
不過,現(xiàn)在不是萬事皆定了。我們?nèi)栽趯W(xué)習(xí)中(但是,該語言本身現(xiàn)在已經(jīng)確定下來了。)
該語言有個最大的弱點,就是它的實現(xiàn)仍需進一步的工作。特別是其編譯器所產(chǎn)生的代碼以及runtime的運行效率還有需要改善的地方,它們還在繼續(xù)的改善之中。現(xiàn)在已經(jīng)有了一些進展;實際上,有些基準(zhǔn)測試表明,同2012年早期發(fā)布的第一個Go版本1相比,現(xiàn)在開發(fā)版的性能已得到雙倍提升。
19. 總結(jié)
軟件工程指導(dǎo)下的Go語言的設(shè)計。同絕大多數(shù)通用型編程語言相比,Go語言更多的是為了解決我們在構(gòu)建大型服務(wù)器軟件過程中所遇到的軟件工程方面的問題而設(shè)計的。 乍看上去,這么講可能會讓人感覺Go非常無趣且工業(yè)化,但實際上,在設(shè)計過程中就著重于清晰和簡潔,以及較高的可組合性,最后得到的反而會是一門使用起來效率高而且很有趣的編程語言,很多程序員都會發(fā)現(xiàn),它有極強的表達力而且功能非常強大。
造成這種效果的因素有:
·清晰的依賴關(guān)系
·清晰的語法
·清晰的語義
·偏向組合而不是繼承
·編程模型(垃圾回收、并發(fā))所代理的簡單性
·易于為它編寫工具(Easy tooling )(gotool、gofmt、godoc、gofix)
如果你還沒有嘗試過用Go編程,我們建議你試一下。
原文鏈接:http://www.oschina.net/translate/go-at-google-language-design-in-the-service-of-software-engineering
英文原文:Go at Google: Language Design in the Service of Software Engineering