Go在谷歌:以軟體工程為目的的語言設計

發表於2013-03-21

編譯:oschina,原文:Go at Google: Language Design in the Service of Software Engineering

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>看上去大致是這樣的:

此舉的目的是讓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。現在所花的時間是27分鐘,但是,這個程式的長度以及它的依賴關係在此期間當然也增加了。為了按比例增大build系統而在工程方面所付出的勞動剛剛比軟體建立的增長速度提前了一小步。

6. 走進 Go 語言

當編譯緩慢進行時,我們有充足的時間來思考。關於 Go 的起源有一個傳說,話說正是一次長達45分鐘的編譯過程中,Go 的設想出現了。人們深信,為類似谷歌網路服務這樣的大型程式編寫一門新的語言是很有意義的,軟體工程師們認為這將極大的改善谷歌程式設計師的生活質量。

儘管現在的討論更專注於依賴關係,這裡依然還有很多其他需要關注的問題。這一門成功語言的主要因素是:

  • 它必須適應於大規模開發,如擁有大量依賴的大型程式,且又一個很大的程式設計師團隊為之工作。
  • 它必須是熟悉的,大致為 C 風格的。谷歌的程式設計師在職業生涯的早期,對函式式語言,特別是 C家族更加熟稔。要想程式設計師用一門新語言快速開發,新語言的語法不能過於激進。
  • 它必須是現代的。C、C++以及Java的某些方面,已經過於老舊,設計於多核計算機、網路和網路應用出現之前。新方法能夠滿足現代世界的特性,例如內建的併發。

說完了背景,現在讓我們從軟體工程的角度談一談 Go 語言的設計。

7. Go 語言的依賴處理

既然我們談及了很多C 和 C++ 中依賴關係處理細節,讓我們看看 Go 語言是如何處理的吧。在語義和語法上,依賴處理是由語言定義的。它們是明確的、清晰的、且“能被計算的”,就是說,應該很容易被編寫工具分析。

在包封裝(下節的主題)之後,每個原始碼檔案都或有至少一個引入語句,包括 import 關鍵詞和一個用來明確當前(只是當前)檔案引入包的字串:

使 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 包程式碼中

語句所需的所有型別資訊。這些資訊也包含著 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”,都以包宣告開始,如同:

“json” 就是“包名稱”,一個簡單的識別符號。通常包名稱都比較精煉。
要使用包,使用 import 宣告引入程式碼,並以 包路徑 區分。“路徑”的意義並未在語言中指定,而是約定為以/分割的程式碼包目錄路徑,如下:

後面用包名稱(有別於路徑)則用來限定引入自程式碼檔案中包的條目。

這種設計非常清晰,從語法(Namevs.pkg.Name)上就能識別一個名字是否屬於某個包(在此之後)。

在我們的示例中,包的路徑是“encoding/json”而包的名稱是 json。標準資源庫以外,通常約定以專案或公司名作為命名控制元件的根:

確認包路徑的唯一性非常重要,而對包名稱則不必強求。包必須通過唯一的路徑引入,而包名稱則為引用者呼叫內容方式的一個約定。包名稱不必唯一,可以通過引入語句重新命名識別符。下面有兩個自稱為“package log”的包,如果要在單個原始碼檔案中引入,需要在引入時重新命名一個。

每個公司都可能有自己的 log 包,不必要特別命名。恰恰相反:Go 的風格建議包名稱保持簡短和清晰,且不必擔心衝突。

另一個例子:在 Google 程式碼庫中有很多server 庫。

9. 遠端包

Go的包管理系統的一個重要特性是包路徑,通常是一個字串,通過識別 網站資源的URL 可以增加遠端儲存庫。

下面就是如何使用儲存在 github 上的包。go get 命令使用 go 編譯工具獲取資源並安裝。一旦安裝完畢,就可以如同其它包一樣引用它。

這是值得注意的,go get 命令遞迴下載依賴,此特性得以實現的原因就是依賴關係的明確性。另外,由於引入路徑的名稱空間依賴於 URL,使得 Go 相較於其它語言,在包命名上更加分散和易於擴充套件。

10. 語法

語法就是程式語言的使用者介面。雖然對於一門程式語言來說更重要的是語意,並且語法對於語意的影響也是有限的,但是語法決定了程式語言的可讀性和明確性。同時,語法對於程式語言相關工具的編寫至關重要:如果程式語言難以解析,那麼自動化工具也將難以編寫。

Go語言因此在設計階段就為語言的明確性和相關工具的編寫做了考慮,設計了一套簡潔的語法。與C語言家族的其他幾個成員相比,Go語言的詞法更為精煉,僅25個關鍵字(C99為37個;C++11為84個;並且數量還在持續增加)。更為重要的是,Go語言的詞法是規範的,因此也是易於解析的(應該說絕大部分是規範的;也存在一些我們本應修正卻沒有能夠及時發現的怪異詞法)。與C、Java特別是C++等語言不同,Go語言可以在沒有型別資訊或者符號表的情況下被解析,並且沒有型別相關的上下文資訊。Go語言的詞法是易於推論的,降低了相關工具編寫的難度。

Go 語法不同於 C 的一個細節是,它的變數宣告語法相較於 C 語言,更接近 Pascal 語言。宣告的變數名稱在型別之前,而有更多的關鍵詞很:

相較於 C 語言

無論是對人還是對計算機,通過關鍵詞進行變數宣告將更容易被識別。而通過型別語法而非 C 的表示式語法對詞法分析有一個顯著的影響:它增加了語法,但消除了歧義。不過,還有一個:你可以丟掉 var 關鍵詞,而只在表示式用使用變數的型別。兩種變數宣告是等價的;只是第二個更簡短且共通用:

golang.org/s/decl-syntax 是一篇更詳細講解 Go 語言宣告語句以及為什麼同 C 如此不同的文章。

函式宣告語法對於簡單函式非常直接。這裡有一個 Abs 函式的宣告示例,它接受一個型別為 T 的變數 x,並返回一個64位浮點值:

一個方法只是一個擁有特殊引數的函式,而它的 接收器(receiver)則可以使用標準的“點”符號傳遞給函式。方法的宣告語法將接收器放在函式名稱之前的括號裡。下面是一個與之前相同的函式,但它是 T 型別的一個方法:

下面則是擁有 T 型別引數的一個變數(閉包);Go 語言擁有第一類函式和閉包功能:

最後,在 Go 語言中,函式可以返回多個值。通用的方法是成對返回函式結果和錯誤值,例如:

我們過會兒再說錯誤。

Go語言缺少的一個特性是它不支援預設引數。這是它故意簡化的。經驗告訴我們預設引數太容易通過新增更多的引數來給API設計缺陷打補丁,進而導致太多使程式難以理清深圳費解的互動引數。預設引數的缺失要求更多的函式或方法被定義,因為一個函式不能控制整個介面,但這使得一個API更清晰易懂。哪些函式也都需要獨立的名字, 使程式更清楚存在哪些組合,同時也鼓勵更多地考慮命名–一個有關清晰性和可讀性的關鍵因素。一個預設引數缺失的緩解因素是Go語言為可變引數函式提供易用和型別安全支援的特性。

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 代表接受者變數) 所以在詞法上(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為程式設計師提供了通過控制資料結構的格式來限制記憶體分配的手段。請看下面這個簡單的型別定義了包含一個位元組(陣列)型的緩衝區:

在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語言規範中並沒有限定必需使用哪種特定的回收器實現方案)。儘管如此,如果程式設計師在使用記憶體時小心謹慎,當前的實現完全可以在生產環境中使用。

15. 要組合,不要繼承

Go 採用了一個不尋常的方法來支援物件導向程式設計,允許新增方法到任意型別,而不僅僅是class,但是並沒有採用任何類似子類化的型別繼承。這也就意味著沒有型別體系(type hierarchy)。這是精心的設計選擇。雖然型別繼承已經被用來建立很多成功的軟體,但是我們認為它還是被過度使用了,我們應該在這個方向上退一步。

Go使用介面(interface), 介面已經在很多地方被詳盡的討論過了 (例如 research.swtch.com/interfaces ), 但是這裡我還是簡單的說一下。

在 Go 中,介面只是一組方法。例如,下面是標準庫中的Hash介面的定義。

實現了這組方法的所有資料型別都滿足這個介面;而不需要用implements宣告。即便如此,由於介面匹配在編譯時靜態檢查,所以這樣也是型別安全的。

一個型別往往要滿足多個介面,其方法的每一個子集滿足每一個介面。例如,任何滿足Hash介面的型別同時也滿足Writer介面:

這種介面滿足的流動性會促成一種不同的軟體構造方法。但在解釋這一點之前,我們應該先解釋一下為什麼Go中沒有子型別化(subclassing)。

物件導向的程式設計提供了一種強大的見解:資料的行為可以獨立於資料的表示進行泛化。這個模型在行為(方法集)是固定不變的情況下效果最好,但是,一旦你為某型別建立了一個子型別並新增了一個方法後,其行為就再也不同了。如果象Go中的靜態定義的介面這樣,將行為集固定下來,那麼這種行為的一致性就使得可以把資料和程式一致地、正交地(orthogonally)、安全地組合到一起了。

有個極端一點的例子,在Plan 9的核心中,所有的系統資料項完全都實現了同一個介面,該介面是一個由14個方法組成的檔案系統API。即使在今天看來,這種一致性所允許的物件組合水平在其它系統中是很罕見的。這樣的例子數不勝數。這裡還有一個:一個系統可以將TCP棧匯入(這是Plan 9中的術語)一個不支援TCP甚至乙太網的計算機中,然後通過網路將其連線到另一臺具有不同CPU架構的機器上,通過匯入其/proctree,就可以允許一個本地的偵錯程式對遠端的程式進行斷點除錯。這類操作在Plan 9中很是平常,一點也不特殊。能夠做這樣的事情的能力完全來自其設計方案,無需任何特殊安排(所有的工作都是在普通的C程式碼中完成的)。

我們認為,這種系統構建中的組合風格完全被推崇型別層次結構設計的語言所忽略了。型別層次結構造成非常脆弱的程式碼。層次結構必需在早期進行設計,通常會是程式設計的第一步,而一旦寫出程式後,早期的決策就很難進行改變了。所以,型別層次結構這種模型會促成早期的過度設計,因為程式設計師要盡力對軟體可能需要的各種可能的用法進行預測,不斷地為了避免掛一漏萬,不斷的增加型別和抽象的層次。這種做法有點顛倒了,系統各個部分之間互動的方式本應該隨著系統的發展而做出相應的改變,而不應該在一開始就固定下來。

因此,通過使用簡單到通常只有一個方法的介面來定義一些很細小的行為,將這些介面作為元件間清晰易懂的邊界, Go鼓勵使用組合而不是繼承

上文中提到過Writer介面,它定義於io包中。任何具有相同簽名(signature)的Write方法的型別都可以很好的同下面這個與之互補的Reader介面共存:

這兩個互補的方法可以拿來進行具有多種不同行為的、型別安全的連線(chaining),比如,一般性的Unix管道。檔案、緩衝區、加密程式、壓縮程式、影像編碼程式等等都能夠連線到一起。與C中的FILE*不同,Fprintf格式化I/O子程式帶有anio.Writer。格式化輸出程式並不瞭解它要輸出到哪裡;可能是輸出給了影像編碼程式,該程式接著輸出給了壓縮程式,該程式再接著輸出給了加密程式,最後加密程式輸出到了網路連線之中。

介面組合是一種不同的程式設計風格,已經熟悉了型別層次結構的人需要調整其思維方式才能做得好,但調整思維所得到的是型別層次結構中難以獲得的具有高度適應性的設計方案。

還要注意,消除了型別層次結構也就消除了一種形式的依賴層次結構。介面滿足式的設計使得程式無需預先確定的合約就能實現有機增長,而且這種增長是線性的;對一個介面進行更改影響的只有直接使用該介面的型別;不存在需要更改的子樹。 沒有implements宣告會讓有些人感覺不安但這麼做可以讓程式以自然、優雅、安全的方式進行發展。

Go的介面對程式設計有一個主要的影響。我們已經看到的一個地方就是使用具有介面引數的函式。這些不是方法而是函式。幾個例子就應該能說明它們的威力。ReadAll返回一段位元組(陣列),其中包含的是能夠從anio.Reader中讀出來的所有資料:

封裝器 —— 指的是以介面為引數並且其返回結果也是一個介面的函式,用的也很廣泛。這裡有幾個原型。LoggingReader將每次的Read呼叫記錄到傳人的引數r這個Reader中。LimitingReader在讀到n位元組後便停止讀取操作。ErrorInjector通過模擬I/O錯誤用以輔助完成測試工作。還有更多的例子。

這種設計方法同層次型的、子型別繼承方法完全不同。它們更加鬆散(甚至是臨時性的),屬於有機式的、解耦式的、獨立式的,因而具有強大的伸縮性。

16. 錯誤

Go不具有傳統意義上的異常機制,也就是說,Go裡沒有同錯誤處理相關的控制結構。(Go的確為類似被零除這樣的異常情況的提供了處理機制。 有一對叫做panic和recover的內建函式,用來讓程式設計師處理這些情況。然而,這些函式是故意弄的不好用因而也很少使用它們,而且也不像Java庫中使用異常那樣,並沒有將它們整合到庫中。)

Go語言中錯誤處理的一個關鍵特性是一個預先定義為error的介面型別,它具有一個返回一個字串讀到Error方法,表示了一個錯誤值。:

Go的庫使用error型別的資料返回對錯誤的描述。結合函式具有返回多個數值的能力, 在返回計算結果的同時返回可能出現的錯誤值很容易實現。比如,Go中同C裡的對應的getchar不會在EOF處返回一個超範圍的值,也不會丟擲異常;它只是返回在返回讀到的字元的同時返回一個error值,以error的值為nil表示讀取成功。以下所示為帶緩衝區的I/O包中bufio.Reader型別的ReadByte方法的簽名:

這樣的設計簡單清晰,也非常容易理解。error僅僅是一種值,程式可以象對其它別的型別的值一樣,對error值進行計算。

Go中不包含異常,是我們故意為之的。雖然有大量的批評者並不同意這個設計決策,但是我們相信有幾個原因讓我們認為這樣做才能編寫出更好的軟體。

首先,計算機程式中的錯誤並不是真正的異常情況。例如,無法開啟一個檔案是種常見的問題,無需任何的特殊語言結構,if和return完全可以勝任。

再者,如果錯誤要使用特殊的控制結構,錯誤處理就會扭曲處理錯誤的程式的控制流(control flow)。象Java那樣try-catch-finally語句結構會形成交叉重疊的多個控制流,這些控制流之間的互動方式非常複雜。雖然相比較而言,Go檢查錯誤的方式更加繁瑣,但這種顯式的設計使得控制流更加直截了當 —— 從字面上的確如此。

毫無疑問這會使程式碼更長一些,但如此編碼帶來的清晰度和簡單性可以彌補其冗長的缺點。顯式地錯誤檢查會迫使程式設計師在錯誤出現的時候對錯誤進行思考並進行相應的處理。異常機制只是將錯誤處理推卸到了呼叫堆疊之中,直到錯過了修復問題或準確診斷錯誤情況的時機,這就使得程式設計師更容易去忽略錯誤而不是處理錯誤了。

17. 工具

軟體工程需要工具的支援。每種語言都要執行於同其它語言共存的環境,它還需要大量工具才能進行編譯、編輯、除錯、效能分析、測試已經執行。

Go的語法、包管理系統、命名規則以及其它功能在設計時就考慮了要易於為這種語言編寫工具以及包括詞法分析器、語法分析器以及型別檢測器等等在內的各種庫。

操作Go程式的工具非常容易編寫,因此現在已經編寫出了許多這樣的工具,其中有些工具對軟體工程來講已經產生了一些值得關注的效果。

其中最著名的是gofmt,它是Go源程式的格式化程式。該專案伊始,我們就將Go程式定位為由機器對其進行格式化, 從而消除了在程式設計師中具有爭議的一大類問題:我要以什麼樣的格式寫程式碼?我們對我們所需的所有Go程式執行Gofmt,絕大多數開源社群也用它進行程式碼格式化。 它是作為“提交前”的例行檢查執行的,它在程式碼提交到程式碼庫之前執行,以確保所有檢入的Go程式都是具有相同的格式。

Go fmt 往往被其使用者推崇為Go最好的特性之一,儘管它本身並屬於Go語言的一個部分。 存在並使用gofmt意味著,從一開始社群裡看到的Go程式碼就是用它進行格式化過的程式碼,因此Go程式具有現在已為人熟知的單一風格。同一的寫法使得程式碼閱讀起來更加容易,因而用起來速度也快。沒有在格式化程式碼方面浪費的時間就是剩下來的時間。Gofmt也會影響伸縮性:既然所有的程式碼看上去格式完全相同,團隊就更易於展開合作,用起別人的程式碼來也更容易。

Go fmt 還讓編寫我們並沒有清晰地預見到的另一類工具成為可能。Gofmt的執行原理就是對原始碼進行語法分析,然後根據語法樹本身對程式碼進行格式化。這讓在格式化程式碼之前對語法樹進行更改成為可能,因此產生了一批進行自動重構的工具。這些工具編寫起來很容易,因為它們直接作用於語法分析樹之上,因而其語義可以非常多樣化,最後產生的格式化程式碼也非常規範。

第一個例子就是gofmt本身的a-r(重寫)標誌,該標誌採用了一種很簡單的模式匹配語言,可以用來進行表示式級的重寫。例如,有一天我們引入了一段表示式右側預設值:該段表示式的長度。整個Go原始碼樹要使用該預設值進行更新,僅限使用下面這一條命令:

該變換中的一個關鍵點在於,因為輸入和輸出二者均為規範格式(canonical format),對原始碼的唯一更改也是語義上的更改

採用與此類似但更復雜一些的處理就可以讓gofmt用於在Go語言中的語句以換行而不再是分號結尾的情況下,對語法樹進行相應的更新。

gofix是另外一個非常重要的工具,它是語法樹重寫模組,而且它用Go語言本身所編寫的,因而可以用來完成更加高階的重構操作。 gofix工具可以用來對直到Go 1釋出為止的所有API和語言特性進行全方位修改,包括修改從map中刪除資料項的語法、引入操作時間值的一個完全不同的API等等很多更新。隨著這些更新一一推出,使用者可以通過執行下面這條簡單的命令對他們的所有程式碼進行更新

注意,這些工具允許我們即使在舊程式碼仍舊能夠正常執行的情況下對它們進行更新。 因此,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的執行效率還有需要改善的地方,它們還在繼續的改善之中。現在已經有了一些進展;實際上,有些基準測試表明,同2012年早期釋出的第一個Go版本1相比,現在開發版的效能已得到雙倍提升。

19. 總結

軟體工程指導下的Go語言的設計。同絕大多數通用型程式語言相比,Go語言更多的是為了解決我們在構建大型伺服器軟體過程中所遇到的軟體工程方面的問題而設計的。 乍看上去,這麼講可能會讓人感覺Go非常無趣且工業化,但實際上,在設計過程中就著重於清晰和簡潔,以及較高的可組合性,最後得到的反而會是一門使用起來效率高而且很有趣的程式語言,很多程式設計師都會發現,它有極強的表達力而且功能非常強大。

造成這種效果的因素有:

  • 清晰的依賴關係
  • 清晰的語法
  • 清晰的語義
  • 偏向組合而不是繼承
  • 程式設計模型(垃圾回收、併發)所代理的簡單性
  • 易於為它編寫工具(Easy tooling )(gotool、gofmt、godoc、gofix)

如果你還沒有嘗試過用Go程式設計,我們建議你試一下。

http://golang.org

 

相關文章