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