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















 
 
 



 
 
 
 